Coverage for  / opt / hostedtoolcache / Python / 3.11.14 / x64 / lib / python3.11 / site-packages / rattlesnake / components / ui_utilities.py: 10%

1196 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-27 18:22 +0000

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

2""" 

3User interface-specific utilities that might be used in multiple environments 

4 

5Rattlesnake Vibration Control Software 

6Copyright (C) 2021 National Technology & Engineering Solutions of Sandia, LLC 

7(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 

25import socket 

26 

27import numpy as np 

28import openpyxl 

29import pyqtgraph 

30import requests 

31from qtpy import QtCore, QtGui, QtWidgets, uic 

32from qtpy.QtCore import Qt, QTimer # pylint: disable=no-name-in-module 

33from scipy.interpolate import interp1d 

34from scipy.io import loadmat 

35 

36from .environments import ( 

37 ControlTypes, 

38 combined_environments_capable, 

39 control_select_ui_path, 

40 environment_long_names, 

41 environment_select_ui_path, 

42 environment_UIs, 

43 modal_mdi_ui_path, 

44 transformation_matrices_ui_path, 

45) 

46from .utilities import ( 

47 DataAcquisitionParameters, 

48 coherence, 

49 error_message_qt, 

50 load_csv_matrix, 

51 save_csv_matrix, 

52 trac, 

53) 

54 

55ACQUISITION_FRAMES_TO_DISPLAY = 4 

56 

57 

58class ProfileTimer(QTimer): 

59 """A timer class that allows storage of controller instruction information""" 

60 

61 def __init__(self, environment: str, operation: str, data: str): 

62 """ 

63 A timer class that allows storage of controller instruction information 

64 

65 When the timer times out, the environment, operation, and any data can 

66 be collected by the callback by accessing the self.sender().environment, 

67 .operation, or .data attributes. 

68 

69 Parameters 

70 ---------- 

71 environment : str 

72 The name of the environment (or 'Global') that the instruction will 

73 be sent to 

74 operation : str 

75 The operation that the environment will be instructed to perform 

76 data : str 

77 Any data corresponding to that operation that is required 

78 

79 

80 """ 

81 super().__init__() 

82 self.environment = environment 

83 self.operation = operation 

84 self.data = data 

85 

86 

87def get_table_strings(tablewidget: QtWidgets.QTableWidget): 

88 """Collect a table of strings from a QTableWidget 

89 

90 Parameters 

91 ---------- 

92 tablewidget : QtWidgets.QTableWidget 

93 A table widget to pull the strings from 

94 

95 Returns 

96 ------- 

97 string_array : list[list[str]] 

98 A nested list of strings from the table items 

99 

100 """ 

101 string_array = [] 

102 for row_idx in range(tablewidget.rowCount()): 

103 string_array.append([]) 

104 for col_idx in range(tablewidget.columnCount()): 

105 value = tablewidget.item(row_idx, col_idx).text() 

106 string_array[-1].append(value) 

107 return string_array 

108 

109 

110def get_table_bools(tablewidget: QtWidgets.QTableWidget): 

111 """Collect a table of booleans from a QTableWidget full of QCheckBoxes 

112 

113 Parameters 

114 ---------- 

115 tablewidget : QtWidgets.QTableWidget 

116 A table widget to pull the strings from 

117 

118 Returns 

119 ------- 

120 bool_array : list[list[bool]] 

121 A nested list of booleans from the table widgets 

122 

123 """ 

124 bool_array = [] 

125 for row_idx in range(tablewidget.rowCount()): 

126 bool_array.append([]) 

127 for col_idx in range(tablewidget.columnCount()): 

128 value = tablewidget.cellWidget(row_idx, col_idx).isChecked() 

129 bool_array[-1].append(value) 

130 return bool_array 

131 

132 

133def load_time_history(signal_path, sample_rate): 

134 """Loads a time history from a given file 

135 

136 The signal can be loaded from numpy files (.npz, .npy) or matlab files (.mat). 

137 For .mat and .npz files, the time data can be included in the file in the 

138 't' field, or it can be excluded and the sample_rate input argument will 

139 be used. If time data is specified, it will be linearly interpolated to the 

140 sample rate of the controller. 

141 For these file types, the signal should be stored in the 'signal' 

142 field. For .npy files, only one array is stored, so it is treated as the 

143 signal, and the sample_rate input argument is used to construct the time 

144 data. 

145 

146 Parameters 

147 ---------- 

148 signal_path : str: 

149 Path to the file from which to load the time history 

150 

151 sample_rate : str: 

152 The sample rate of the loaded signal. 

153 

154 Returns 

155 ------- 

156 signal : np.ndarray: 

157 A signal loaded from the file 

158 

159 """ 

160 _, extension = os.path.splitext(signal_path) 

161 if extension.lower() == ".npy": 

162 signal = np.load(signal_path) 

163 elif extension.lower() == ".npz": 

164 data = np.load(signal_path) 

165 signal = data["signal"] 

166 try: 

167 times = data["t"].squeeze() 

168 fn = interp1d(times, signal) 

169 abscissa = np.arange(0, max(times) + 1 / sample_rate - 1e-10, 1 / sample_rate) 

170 abscissa = abscissa[abscissa <= max(times)] 

171 signal = fn(abscissa) 

172 except KeyError: 

173 pass 

174 elif extension.lower() == ".mat": 

175 data = loadmat(signal_path) 

176 signal = data["signal"] 

177 try: 

178 times = data["t"].squeeze() 

179 fn = interp1d(times, signal) 

180 abscissa = np.arange(0, max(times) + 1 / sample_rate - 1e-10, 1 / sample_rate) 

181 abscissa = abscissa[abscissa <= max(times)] 

182 signal = fn(abscissa) 

183 except KeyError: 

184 pass 

185 else: 

186 raise ValueError( 

187 f"Could Not Determine the file type from the filename {signal_path}: {extension}" 

188 ) 

189 if signal.shape[-1] % 2 == 1: 

190 signal = signal[..., :-1] 

191 return signal 

192 

193 

194colororder = [ 

195 "#1f77b4", 

196 "#ff7f0e", 

197 "#2ca02c", 

198 "#d62728", 

199 "#9467bd", 

200 "#8c564b", 

201 "#e377c2", 

202 "#7f7f7f", 

203 "#bcbd22", 

204 "#17becf", 

205] 

206 

207 

208def multiline_plotter( 

209 x, 

210 y, 

211 widget=None, 

212 curve_list=None, 

213 names=None, 

214 other_pen_options=None, 

215 legend=False, 

216 downsample=None, 

217 clip_to_view=False, 

218): 

219 """Helper function for PyQtGraph to deal with plots with multiple curves 

220 

221 Parameters 

222 ---------- 

223 x : np.ndarray 

224 Abscissa for the data that will be plotted, 1D array with shape n_samples 

225 y : np.ndarray 

226 Ordinates for the data that will be plotted. 2D array with shape 

227 n_curves x n_samples 

228 widget : 

229 The plot widget on which the curves will be drawn. (Default value = None) 

230 curve_list : 

231 Alternatively to specifying the widget, a curve list can be specified 

232 directly. (Default value = None) 

233 names : 

234 Names of the curves that will appear in the legend. (Default value = None) 

235 other_pen_options : dict 

236 Additional options besides color that will be applied to the curves. 

237 (Default value = {'width':1}) 

238 legend : 

239 Whether or not to draw a legend (Default value = False) 

240 

241 Returns 

242 ------- 

243 

244 """ 

245 if other_pen_options is None: 

246 other_pen_options = {"width": 1} 

247 if downsample is None: 

248 downsample = {"ds": 1, "auto": False, "mode": "peak"} 

249 if widget is not None: 

250 plot_item = widget.getPlotItem() 

251 plot_item.setDownsampling(**downsample) 

252 plot_item.setClipToView(clip_to_view) 

253 if legend: 

254 plot_item.addLegend(colCount=len(y) // 10) 

255 handles = [] 

256 for i, this_y in enumerate(y): 

257 pen = {"color": colororder[i % len(colororder)]} 

258 pen.update(other_pen_options) 

259 handles.append( 

260 plot_item.plot(x, this_y, pen=pen, name=None if names is None else names[i]) 

261 ) 

262 return handles 

263 elif curve_list is not None: 

264 for this_y, curve in zip(y, curve_list): 

265 curve.setData(x, y) 

266 return curve_list 

267 else: 

268 raise ValueError("Either Widget or list of curves must be specified") 

269 

270 

271def blended_scatter_plot(xy, widget=None, curve_list=None, names=None, symbol="o"): 

272 """Creates a scatter plot with the specified symbols""" 

273 if widget is not None: 

274 plot_item = widget.getPlotItem() 

275 handles = [] 

276 for index, (x, y) in enumerate(xy): 

277 c = (1 - (index + 1) / len(xy)) * 255 

278 handles.append( 

279 plot_item.plot( 

280 [x], 

281 [y], 

282 symbolBrush=(c, c, c), 

283 name=None if names is None else names[index], 

284 symbol=symbol, 

285 ) 

286 ) 

287 return handles 

288 elif curve_list is not None: 

289 for (x, y), curve in zip(xy, curve_list): 

290 curve.setData([x], [y]) 

291 return curve_list 

292 else: 

293 raise ValueError("Either Widget or list of curves must be specified") 

294 

295 

296def save_combined_environments_profile_template(filename, environment_data): 

297 """Creates a spreadsheet template that can be completed and loaded to define a test""" 

298 # Create the header 

299 workbook = openpyxl.Workbook() 

300 worksheet = workbook.active 

301 worksheet.title = "Channel Table" 

302 hardware_worksheet = workbook.create_sheet("Hardware") 

303 # Create the header 

304 worksheet.cell(row=1, column=2, value="Test Article Definition") 

305 worksheet.merge_cells(start_row=1, start_column=2, end_row=1, end_column=4) 

306 worksheet.cell(row=1, column=5, value="Instrument Definition") 

307 worksheet.merge_cells(start_row=1, start_column=5, end_row=1, end_column=11) 

308 worksheet.cell(row=1, column=12, value="Channel Definition") 

309 worksheet.merge_cells(start_row=1, start_column=12, end_row=1, end_column=19) 

310 worksheet.cell(row=1, column=20, value="Output Feedback") 

311 worksheet.merge_cells(start_row=1, start_column=20, end_row=1, end_column=21) 

312 worksheet.cell(row=1, column=22, value="Limits") 

313 worksheet.merge_cells(start_row=1, start_column=22, end_row=1, end_column=23) 

314 for col_idx, val in enumerate( 

315 [ 

316 "Channel Index", 

317 "Node Number", 

318 "Node Direction", 

319 "Comment", 

320 "Serial Number", 

321 "Triax DoF", 

322 "Sensitivity (mV/EU)", 

323 "Engineering Unit", 

324 "Make", 

325 "Model", 

326 "Calibration Exp Date", 

327 "Physical Device", 

328 "Physical Channel", 

329 "Type", 

330 "Minimum Value (V)", 

331 "Maximum Value (V)", 

332 "Coupling", 

333 "Current Excitation Source", 

334 "Current Excitation Value", 

335 "Physical Device", 

336 "Physical Channel", 

337 "Warning Level (EU)", 

338 "Abort Level (EU)", 

339 ] 

340 ): 

341 worksheet.cell(row=2, column=1 + col_idx, value=val) 

342 # Fill out the hardware worksheet 

343 hardware_worksheet.cell(1, 1, "Hardware Type") 

344 hardware_worksheet.cell(1, 2, "# Enter hardware index here") 

345 hardware_worksheet.cell( 

346 1, 

347 3, 

348 "Hardware Indices: 0 - NI DAQmx; 1 - LAN XI; 2 - Data Physics Quattro; " 

349 "3 - Data Physics 900 Series; 4 - Exodus Modal Solution; 5 - State Space Integration; " 

350 "6 - SDynPy System Integration", 

351 ) 

352 hardware_worksheet.cell(2, 1, "Hardware File") 

353 hardware_worksheet.cell( 

354 2, 

355 2, 

356 "# Path to Hardware File (Depending on Hardware Device: 0 - Not Used; 1 - Not Used; " 

357 "2 - Path to DpQuattro.dll library file; 3 - Not Used; 4 - Path to Exodus Eigensolution; " 

358 "5 - Path to State Space File; 6 - Path to SDynPy system file)", 

359 ) 

360 hardware_worksheet.cell(3, 1, "Sample Rate") 

361 hardware_worksheet.cell(3, 2, "# Sample Rate of Data Acquisition System") 

362 hardware_worksheet.cell(4, 1, "Time Per Read") 

363 hardware_worksheet.cell(4, 2, "# Number of seconds per Read from the Data Acquisition System") 

364 hardware_worksheet.cell(5, 1, "Time Per Write") 

365 hardware_worksheet.cell(5, 2, "# Number of seconds per Write to the Data Acquisition System") 

366 hardware_worksheet.cell(6, 1, "Maximum Acquisition Processes") 

367 hardware_worksheet.cell( 

368 6, 

369 2, 

370 "# Maximum Number of Acquisition Processes to start to pull data from hardware", 

371 ) 

372 hardware_worksheet.cell( 

373 6, 

374 3, 

375 "Only Used by LAN-XI Hardware. This row can be deleted if LAN-XI is not used", 

376 ) 

377 hardware_worksheet.cell(7, 1, "Integration Oversampling") 

378 hardware_worksheet.cell( 

379 7, 2, "# For virtual control, an integration oversampling can be specified" 

380 ) 

381 hardware_worksheet.cell( 

382 7, 

383 3, 

384 "Only used for virtual control (Exodus, State Space, or SDynPy). " 

385 "This row can be deleted if these are not used.", 

386 ) 

387 hardware_worksheet.cell(8, 1, "Task Trigger") 

388 hardware_worksheet.cell(8, 2, "# Start trigger type") 

389 hardware_worksheet.cell( 

390 8, 

391 3, 

392 "Task Triggers: 0 - Internal, 1 - PFI0 with external trigger, 2 - PFI0 with Analog Output " 

393 "trigger. Only used for NI hardware. This row can be deleted if NI is not used.", 

394 ) 

395 hardware_worksheet.cell(9, 1, "Task Trigger Output Channel") 

396 hardware_worksheet.cell(9, 2, "# Physical device and channel that generates a trigger signal") 

397 hardware_worksheet.cell( 

398 9, 

399 3, 

400 "Only used if Task Triggers is 2. Only used for NI hardware. " 

401 "This row can be deleted if it is not used.", 

402 ) 

403 

404 # Now do the environment 

405 worksheet.cell(row=1, column=24, value="Environments") 

406 for row, (value, name) in enumerate(environment_data): 

407 environment_UIs[value].create_environment_template(name, workbook) 

408 worksheet.cell(row=2, column=24 + row, value=name) 

409 # Now create a profile page 

410 profile_sheet = workbook.create_sheet("Test Profile") 

411 profile_sheet.cell(1, 1, "Time (s)") 

412 profile_sheet.cell(1, 2, "Environment") 

413 profile_sheet.cell(1, 3, "Operation") 

414 profile_sheet.cell(1, 4, "Data") 

415 

416 workbook.save(filename) 

417 

418 

419class EnvironmentSelect(QtWidgets.QDialog): 

420 """QDialog for selecting the environments in a combined environments run""" 

421 

422 def __init__(self, parent=None): 

423 """ 

424 Constructor for the EnvironmentSelect dialog box. 

425 

426 Parameters 

427 ---------- 

428 parent : QWidget, optional 

429 Parent widget to the dialog. The default is None. 

430 

431 """ 

432 super(QtWidgets.QDialog, self).__init__(parent) 

433 uic.loadUi(environment_select_ui_path, self) 

434 self.setWindowIcon(QtGui.QIcon("logo/Rattlesnake_Icon.png")) 

435 

436 self.add_environment_button.clicked.connect(self.add_environment) 

437 self.remove_environment_button.clicked.connect(self.remove_environment) 

438 self.load_profile_button.clicked.connect(self.load_profile) 

439 self.save_profile_template_button.clicked.connect(self.save_profile_template) 

440 self.loaded_profile = None 

441 

442 def add_environment(self): 

443 """Adds a row to the environment table""" 

444 selected_row = self.environment_display_table.rowCount() 

445 self.environment_display_table.insertRow(selected_row) 

446 combobox = QtWidgets.QComboBox() 

447 for control_type in combined_environments_capable: 

448 combobox.addItem(control_type.name.title(), control_type.value) 

449 self.environment_display_table.setCellWidget(selected_row, 0, combobox) 

450 

451 def remove_environment(self): 

452 """Removes a row from the environment table""" 

453 selected_row = self.environment_display_table.currentRow() 

454 if selected_row >= 0: 

455 self.environment_display_table.removeRow(selected_row) 

456 

457 def save_profile_template(self): 

458 """Saves a template for the given environments table 

459 

460 This template can be filled out by a user and then loaded.""" 

461 filename, _ = QtWidgets.QFileDialog.getSaveFileName( 

462 self, "Save Combined Environments Template", filter="Excel File (*.xlsx)" 

463 ) 

464 if filename == "": 

465 return 

466 # Now do the environments 

467 environment_data = [] 

468 for row in range(self.environment_display_table.rowCount()): 

469 combobox = self.environment_display_table.cellWidget(row, 0) 

470 value = ControlTypes(combobox.currentData()) 

471 name = self.environment_display_table.item(row, 1).text() 

472 environment_data.append((value, name)) 

473 save_combined_environments_profile_template(filename, environment_data) 

474 

475 def load_profile(self): 

476 """Loads a profile from an excel spreadsheet.""" 

477 filename, _ = QtWidgets.QFileDialog.getOpenFileName( 

478 self, "Load Combined Environments Profile", filter="Excel File (*.xlsx)" 

479 ) 

480 if filename == "": 

481 return 

482 else: 

483 self.loaded_profile = filename 

484 self.accept() 

485 

486 @staticmethod 

487 def select_environment(parent=None): 

488 """Creates the dialog box and then parses the output. 

489 

490 Note that there are variable numbers of outputs for this function 

491 

492 Parameters 

493 ---------- 

494 parent : QWidget 

495 Parent to the dialog box (Default value = None) 

496 

497 Returns 

498 ------- 

499 result : int 

500 A flag specifying the outcome of the dialog box. Will be 1 if the 

501 dialog was accepted, zero if cancelled, and -1 if a profile was 

502 loaded instead. 

503 environment_table : list of lists 

504 A list of environment type, environment name pairs that will be 

505 used to specify the environments in a test. 

506 loaded_profile : str 

507 File name to the profile file that needs to be loaded. Only 

508 output if result == -1 

509 """ 

510 dialog = EnvironmentSelect(parent) 

511 result = 1 if (dialog.exec_() == QtWidgets.QDialog.Accepted) else 0 

512 if dialog.loaded_profile is None: 

513 environment_table = [] 

514 if result: 

515 for row in range(dialog.environment_display_table.rowCount()): 

516 combobox = dialog.environment_display_table.cellWidget(row, 0) 

517 value = ControlTypes(combobox.currentData()) 

518 name = dialog.environment_display_table.item(row, 1).text() 

519 environment_table.append([value, name]) 

520 # print(environment_table) 

521 return result, environment_table 

522 else: 

523 result = -1 

524 workbook = openpyxl.load_workbook(dialog.loaded_profile) 

525 environment_sheets = [ 

526 sheet 

527 for sheet in workbook 

528 if (sheet.title not in ["Channel Table", "Hardware", "Test Profile"]) 

529 and sheet.cell(1, 1).value == "Control Type" 

530 ] 

531 environment_table = [ 

532 (ControlTypes[sheet.cell(1, 2).value.upper()], sheet.title) 

533 for sheet in environment_sheets 

534 ] 

535 workbook.close() 

536 return result, environment_table, dialog.loaded_profile 

537 

538 

539class ControlSelect(QtWidgets.QDialog): 

540 """Environment selector dialog box to select the control type for the test""" 

541 

542 def __init__(self, parent=None): 

543 """ 

544 Selects the environment type that gets used for the test. 

545 

546 This function reads from the environment control types to populate the 

547 radiobuttons on the dialog. 

548 

549 Parameters 

550 ---------- 

551 parent : QWidget, optional 

552 Parent of the dialog box. The default is None. 

553 

554 """ 

555 super(QtWidgets.QDialog, self).__init__(parent) 

556 uic.loadUi(control_select_ui_path, self) 

557 self.setWindowIcon(QtGui.QIcon("logo/Rattlesnake_Icon.png")) 

558 

559 self.buttonBox.accepted.connect(self.accept) 

560 self.buttonBox.rejected.connect(self.reject) 

561 self.control_select_buttongroup = QtWidgets.QButtonGroup() 

562 

563 # Go through and create radiobuttons for each control type 

564 control_types_sorted = sorted( 

565 [(control_type.value, control_type) for control_type in ControlTypes] 

566 ) 

567 

568 for value, control_type in control_types_sorted[1:] + control_types_sorted[:1]: 

569 radiobutton = QtWidgets.QRadioButton(environment_long_names[control_type]) 

570 self.control_select_buttongroup.addButton(radiobutton, value) 

571 if value == ControlTypes.RANDOM.value: 

572 radiobutton.setChecked(True) 

573 self.environment_radiobutton_layout.addWidget(radiobutton) 

574 

575 @staticmethod 

576 def select_control(parent=None): 

577 """Create the dialog box and parse the output 

578 

579 Parameters 

580 ---------- 

581 parent : QWidget 

582 Parent of the dialog box (Default value = None) 

583 

584 Returns 

585 ------- 

586 button_id : int 

587 The index of the button that was pressed 

588 result : bool 

589 True if dialog was accepted, otherwise false if cancelled. 

590 """ 

591 dialog = ControlSelect(parent) 

592 result = dialog.exec_() == QtWidgets.QDialog.Accepted 

593 index = dialog.control_select_buttongroup.checkedId() 

594 button_id = ControlTypes(index) 

595 # print(button_id) 

596 return (button_id, result) 

597 

598 

599class PlotWindow(QtWidgets.QDialog): 

600 """Class defining a subwindow that displays specific channel information""" 

601 

602 WARNING_COLOR = (225, 190, 0) 

603 WARNING_LINEWIDTH = 0.3 

604 WARNING_LINESTYLE = Qt.SolidLine 

605 ABORT_COLOR = (200, 0, 0) 

606 ABORT_LINEWIDTH = 0.3 

607 ABORT_LINESTYLE = Qt.SolidLine 

608 

609 def __init__( 

610 self, 

611 parent, 

612 row, 

613 column, 

614 datatype, 

615 specification, 

616 row_name, 

617 column_name, 

618 datatype_name, 

619 warning_matrix=None, 

620 abort_matrix=None, 

621 ): 

622 """ 

623 Creates a window showing CPSD matrix information for a single channel. 

624 

625 Parameters 

626 ---------- 

627 parent : QWidget 

628 Parent of the window. 

629 row : int 

630 Row of the CPSD matrix to plot. 

631 column : int 

632 Column of the CPSD matrix to plot. 

633 datatype : int 

634 Type of data to plot: 0 - Magnitude, 1 - Coherence, 2 - Phase, 3 - 

635 Real, 4 - Imaginary. 

636 specification : np.ndarray 

637 The specification against which data will be compared. 

638 row_name : str 

639 Channel name for the row. 

640 column_name : str 

641 Channel name for the column. 

642 datatype_name : str 

643 Name for the datatype. 

644 

645 

646 """ 

647 super(QtWidgets.QDialog, self).__init__(parent) 

648 self.setWindowFlags(self.windowFlags() & Qt.Tool) 

649 self.row = row 

650 self.column = column 

651 self.datatype = datatype 

652 self.frequencies = specification[0] 

653 self.spec_data = self.reduce_matrix(specification[1]) 

654 self.data = np.zeros(self.spec_data.shape) 

655 # Now plot the data 

656 layout = QtWidgets.QVBoxLayout() 

657 plotwidget = pyqtgraph.PlotWidget() 

658 layout.addWidget(plotwidget) 

659 self.setLayout(layout) 

660 plot_item = plotwidget.getPlotItem() 

661 plot_item.showGrid(True, True, 0.25) 

662 plot_item.enableAutoRange() 

663 plot_item.getViewBox().enableAutoRange(enable=True) 

664 if self.datatype == 0: 

665 plot_item.setLogMode(False, True) 

666 plot_item.plot(self.frequencies, self.spec_data, pen={"color": "b", "width": 1}) 

667 if warning_matrix is not None: 

668 plot_item.plot( 

669 self.frequencies, 

670 warning_matrix[0, :, row], 

671 pen={ 

672 "color": PlotWindow.WARNING_COLOR, 

673 "width": PlotWindow.WARNING_LINEWIDTH, 

674 "style": PlotWindow.WARNING_LINESTYLE, 

675 }, 

676 ) 

677 plot_item.plot( 

678 self.frequencies, 

679 warning_matrix[1, :, row], 

680 pen={ 

681 "color": PlotWindow.WARNING_COLOR, 

682 "width": PlotWindow.WARNING_LINEWIDTH, 

683 "style": PlotWindow.WARNING_LINESTYLE, 

684 }, 

685 ) 

686 if abort_matrix is not None: 

687 plot_item.plot( 

688 self.frequencies, 

689 abort_matrix[0, :, row], 

690 pen={ 

691 "color": PlotWindow.ABORT_COLOR, 

692 "width": PlotWindow.ABORT_LINEWIDTH, 

693 "style": PlotWindow.ABORT_LINESTYLE, 

694 }, 

695 ) 

696 plot_item.plot( 

697 self.frequencies, 

698 abort_matrix[1, :, row], 

699 pen={ 

700 "color": PlotWindow.ABORT_COLOR, 

701 "width": PlotWindow.ABORT_LINEWIDTH, 

702 "style": PlotWindow.ABORT_LINESTYLE, 

703 }, 

704 ) 

705 self.curve = plot_item.plot(self.frequencies, self.data, pen={"color": "r", "width": 1}) 

706 self.setWindowTitle(f"{datatype_name} {row_name} / {column_name}") 

707 self.show() 

708 

709 def reduce_matrix(self, matrix): 

710 """Collects the data specific to the row and column and datatype 

711 

712 Parameters 

713 ---------- 

714 matrix : np.ndarray 

715 The 3D CPSD data that will be reduced 

716 

717 Returns 

718 ------- 

719 plot_data : np.ndarray 

720 The data that will be plotted 

721 

722 """ 

723 if self.datatype == 0: # Magnitude 

724 return np.abs(matrix[..., self.row, self.column]) 

725 elif self.datatype == 1: # Coherence 

726 return coherence(matrix, (self.row, self.column)) 

727 elif self.datatype == 2: # Phase 

728 return np.angle(matrix[..., self.row, self.column]) 

729 elif self.datatype == 3: # Real 

730 return np.real(matrix[..., self.row, self.column]) 

731 elif self.datatype == 4: # Imag 

732 return np.imag(matrix[..., self.row, self.column]) 

733 else: 

734 raise ValueError(f"{self.datatype} is not a valid datatype!") 

735 

736 def update_plot(self, cpsd_matrix): 

737 """Updates the plot with the given CPSD matrix data 

738 

739 Parameters 

740 ---------- 

741 cpsd_matrix : np.ndarray 

742 3D CPSD matrix that will be reduced for plotting 

743 

744 """ 

745 self.curve.setData(self.frequencies, self.reduce_matrix(cpsd_matrix)) 

746 

747 

748class PlotTimeWindow(QtWidgets.QDialog): 

749 """Class defining a subwindow that displays specific channel information""" 

750 

751 def __init__(self, parent, index, specification, sample_rate, index_name): 

752 """ 

753 Creates a window showing time history information for a single channel. 

754 

755 Parameters 

756 ---------- 

757 parent : QWidget 

758 Parent of the window. 

759 index : int 

760 Row of the time history matrix to plot 

761 specification : np.ndarray 

762 The specification against which data will be compared. 

763 sample_rate : int 

764 The sample rate of the time signal 

765 index_name : str 

766 Channel name for the row. 

767 """ 

768 super(QtWidgets.QDialog, self).__init__(parent) 

769 self.setWindowFlags(self.windowFlags() & Qt.Tool) 

770 self.index = index 

771 self.times = np.arange(specification.shape[-1]) / sample_rate 

772 self.spec_data = self.reduce_matrix(specification) 

773 self.data = np.zeros(self.spec_data.shape) 

774 # Now plot the data 

775 layout = QtWidgets.QVBoxLayout() 

776 plotwidget = pyqtgraph.PlotWidget() 

777 layout.addWidget(plotwidget) 

778 self.setLayout(layout) 

779 plot_item = plotwidget.getPlotItem() 

780 plot_item.showGrid(True, True, 0.25) 

781 plot_item.enableAutoRange() 

782 plot_item.getViewBox().enableAutoRange(enable=True) 

783 plot_item.plot(self.times, self.spec_data, pen={"color": "b", "width": 1}) 

784 plot_item.setLabel("left", "TRAC: 0.0") 

785 self.plot_item = plot_item 

786 self.curve = plot_item.plot(self.times, self.data, pen={"color": "r", "width": 1}) 

787 self.setWindowTitle(f"{index_name}") 

788 self.show() 

789 

790 def reduce_matrix(self, matrix): 

791 """Collects the data specific to the row and column and datatype 

792 

793 Parameters 

794 ---------- 

795 matrix : np.ndarray 

796 The 3D CPSD data that will be reduced 

797 

798 Returns 

799 ------- 

800 plot_data : np.ndarray 

801 The data that will be plotted 

802 

803 """ 

804 return matrix[self.index] 

805 

806 def update_plot(self, data): 

807 """Updates the plot with the given CPSD matrix data 

808 

809 Parameters 

810 ---------- 

811 cpsd_matrix : np.ndarray 

812 3D CPSD matrix that will be reduced for plotting 

813 

814 """ 

815 data = self.reduce_matrix(data) 

816 self.curve.setData(self.times, data) 

817 self.plot_item.setLabel("left", f"TRAC: {trac(data, self.spec_data).squeeze():0.2f}") 

818 

819 

820class TransformationMatrixWindow(QtWidgets.QDialog): 

821 """Dialog box for specifying transformation matrices""" 

822 

823 def __init__( 

824 self, 

825 parent, 

826 current_response_transformation_matrix, 

827 num_responses, 

828 current_output_transformation_matrix, 

829 num_outputs, 

830 ): 

831 """ 

832 Creates a dialog box for specifying response and output transformations 

833 

834 Parameters 

835 ---------- 

836 parent : QWidget 

837 Parent to the dialog box. 

838 current_response_transformation_matrix : np.ndarray 

839 The current value of the transformation matrix that will be used to 

840 populate the entries in the table. 

841 num_responses : int 

842 Number of physical responses in the transformation. 

843 current_output_transformation_matrix : np.ndarray 

844 The current value of the transformation matrix that will be used to 

845 populate the entries in the table. 

846 num_outputs : int 

847 Number of physical outputs in the transformation. 

848 

849 """ 

850 super().__init__(parent) 

851 uic.loadUi(transformation_matrices_ui_path, self) 

852 self.setWindowTitle("Transformation Matrix Definition") 

853 

854 self.response_transformation_matrix.setColumnCount(num_responses) 

855 self.output_transformation_matrix.setColumnCount(num_outputs) 

856 

857 if current_response_transformation_matrix is None: 

858 self.set_response_transformation_identity() 

859 else: 

860 self.response_transformation_matrix.setRowCount( 

861 current_response_transformation_matrix.shape[0] 

862 ) 

863 for row_idx, row in enumerate(current_response_transformation_matrix): 

864 for col_idx, col in enumerate(row): 

865 try: 

866 self.response_transformation_matrix.item(row_idx, col_idx).setText(str(col)) 

867 except AttributeError: 

868 item = QtWidgets.QTableWidgetItem(str(col)) 

869 self.response_transformation_matrix.setItem(row_idx, col_idx, item) 

870 if current_output_transformation_matrix is None: 

871 self.set_output_transformation_identity() 

872 else: 

873 self.output_transformation_matrix.setRowCount( 

874 current_output_transformation_matrix.shape[0] 

875 ) 

876 for row_idx, row in enumerate(current_output_transformation_matrix): 

877 for col_idx, col in enumerate(row): 

878 try: 

879 self.output_transformation_matrix.item(row_idx, col_idx).setText(str(col)) 

880 except AttributeError: 

881 item = QtWidgets.QTableWidgetItem(str(col)) 

882 self.output_transformation_matrix.setItem(row_idx, col_idx, item) 

883 

884 # Callbacks 

885 self.ok_button.clicked.connect(self.accept) 

886 self.cancel_button.clicked.connect(self.reject) 

887 

888 self.response_transformation_add_row_button.clicked.connect( 

889 self.response_transformation_add_row 

890 ) 

891 self.response_transformation_remove_row_button.clicked.connect( 

892 self.response_transformation_remove_row 

893 ) 

894 self.response_transformation_save_matrix_button.clicked.connect( 

895 self.save_response_transformation_matrix 

896 ) 

897 self.response_transformation_load_matrix_button.clicked.connect( 

898 self.load_response_transformation_matrix 

899 ) 

900 self.response_transformation_identity_button.clicked.connect( 

901 self.set_response_transformation_identity 

902 ) 

903 self.response_transformation_6dof_kinematic_button.clicked.connect( 

904 self.set_response_transformation_6dof 

905 ) 

906 self.response_transformation_reversed_6dof_kinematic_button.clicked.connect( 

907 self.set_response_transformation_6dof_reversed 

908 ) 

909 

910 self.output_transformation_add_row_button.clicked.connect( 

911 self.output_transformation_add_row 

912 ) 

913 self.output_transformation_remove_row_button.clicked.connect( 

914 self.output_transformation_remove_row 

915 ) 

916 self.output_transformation_save_matrix_button.clicked.connect( 

917 self.save_output_transformation_matrix 

918 ) 

919 self.output_transformation_load_matrix_button.clicked.connect( 

920 self.load_output_transformation_matrix 

921 ) 

922 self.output_transformation_identity_button.clicked.connect( 

923 self.set_output_transformation_identity 

924 ) 

925 self.output_transformation_6dof_kinematic_button.clicked.connect( 

926 self.set_output_transformation_6dof 

927 ) 

928 self.output_transformation_reversed_6dof_kinematic_button.clicked.connect( 

929 self.set_output_transformation_6dof_reversed 

930 ) 

931 

932 @staticmethod 

933 def define_transformation_matrices( 

934 current_response_transformation_matrix, 

935 num_responses, 

936 current_output_transformation_matrix, 

937 num_outputs, 

938 parent=None, 

939 ): 

940 """ 

941 Shows the dialog and returns the transformation matrices 

942 

943 Parameters 

944 ---------- 

945 current_response_transformation_matrix : np.ndarray 

946 The current value of the transformation matrix that will be used to 

947 populate the entries in the table. 

948 num_responses : int 

949 Number of physical responses in the transformation. 

950 current_output_transformation_matrix : np.ndarray 

951 The current value of the transformation matrix that will be used to 

952 populate the entries in the table. 

953 num_outputs : int 

954 Number of physical outputs in the transformation. 

955 parent : QWidget 

956 Parent to the dialog box. (Default value = None) 

957 

958 Returns 

959 ------- 

960 response_transformation : np.ndarray 

961 Response transformation (or None if Identity) 

962 output_transformation : np.ndarray 

963 Output transformation (or None if Identity) 

964 result : bool 

965 True if dialog was accepted, false if cancelled. 

966 """ 

967 dialog = TransformationMatrixWindow( 

968 parent, 

969 current_response_transformation_matrix, 

970 num_responses, 

971 current_output_transformation_matrix, 

972 num_outputs, 

973 ) 

974 result = dialog.exec_() == QtWidgets.QDialog.Accepted 

975 response_transformation = np.array( 

976 [ 

977 [float(val) for val in row] 

978 for row in get_table_strings(dialog.response_transformation_matrix) 

979 ] 

980 ) 

981 if all( 

982 val == response_transformation.shape[0] for val in response_transformation.shape 

983 ) and np.allclose(response_transformation, np.eye(response_transformation.shape[0])): 

984 response_transformation = None 

985 output_transformation = np.array( 

986 [ 

987 [float(val) for val in row] 

988 for row in get_table_strings(dialog.output_transformation_matrix) 

989 ] 

990 ) 

991 if all( 

992 val == output_transformation.shape[0] for val in output_transformation.shape 

993 ) and np.allclose(output_transformation, np.eye(output_transformation.shape[0])): 

994 output_transformation = None 

995 return (response_transformation, output_transformation, result) 

996 

997 def response_transformation_add_row(self): 

998 """Adds a row to the response transformation""" 

999 num_rows = self.response_transformation_matrix.rowCount() 

1000 self.response_transformation_matrix.insertRow(num_rows) 

1001 for col_idx in range(self.response_transformation_matrix.columnCount()): 

1002 item = QtWidgets.QTableWidgetItem("0.0") 

1003 self.response_transformation_matrix.setItem(num_rows, col_idx, item) 

1004 

1005 def response_transformation_remove_row(self): 

1006 """Removes a row from the response transformation""" 

1007 num_rows = self.response_transformation_matrix.rowCount() 

1008 self.response_transformation_matrix.removeRow(num_rows - 1) 

1009 

1010 def save_response_transformation_matrix(self): 

1011 """Saves the response transformation matrix to a csv file""" 

1012 string_array = self.get_table_strings(self.response_transformation_matrix) 

1013 filename, _ = QtWidgets.QFileDialog.getSaveFileName( 

1014 self, 

1015 "Save Response Transformation", 

1016 filter="Comma-separated Values (*.csv)", 

1017 ) 

1018 if filename == "": 

1019 return 

1020 save_csv_matrix(string_array, filename) 

1021 

1022 def load_response_transformation_matrix(self): 

1023 """Loads the response transformation from a csv file""" 

1024 filename, _ = QtWidgets.QFileDialog.getOpenFileName( 

1025 self, 

1026 "Load Response Transformation", 

1027 filter="Comma-separated values (*.csv *.txt);;" 

1028 "Numpy Files (*.npy *.npz);;Matlab Files (*.mat)", 

1029 ) 

1030 if filename == "": 

1031 return 

1032 _, extension = os.path.splitext(filename) 

1033 string_array = None 

1034 if extension.lower() == ".npy": 

1035 string_array = np.load(filename).astype("U") 

1036 elif extension.lower() == ".npz": 

1037 data = np.load(filename) 

1038 for key, array in data.items(): 

1039 string_array = array.astype("U") 

1040 break 

1041 elif extension.lower() == ".mat": 

1042 data = loadmat(filename) 

1043 for key, array in data.items(): 

1044 if "__" in key: 

1045 continue 

1046 string_array = array.astype("U") 

1047 break 

1048 else: 

1049 string_array = load_csv_matrix(filename) 

1050 if string_array is None: 

1051 return 

1052 # Set the number of rows 

1053 self.response_transformation_matrix.setRowCount(len(string_array)) 

1054 num_rows = self.response_transformation_matrix.rowCount() 

1055 num_cols = self.response_transformation_matrix.columnCount() 

1056 for row_idx, row in enumerate(string_array): 

1057 if row_idx == num_rows: 

1058 break 

1059 for col_idx, value in enumerate(row): 

1060 if col_idx == num_cols: 

1061 break 

1062 try: 

1063 self.response_transformation_matrix.item(row_idx, col_idx).setText(value) 

1064 except AttributeError: 

1065 item = QtWidgets.QTableWidgetItem(value) 

1066 self.response_transformation_matrix.setItem(row_idx, col_idx, item) 

1067 

1068 def set_response_transformation_identity(self): 

1069 """Sets the response transformation to identity matrix (no transform)""" 

1070 num_columns = self.response_transformation_matrix.columnCount() 

1071 self.response_transformation_matrix.setRowCount(num_columns) 

1072 for row_idx in range(num_columns): 

1073 for col_idx in range(num_columns): 

1074 if row_idx == col_idx: 

1075 value = 1.0 

1076 else: 

1077 value = 0.0 

1078 try: 

1079 self.response_transformation_matrix.item(row_idx, col_idx).setText(str(value)) 

1080 except AttributeError: 

1081 item = QtWidgets.QTableWidgetItem(str(value)) 

1082 self.response_transformation_matrix.setItem(row_idx, col_idx, item) 

1083 

1084 def set_response_transformation_6dof(self): 

1085 """Sets the response transformation matrix to the 6DoF table""" 

1086 num_columns = self.response_transformation_matrix.columnCount() 

1087 if num_columns != 12: 

1088 error_message_qt( 

1089 "Invalid Number of Control Channels.", 

1090 "Invalid Number of Control Channels. " 

1091 "6DoF Transform assumes 12 control accelerometer channels.", 

1092 ) 

1093 return 

1094 self.response_transformation_matrix.setRowCount(6) 

1095 matrix = [ 

1096 [0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0], 

1097 [0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0], 

1098 [0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25], 

1099 [0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, -0.25, 0.0, 0.0, -0.25], 

1100 [0.0, 0.0, -0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, -0.25], 

1101 [ 

1102 -0.125, 

1103 0.125, 

1104 0.0, 

1105 -0.125, 

1106 -0.125, 

1107 0.0, 

1108 0.125, 

1109 -0.125, 

1110 0.0, 

1111 0.125, 

1112 0.125, 

1113 0.0, 

1114 ], 

1115 ] 

1116 for row_idx, row in enumerate(matrix): 

1117 for col_idx, value in enumerate(row): 

1118 try: 

1119 self.response_transformation_matrix.item(row_idx, col_idx).setText(str(value)) 

1120 except AttributeError: 

1121 item = QtWidgets.QTableWidgetItem(str(value)) 

1122 self.response_transformation_matrix.setItem(row_idx, col_idx, item) 

1123 

1124 def set_response_transformation_6dof_reversed(self): 

1125 """Sets the response transformation matrix to the 6DoF table""" 

1126 num_columns = self.response_transformation_matrix.columnCount() 

1127 if num_columns != 12: 

1128 error_message_qt( 

1129 "Invalid Number of Control Channels.", 

1130 "Invalid Number of Control Channels. " 

1131 "6DoF Transform assumes 12 control accelerometer channels.", 

1132 ) 

1133 return 

1134 self.response_transformation_matrix.setRowCount(6) 

1135 matrix = [ 

1136 [0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0], 

1137 [0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0], 

1138 [0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25], 

1139 [0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, -0.25, 0.0, 0.0, -0.25], 

1140 [0.0, 0.0, -0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, -0.25], 

1141 [ 

1142 -0.125, 

1143 0.125, 

1144 0.0, 

1145 -0.125, 

1146 -0.125, 

1147 0.0, 

1148 0.125, 

1149 -0.125, 

1150 0.0, 

1151 0.125, 

1152 0.125, 

1153 0.0, 

1154 ], 

1155 ] 

1156 for row_idx, row in enumerate(matrix): 

1157 for col_idx, value in enumerate(row): 

1158 try: 

1159 self.response_transformation_matrix.item(row_idx, col_idx).setText(str(value)) 

1160 except AttributeError: 

1161 item = QtWidgets.QTableWidgetItem(str(value)) 

1162 self.response_transformation_matrix.setItem(row_idx, col_idx, item) 

1163 

1164 def output_transformation_add_row(self): 

1165 """Adds a row to the output transformation""" 

1166 num_rows = self.output_transformation_matrix.rowCount() 

1167 self.output_transformation_matrix.insertRow(num_rows) 

1168 for col_idx in range(self.output_transformation_matrix.columnCount()): 

1169 item = QtWidgets.QTableWidgetItem("0.0") 

1170 self.output_transformation_matrix.setItem(num_rows, col_idx, item) 

1171 

1172 def output_transformation_remove_row(self): 

1173 """Removes a row from the output tranformation""" 

1174 num_rows = self.output_transformation_matrix.rowCount() 

1175 self.output_transformation_matrix.removeRow(num_rows - 1) 

1176 

1177 def save_output_transformation_matrix(self): 

1178 """Saves output transformation matrix to a CSV file""" 

1179 string_array = self.get_table_strings(self.output_transformation_matrix) 

1180 filename, _ = QtWidgets.QFileDialog.getSaveFileName( 

1181 self, "Save Output Transformation", filter="Comma-separated Values (*.csv)" 

1182 ) 

1183 if filename == "": 

1184 return 

1185 save_csv_matrix(string_array, filename) 

1186 

1187 def load_output_transformation_matrix(self): 

1188 """Loads the output transformation from a CSV file""" 

1189 filename, _ = QtWidgets.QFileDialog.getOpenFileName( 

1190 self, 

1191 "Load Output Transformation", 

1192 filter="Comma-separated values (*.csv *.txt);;" 

1193 "Numpy Files (*.npy *.npz);;Matlab Files (*.mat)", 

1194 ) 

1195 if filename == "": 

1196 return 

1197 _, extension = os.path.splitext(filename) 

1198 string_array = None 

1199 if extension.lower() == ".npy": 

1200 string_array = np.load(filename).astype("U") 

1201 elif extension.lower() == ".npz": 

1202 data = np.load(filename) 

1203 for key, array in data.items(): 

1204 string_array = array.astype("U") 

1205 break 

1206 elif extension.lower() == ".mat": 

1207 data = loadmat(filename) 

1208 for key, array in data.items(): 

1209 if "__" in key: 

1210 continue 

1211 string_array = array.astype("U") 

1212 break 

1213 else: 

1214 string_array = load_csv_matrix(filename) 

1215 if string_array is None: 

1216 return 

1217 # Set the number of rows 

1218 self.output_transformation_matrix.setRowCount(len(string_array)) 

1219 num_rows = self.output_transformation_matrix.rowCount() 

1220 num_cols = self.output_transformation_matrix.columnCount() 

1221 for row_idx, row in enumerate(string_array): 

1222 if row_idx == num_rows: 

1223 break 

1224 for col_idx, value in enumerate(row): 

1225 if col_idx == num_cols: 

1226 break 

1227 try: 

1228 self.output_transformation_matrix.item(row_idx, col_idx).setText(value) 

1229 except AttributeError: 

1230 item = QtWidgets.QTableWidgetItem(value) 

1231 self.output_transformation_matrix.setItem(row_idx, col_idx, item) 

1232 

1233 def set_output_transformation_identity(self): 

1234 """Sets the output transformation to identity (no transform)""" 

1235 num_columns = self.output_transformation_matrix.columnCount() 

1236 self.output_transformation_matrix.setRowCount(num_columns) 

1237 for row_idx in range(num_columns): 

1238 for col_idx in range(num_columns): 

1239 if row_idx == col_idx: 

1240 value = 1.0 

1241 else: 

1242 value = 0.0 

1243 try: 

1244 self.output_transformation_matrix.item(row_idx, col_idx).setText(str(value)) 

1245 except AttributeError: 

1246 item = QtWidgets.QTableWidgetItem(str(value)) 

1247 self.output_transformation_matrix.setItem(row_idx, col_idx, item) 

1248 

1249 def set_output_transformation_6dof(self): 

1250 """Sets the output transformation matrix to the 6DoF table""" 

1251 num_columns = self.output_transformation_matrix.columnCount() 

1252 if num_columns != 12: 

1253 error_message_qt( 

1254 "Invalid Number of Output Signals.", 

1255 "Invalid Number of Output Signals. 6DoF Transform assumes 12 drive channels.", 

1256 ) 

1257 return 

1258 self.output_transformation_matrix.setRowCount(6) 

1259 matrix = [ 

1260 [0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0], 

1261 [0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0], 

1262 [0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25], 

1263 [0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, -0.25, 0.0, 0.0, -0.25], 

1264 [0.0, 0.0, -0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, -0.25], 

1265 [ 

1266 -0.125, 

1267 0.125, 

1268 0.0, 

1269 -0.125, 

1270 -0.125, 

1271 0.0, 

1272 0.125, 

1273 -0.125, 

1274 0.0, 

1275 0.125, 

1276 0.125, 

1277 0.0, 

1278 ], 

1279 ] 

1280 for row_idx, row in enumerate(matrix): 

1281 for col_idx, value in enumerate(row): 

1282 try: 

1283 self.output_transformation_matrix.item(row_idx, col_idx).setText(str(value)) 

1284 except AttributeError: 

1285 item = QtWidgets.QTableWidgetItem(str(value)) 

1286 self.output_transformation_matrix.setItem(row_idx, col_idx, item) 

1287 

1288 def set_output_transformation_6dof_reversed(self): 

1289 """Sets the output transformation matrix to the 6DoF table""" 

1290 num_columns = self.output_transformation_matrix.columnCount() 

1291 if num_columns != 12: 

1292 error_message_qt( 

1293 "Invalid Number of Output Signals.", 

1294 "Invalid Number of Output Signals. 6DoF Transform assumes 12 drive channels.", 

1295 ) 

1296 return 

1297 self.output_transformation_matrix.setRowCount(6) 

1298 matrix = [ 

1299 [0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0], 

1300 [0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0], 

1301 [0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25], 

1302 [0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, -0.25, 0.0, 0.0, -0.25], 

1303 [0.0, 0.0, -0.25, 0.0, 0.0, 0.25, 0.0, 0.0, 0.25, 0.0, 0.0, -0.25], 

1304 [ 

1305 -0.125, 

1306 0.125, 

1307 0.0, 

1308 -0.125, 

1309 -0.125, 

1310 0.0, 

1311 0.125, 

1312 -0.125, 

1313 0.0, 

1314 0.125, 

1315 0.125, 

1316 0.0, 

1317 ], 

1318 ] 

1319 for row_idx, row in enumerate(matrix): 

1320 for col_idx, value in enumerate(row): 

1321 try: 

1322 self.output_transformation_matrix.item(row_idx, col_idx).setText(str(value)) 

1323 except AttributeError: 

1324 item = QtWidgets.QTableWidgetItem(str(value)) 

1325 self.output_transformation_matrix.setItem(row_idx, col_idx, item) 

1326 

1327 

1328class ModalMDISubWindow(QtWidgets.QWidget): 

1329 """A window that shows modal data""" 

1330 

1331 def __init__(self, parent): 

1332 super().__init__(parent) 

1333 uic.loadUi(modal_mdi_ui_path, self) 

1334 

1335 self.parent = parent 

1336 self.channel_names = self.parent.channel_names 

1337 self.reference_names = np.array( 

1338 [self.parent.channel_names[i] for i in self.parent.reference_channel_indices] 

1339 ) 

1340 self.response_names = np.array( 

1341 [self.parent.channel_names[i] for i in self.parent.response_channel_indices] 

1342 ) 

1343 self.reciprocal_responses = self.parent.reciprocal_responses 

1344 

1345 self.signal_selector.currentIndexChanged.connect(self.update_ui) 

1346 self.data_type_selector.currentIndexChanged.connect(self.update_ui_no_clear) 

1347 self.response_coordinate_selector.currentIndexChanged.connect(self.update_data) 

1348 self.reference_coordinate_selector.currentIndexChanged.connect(self.update_data) 

1349 

1350 self.primary_plotitem = self.primary_plot.getPlotItem() 

1351 self.secondary_plotitem = self.secondary_plot.getPlotItem() 

1352 self.primary_viewbox = self.primary_plotitem.getViewBox() 

1353 self.secondary_viewbox = self.secondary_plotitem.getViewBox() 

1354 self.primary_axis = self.primary_plotitem.getAxis("left") 

1355 self.secondary_axis = self.secondary_plotitem.getAxis("left") 

1356 

1357 self.secondary_plotitem.setXLink(self.primary_plotitem) 

1358 

1359 self.primary_plotdataitem = pyqtgraph.PlotDataItem( 

1360 np.arange(2), np.zeros(2), pen={"color": "r", "width": 1} 

1361 ) 

1362 self.secondary_plotdataitem = pyqtgraph.PlotDataItem( 

1363 np.arange(2), np.zeros(2), pen={"color": "r", "width": 1} 

1364 ) 

1365 

1366 self.primary_viewbox.addItem(self.primary_plotdataitem) 

1367 self.secondary_viewbox.addItem(self.secondary_plotdataitem) 

1368 

1369 self.twinx_viewbox = None 

1370 self.twinx_axis = None 

1371 self.twinx_original_plotitem = None 

1372 self.twinx_plotdataitem = None 

1373 

1374 self.is_comparing = False 

1375 self.primary_plotdataitem_compare = pyqtgraph.PlotDataItem( 

1376 np.arange(2), np.zeros(2), pen={"color": "b", "width": 1} 

1377 ) 

1378 self.secondary_plotdataitem_compare = pyqtgraph.PlotDataItem( 

1379 np.arange(2), np.zeros(2), pen={"color": "b", "width": 1} 

1380 ) 

1381 

1382 self.update_ui() 

1383 

1384 def remove_twinx(self): 

1385 """Removes the overlaid plot""" 

1386 if self.twinx_viewbox is None: 

1387 return 

1388 self.twinx_original_plotitem.layout.removeItem(self.twinx_axis) 

1389 self.twinx_original_plotitem.scene().removeItem(self.twinx_viewbox) 

1390 self.twinx_original_plotitem.scene().removeItem(self.twinx_axis) 

1391 self.twinx_viewbox = None 

1392 self.twinx_axis = None 

1393 self.twinx_original_plotitem = None 

1394 

1395 def add_twinx(self, existing_plot_item: pyqtgraph.PlotItem): 

1396 """Adds an overlaid plot""" 

1397 # Create a viewbox 

1398 self.twinx_original_plotitem = existing_plot_item 

1399 self.twinx_viewbox = pyqtgraph.ViewBox() 

1400 self.twinx_original_plotitem.scene().addItem(self.twinx_viewbox) 

1401 self.twinx_axis = pyqtgraph.AxisItem("right") 

1402 self.twinx_axis.setLogMode(False) 

1403 self.twinx_axis.linkToView(self.twinx_viewbox) 

1404 self.twinx_original_plotitem.layout.addItem(self.twinx_axis, 2, 3) 

1405 self.updateTwinXViews() 

1406 self.twinx_viewbox.setXLink(self.twinx_original_plotitem) 

1407 self.twinx_original_plotitem.vb.sigResized.connect(self.updateTwinXViews) 

1408 self.twinx_plotdataitem = pyqtgraph.PlotDataItem( 

1409 np.arange(2), np.zeros(2), pen={"color": "b", "width": 1} 

1410 ) 

1411 self.twinx_viewbox.addItem(self.twinx_plotdataitem) 

1412 

1413 def add_compare(self): 

1414 """Adds a second function for comparison for reciprocal plots""" 

1415 self.is_comparing = True 

1416 self.primary_viewbox.addItem(self.primary_plotdataitem_compare) 

1417 self.secondary_viewbox.addItem(self.secondary_plotdataitem_compare) 

1418 

1419 def remove_compare(self): 

1420 """Removes the second function that was used for comparison""" 

1421 if self.is_comparing: 

1422 self.primary_viewbox.removeItem(self.primary_plotdataitem_compare) 

1423 self.secondary_viewbox.removeItem(self.secondary_plotdataitem_compare) 

1424 self.is_comparing = False 

1425 

1426 def updateTwinXViews(self): # pylint: disable=invalid-name 

1427 """Updates the second view box based on the view from the first box""" 

1428 if self.twinx_viewbox is None: 

1429 return 

1430 self.twinx_viewbox.setGeometry(self.twinx_original_plotitem.vb.sceneBoundingRect()) 

1431 # self.twinx_viewbox.linkedViewChanged( 

1432 # self.twinx_original_plotitem.vb, self.twinx_viewbox.XAxis) 

1433 

1434 def update_ui_no_clear(self): 

1435 """Updates the UI without clearing the data""" 

1436 self.update_ui(False) 

1437 

1438 def update_ui(self, clear_channels=True): 

1439 """Updates the UI based on which function type is selected""" 

1440 self.response_coordinate_selector.blockSignals(True) 

1441 self.reference_coordinate_selector.blockSignals(True) 

1442 self.remove_twinx() 

1443 self.remove_compare() 

1444 if self.signal_selector.currentIndex() in [ 

1445 0, 

1446 1, 

1447 2, 

1448 3, 

1449 ]: # Time or Windowed Time or Spectrum or Autospectrum 

1450 self.reference_coordinate_selector.hide() 

1451 self.data_type_selector.hide() 

1452 self.secondary_plot.hide() 

1453 if clear_channels: 

1454 self.response_coordinate_selector.clear() 

1455 self.reference_coordinate_selector.clear() 

1456 for channel_name in self.channel_names: 

1457 self.response_coordinate_selector.addItem(channel_name) 

1458 if self.signal_selector.currentIndex() in [0, 1]: 

1459 self.primary_axis.setLogMode(False) 

1460 self.primary_plotdataitem.setLogMode(False, False) 

1461 else: 

1462 self.primary_axis.setLogMode(True) 

1463 self.primary_plotdataitem.setLogMode(False, True) 

1464 elif self.signal_selector.currentIndex() in [ 

1465 4, 

1466 6, 

1467 7, 

1468 ]: # FRF or FRF Coherence or Reciprocity 

1469 self.reference_coordinate_selector.show() 

1470 self.data_type_selector.show() 

1471 if self.data_type_selector.currentIndex() in [1, 4]: 

1472 self.secondary_plot.show() 

1473 if self.signal_selector.currentIndex() == 6: 

1474 self.add_twinx(self.secondary_plotitem) 

1475 else: 

1476 self.secondary_plot.hide() 

1477 if self.signal_selector.currentIndex() == 6: 

1478 self.add_twinx(self.primary_plotitem) 

1479 if self.signal_selector.currentIndex() == 7: 

1480 if any([val is None for val in self.reciprocal_responses]): 

1481 error_message_qt( 

1482 "Invalid Reciprocal Channels", 

1483 "Could not deterimine reciprocal channels for this test", 

1484 ) 

1485 self.signal_selector.setCurrentIndex(4) 

1486 return 

1487 self.add_compare() 

1488 if clear_channels: 

1489 self.response_coordinate_selector.clear() 

1490 self.reference_coordinate_selector.clear() 

1491 if self.signal_selector.currentIndex() == 7: 

1492 for channel_name in self.response_names[self.reciprocal_responses]: 

1493 self.response_coordinate_selector.addItem(channel_name) 

1494 else: 

1495 for channel_name in self.response_names: 

1496 self.response_coordinate_selector.addItem(channel_name) 

1497 for channel_name in self.reference_names: 

1498 self.reference_coordinate_selector.addItem(channel_name) 

1499 if self.data_type_selector.currentIndex() == 0: 

1500 self.primary_axis.setLogMode(True) 

1501 self.primary_plotdataitem.setLogMode(False, True) 

1502 self.primary_plotdataitem_compare.setLogMode(False, True) 

1503 elif self.data_type_selector.currentIndex() == 1: 

1504 self.primary_axis.setLogMode(False) 

1505 self.primary_plotdataitem.setLogMode(False, False) 

1506 self.primary_plotdataitem_compare.setLogMode(False, False) 

1507 self.secondary_axis.setLogMode(True) 

1508 self.secondary_plotdataitem.setLogMode(False, True) 

1509 self.secondary_plotdataitem_compare.setLogMode(False, True) 

1510 elif self.data_type_selector.currentIndex() in [2, 3]: 

1511 self.primary_axis.setLogMode(False) 

1512 self.primary_plotdataitem.setLogMode(False, False) 

1513 self.primary_plotdataitem_compare.setLogMode(False, False) 

1514 elif self.data_type_selector.currentIndex() == 4: 

1515 self.primary_axis.setLogMode(False) 

1516 self.primary_plotdataitem.setLogMode(False, False) 

1517 self.primary_plotdataitem_compare.setLogMode(False, False) 

1518 self.secondary_axis.setLogMode(False) 

1519 self.secondary_plotdataitem.setLogMode(False, False) 

1520 self.secondary_plotdataitem_compare.setLogMode(False, False) 

1521 if self.signal_selector.currentIndex() == 6: 

1522 self.twinx_axis.setLogMode(False) 

1523 self.twinx_plotdataitem.setLogMode(False, False) 

1524 elif self.signal_selector.currentIndex() in [5]: # Coherence 

1525 self.reference_coordinate_selector.hide() 

1526 self.data_type_selector.hide() 

1527 self.secondary_plot.hide() 

1528 if clear_channels: 

1529 self.response_coordinate_selector.clear() 

1530 self.reference_coordinate_selector.clear() 

1531 for channel_name in self.response_names: 

1532 self.response_coordinate_selector.addItem(channel_name) 

1533 self.primary_axis.setLogMode(False) 

1534 self.primary_plotdataitem.setLogMode(False, False) 

1535 self.update_data() 

1536 self.response_coordinate_selector.blockSignals(False) 

1537 self.reference_coordinate_selector.blockSignals(False) 

1538 

1539 def set_window_title(self): 

1540 """Sets the window title""" 

1541 signal_name = self.signal_selector.itemText(self.signal_selector.currentIndex()) 

1542 response_name = self.response_coordinate_selector.itemText( 

1543 self.response_coordinate_selector.currentIndex() 

1544 ) 

1545 reference_name = ( 

1546 self.reference_coordinate_selector.itemText( 

1547 self.reference_coordinate_selector.currentIndex() 

1548 ) 

1549 if self.signal_selector.currentIndex() == 4 

1550 else "" 

1551 ) 

1552 self.setWindowTitle(f"{signal_name} {response_name} {reference_name}") 

1553 

1554 def update_data(self): 

1555 """Updates the data in the plot""" 

1556 self.set_window_title() 

1557 current_index = self.signal_selector.currentIndex() 

1558 if current_index in [0, 1]: # Time history 

1559 if self.parent.last_frame is None: 

1560 return 

1561 data = self.parent.last_frame[self.response_coordinate_selector.currentIndex()] 

1562 if current_index == 1: 

1563 data = data * self.parent.window_function 

1564 self.primary_plotdataitem.setData(self.parent.time_abscissa, data) 

1565 elif current_index == 2: # Spectrum 

1566 if self.parent.last_spectrum is None: 

1567 return 

1568 data = self.parent.last_spectrum[self.response_coordinate_selector.currentIndex()] 

1569 self.primary_plotdataitem.setData(self.parent.frequency_abscissa, data) 

1570 elif current_index == 3: # Autospectrum 

1571 if self.parent.last_autospectrum is None: 

1572 return 

1573 data = self.parent.last_autospectrum[self.response_coordinate_selector.currentIndex()] 

1574 self.primary_plotdataitem.setData(self.parent.frequency_abscissa, data) 

1575 elif current_index == 4 or current_index == 6: # FRF or FRF Coherence 

1576 if self.parent.last_frf is None: 

1577 return 

1578 data = self.parent.last_frf[ 

1579 :, 

1580 self.response_coordinate_selector.currentIndex(), 

1581 self.reference_coordinate_selector.currentIndex(), 

1582 ] 

1583 if self.data_type_selector.currentIndex() == 0: # Magnitude 

1584 self.primary_plotdataitem.setData(self.parent.frequency_abscissa, np.abs(data)) 

1585 elif self.data_type_selector.currentIndex() == 1: # Magnitude/Phase 

1586 self.primary_plotdataitem.setData(self.parent.frequency_abscissa, np.angle(data)) 

1587 self.secondary_plotdataitem.setData(self.parent.frequency_abscissa, np.abs(data)) 

1588 elif self.data_type_selector.currentIndex() == 2: # Real 

1589 self.primary_plotdataitem.setData(self.parent.frequency_abscissa, np.real(data)) 

1590 elif self.data_type_selector.currentIndex() == 3: # Imag 

1591 self.primary_plotdataitem.setData(self.parent.frequency_abscissa, np.imag(data)) 

1592 elif self.data_type_selector.currentIndex() == 4: # Real/Imag 

1593 self.primary_plotdataitem.setData(self.parent.frequency_abscissa, np.real(data)) 

1594 self.secondary_plotdataitem.setData(self.parent.frequency_abscissa, np.imag(data)) 

1595 if current_index == 6: 

1596 data = self.parent.last_coh[self.response_coordinate_selector.currentIndex()] 

1597 self.twinx_plotdataitem.setData(self.parent.frequency_abscissa, data) 

1598 elif current_index == 5: # Coherence 

1599 if self.parent.last_coh is None: 

1600 return 

1601 data = self.parent.last_coh[self.response_coordinate_selector.currentIndex()] 

1602 self.primary_plotdataitem.setData(self.parent.frequency_abscissa, data) 

1603 elif current_index == 7: # FRF or FRF Coherence 

1604 if self.parent.last_frf is None: 

1605 return 

1606 resp_ind = self.response_coordinate_selector.currentIndex() 

1607 ref_ind = self.reference_coordinate_selector.currentIndex() 

1608 data = self.parent.last_frf[:, self.reciprocal_responses[resp_ind], ref_ind] 

1609 compare_data = self.parent.last_frf[:, self.reciprocal_responses[ref_ind], resp_ind] 

1610 if self.data_type_selector.currentIndex() == 0: # Magnitude 

1611 self.primary_plotdataitem.setData(self.parent.frequency_abscissa, np.abs(data)) 

1612 self.primary_plotdataitem_compare.setData( 

1613 self.parent.frequency_abscissa, np.abs(compare_data) 

1614 ) 

1615 elif self.data_type_selector.currentIndex() == 1: # Magnitude/Phase 

1616 self.primary_plotdataitem.setData(self.parent.frequency_abscissa, np.angle(data)) 

1617 self.secondary_plotdataitem.setData(self.parent.frequency_abscissa, np.abs(data)) 

1618 self.primary_plotdataitem_compare.setData( 

1619 self.parent.frequency_abscissa, np.angle(compare_data) 

1620 ) 

1621 self.secondary_plotdataitem_compare.setData( 

1622 self.parent.frequency_abscissa, np.abs(compare_data) 

1623 ) 

1624 elif self.data_type_selector.currentIndex() == 2: # Real 

1625 self.primary_plotdataitem.setData(self.parent.frequency_abscissa, np.real(data)) 

1626 self.primary_plotdataitem_compare.setData( 

1627 self.parent.frequency_abscissa, np.real(compare_data) 

1628 ) 

1629 elif self.data_type_selector.currentIndex() == 3: # Imag 

1630 self.primary_plotdataitem.setData(self.parent.frequency_abscissa, np.imag(data)) 

1631 self.primary_plotdataitem_compare.setData( 

1632 self.parent.frequency_abscissa, np.imag(compare_data) 

1633 ) 

1634 elif self.data_type_selector.currentIndex() == 4: # Real/Imag 

1635 self.primary_plotdataitem.setData(self.parent.frequency_abscissa, np.real(data)) 

1636 self.secondary_plotdataitem.setData(self.parent.frequency_abscissa, np.imag(data)) 

1637 self.primary_plotdataitem_compare.setData( 

1638 self.parent.frequency_abscissa, np.real(compare_data) 

1639 ) 

1640 self.secondary_plotdataitem_compare.setData( 

1641 self.parent.frequency_abscissa, np.imag(compare_data) 

1642 ) 

1643 

1644 def increment_channel(self, increment=1): 

1645 """Increments the channel number by the specified amount""" 

1646 if not self.lock_response_checkbox.isChecked(): 

1647 num_channels = self.response_coordinate_selector.count() 

1648 current_index = self.response_coordinate_selector.currentIndex() 

1649 new_index = (current_index + increment) % num_channels 

1650 self.response_coordinate_selector.setCurrentIndex(new_index) 

1651 

1652 

1653class ChannelMonitor(QtWidgets.QDialog): 

1654 """Class defining a subwindow that displays specific channel information""" 

1655 

1656 def __init__(self, parent, daq_settings: DataAcquisitionParameters): 

1657 """ 

1658 Creates a window showing CPSD matrix information for a single channel. 

1659 

1660 Parameters 

1661 ---------- 

1662 parent : QWidget 

1663 Parent of the window. 

1664 """ 

1665 super(QtWidgets.QDialog, self).__init__(parent) 

1666 self.setWindowFlags(self.windowFlags() & Qt.Tool) 

1667 self.channels = daq_settings.channel_list 

1668 # Set up the window 

1669 self.graphics_layout_widget = pyqtgraph.GraphicsLayoutWidget(self) 

1670 self.push_button = QtWidgets.QPushButton("Clear Alerts", self) 

1671 self.channels_per_row_label = QtWidgets.QLabel("Channels per Row: ", self) 

1672 self.channels_per_row_selector = QtWidgets.QSpinBox(self) 

1673 self.channels_per_row_selector.setMinimum(2) 

1674 self.channels_per_row_selector.setMaximum(100) 

1675 self.channels_per_row_selector.setValue(20) 

1676 self.channels_per_row_selector.setKeyboardTracking(False) 

1677 layout = QtWidgets.QVBoxLayout() 

1678 control_layout = QtWidgets.QHBoxLayout() 

1679 layout.addWidget(self.graphics_layout_widget) 

1680 control_layout.addWidget(self.channels_per_row_label) 

1681 control_layout.addWidget(self.channels_per_row_selector) 

1682 control_layout.addStretch() 

1683 control_layout.addWidget(self.push_button) 

1684 layout.addLayout(control_layout) 

1685 self.setLayout(layout) 

1686 # Set up defaults for the channel ranges 

1687 self.channel_ranges = None 

1688 self.channel_warning_limits = None 

1689 self.channel_abort_limits = None 

1690 self.background_bars = None 

1691 self.history_bars = None 

1692 self.level_bars = None 

1693 self.history_last_update = None 

1694 self.history_hold_frames = int( 

1695 np.ceil(10 * daq_settings.sample_rate / daq_settings.samples_per_read) 

1696 ) 

1697 self.aborted_channels = None 

1698 # Set up defaults for the plot 

1699 self.plots = None 

1700 self.bar_channel_indices = None 

1701 self.pen = pyqtgraph.mkPen(color=(0, 0, 0, 255), width=1) 

1702 self.background_brush = pyqtgraph.mkBrush((255, 255, 255)) 

1703 self.history_brush = pyqtgraph.mkBrush((124, 124, 255)) 

1704 self.current_brush = pyqtgraph.mkBrush((34, 139, 34)) 

1705 self.limit_brush = pyqtgraph.mkBrush((145, 197, 17)) 

1706 self.abort_brush = pyqtgraph.mkBrush((145, 70, 17)) 

1707 self.limit_background_brush = pyqtgraph.mkBrush( 

1708 ( 

1709 255, 

1710 255, 

1711 0, 

1712 ) 

1713 ) 

1714 self.abort_background_brush = pyqtgraph.mkBrush((255, 0, 0)) 

1715 self.limit_history_brush = pyqtgraph.mkBrush((190, 190, 128)) 

1716 self.abort_history_brush = pyqtgraph.mkBrush((190, 62, 128)) 

1717 # Connect everything and do final builds 

1718 self.connect_callbacks() 

1719 self.build_plot() 

1720 self.setWindowTitle("Channel Monitor") 

1721 self.resize(400, 300) 

1722 self.show() 

1723 

1724 def connect_callbacks(self): 

1725 """Connects callback functions to the respective widgets""" 

1726 self.channels_per_row_selector.valueChanged.connect(self.build_plot) 

1727 self.push_button.clicked.connect(self.clear_alerts) 

1728 

1729 def update_channel_list(self, daq_settings): 

1730 """Updates the channel list in the test""" 

1731 self.channels = daq_settings.channel_list 

1732 self.history_hold_frames = int( 

1733 np.ceil(10 * daq_settings.sample_rate / daq_settings.samples_per_read) 

1734 ) 

1735 self.build_plot() 

1736 

1737 def clear_alerts(self): 

1738 """Clears any alerts that have been triggered by high values""" 

1739 self.aborted_channels = [False for val in self.aborted_channels] 

1740 for current_bar in self.level_bars: 

1741 current_bar.setOpts(brushes=[self.current_brush]) 

1742 for history_bar in self.history_bars: 

1743 history_bar.setOpts(brushes=[self.history_brush]) 

1744 for background_bar in self.background_bars: 

1745 background_bar.setOpts(brushes=[self.background_brush]) 

1746 

1747 def build_plot(self): 

1748 """Builds the channel monitor window and plots""" 

1749 # TODO Need to get the values from the bars before deleting them so we 

1750 # can maintain the levels from before the value was changed 

1751 self.graphics_layout_widget.clear() 

1752 num_channels = len(self.channels) 

1753 num_bars = int(np.ceil(num_channels / self.channels_per_row_selector.value())) 

1754 # Compute number of channels per bar 

1755 channels_per_bar = [0 for i in range(num_bars)] 

1756 for i in range(num_channels): 

1757 channels_per_bar[i % num_bars] += 1 

1758 

1759 # print('Channels per Bar {:}'.format(channels_per_bar)) 

1760 # Now let's actually make the plots 

1761 self.plots = [self.graphics_layout_widget.addPlot(i, 0) for i in range(num_bars)] 

1762 

1763 # Now parse the channel ranges 

1764 self.channel_ranges = [] 

1765 self.channel_warning_limits = [] 

1766 self.channel_abort_limits = [] 

1767 for channel in self.channels: 

1768 try: 

1769 max_abs_volt = np.min( 

1770 np.abs([float(channel.maximum_value), float(channel.minimum_value)]) 

1771 ) 

1772 except (ValueError, TypeError): 

1773 max_abs_volt = 10 # Assume 10 V range on DAQ 

1774 try: 

1775 sensitivity = float(channel.sensitivity) / 1000 # mV -> V 

1776 except (ValueError, TypeError): 

1777 sensitivity = 0.01 # Assume 10 mV/EU 

1778 max_abs_eu = max_abs_volt / sensitivity 

1779 try: 

1780 warning_limit = float(channel.warning_level) 

1781 except (ValueError, TypeError): 

1782 warning_limit = max_abs_eu * 0.9 # Put out warning at 90% the max range 

1783 try: 

1784 abort_limit = float(channel.abort_level) 

1785 except (ValueError, TypeError): 

1786 abort_limit = max_abs_eu # Never abort on this channel if not specified 

1787 self.channel_ranges.append(max_abs_eu) 

1788 self.channel_warning_limits.append(warning_limit) 

1789 self.channel_abort_limits.append(abort_limit) 

1790 self.channel_ranges = np.array(self.channel_ranges) 

1791 self.channel_warning_limits = np.array(self.channel_warning_limits) 

1792 self.channel_abort_limits = np.array(self.channel_abort_limits) 

1793 # Display abort limit as range rather than channel if it is lower 

1794 abort_lower = self.channel_ranges > self.channel_abort_limits 

1795 self.channel_ranges[abort_lower] = self.channel_abort_limits[abort_lower] 

1796 

1797 # Now build the plots 

1798 self.bar_channel_indices = [] 

1799 for i, num_channels in enumerate(channels_per_bar): 

1800 try: 

1801 next_starting_index = self.bar_channel_indices[-1][-1] + 1 

1802 except IndexError: 

1803 next_starting_index = 0 

1804 self.bar_channel_indices.append(next_starting_index + np.arange(num_channels)) 

1805 # print(self.bar_channel_indices) 

1806 self.background_bars = [] 

1807 self.history_bars = [] 

1808 self.level_bars = [] 

1809 self.history_last_update = [] 

1810 self.aborted_channels = [] 

1811 for indices, plot in zip(self.bar_channel_indices, self.plots): 

1812 plot.hideAxis("left") 

1813 for _, index in enumerate(indices): 

1814 background_bar = pyqtgraph.BarGraphItem( 

1815 x=[index + 1], 

1816 height=1.0, 

1817 width=0.9, 

1818 pen=self.pen, 

1819 brush=self.background_brush, 

1820 ) 

1821 plot.addItem(background_bar) 

1822 self.background_bars.append(background_bar) 

1823 history_bar = pyqtgraph.BarGraphItem( 

1824 x=[index + 1], 

1825 height=0, 

1826 width=0.9, 

1827 pen=self.pen, 

1828 brush=self.history_brush, 

1829 ) 

1830 plot.addItem(history_bar) 

1831 self.history_bars.append(history_bar) 

1832 current_bar = pyqtgraph.BarGraphItem( 

1833 x=[index + 1], 

1834 height=0, 

1835 width=0.9, 

1836 pen=self.pen, 

1837 brush=self.current_brush, 

1838 ) 

1839 plot.addItem(current_bar) 

1840 self.level_bars.append(current_bar) 

1841 self.history_last_update.append(0) 

1842 self.aborted_channels.append(False) 

1843 

1844 def update(self, channel_levels): 

1845 """Updates the level data in each bar""" 

1846 # print('Data {:}'.format(channel_levels.shape)) 

1847 # print(channel_levels) 

1848 for index, ( 

1849 level, 

1850 current_bar, 

1851 history_bar, 

1852 background_bar, 

1853 history_last_update, 

1854 warning, 

1855 abort, 

1856 value_range, 

1857 aborted, 

1858 ) in enumerate( 

1859 zip( 

1860 channel_levels, 

1861 self.level_bars, 

1862 self.history_bars, 

1863 self.background_bars, 

1864 self.history_last_update, 

1865 self.channel_warning_limits, 

1866 self.channel_abort_limits, 

1867 self.channel_ranges, 

1868 self.aborted_channels, 

1869 ) 

1870 ): 

1871 # Set the current bar height 

1872 current_height = level / value_range 

1873 current_bar.setOpts(height=current_height if current_height < 1 else 1) 

1874 # Now look at the history bar 

1875 last_history_height = history_bar.opts.get("height") 

1876 # print(last_history_height) 

1877 if history_last_update > self.history_hold_frames: 

1878 desired_history_height = last_history_height - 1 / self.history_hold_frames 

1879 else: 

1880 desired_history_height = last_history_height 

1881 if desired_history_height < current_height: 

1882 desired_history_height = current_height 

1883 self.history_last_update[index] = 0 

1884 else: 

1885 self.history_last_update[index] += 1 

1886 history_bar.setOpts(height=1 if desired_history_height > 1 else desired_history_height) 

1887 # Now look at the pen color 

1888 if level > abort or aborted: 

1889 current_bar.setOpts(brushes=[self.abort_brush]) 

1890 background_bar.setOpts(brushes=[self.abort_background_brush]) 

1891 history_bar.setOpts(brushes=[self.abort_history_brush]) 

1892 self.aborted_channels[index] = True 

1893 elif level > warning: 

1894 current_bar.setOpts(brushes=[self.limit_brush]) 

1895 background_bar.setOpts(brushes=[self.limit_background_brush]) 

1896 history_bar.setOpts(brushes=[self.limit_history_brush]) 

1897 

1898 

1899class VaryingNumberOfLinePlot: 

1900 """A plot that can have a dynamic number of lines assigned, 

1901 adding or removing lines as necessary""" 

1902 

1903 def __init__(self, plot_item, initial_abscissa=None, initial_ordinate=None): 

1904 self.plot_item = plot_item 

1905 self.lines = [] 

1906 if initial_abscissa is not None and initial_ordinate is not None: 

1907 self.set_data(initial_abscissa, initial_ordinate) 

1908 

1909 def set_data(self, abscissa, ordinate): 

1910 """Sets the data of the plot 

1911 

1912 Parameters 

1913 ---------- 

1914 abscissa : np.ndarray 

1915 A 2D dataset where each row is a different plot and the columns are the abscissa values 

1916 of each curve 

1917 ordinate : np.ndarray 

1918 A 2D dataset where each row is a different plot and the columns are the ordinate values 

1919 of each curve 

1920 """ 

1921 for i, (this_ordinate, this_abscissa) in enumerate(zip(ordinate, abscissa)): 

1922 try: 

1923 self.lines[i].setData(this_abscissa, this_ordinate) 

1924 except IndexError: 

1925 pen = {"color": colororder[i % len(colororder)]} 

1926 self.lines.append(self.plot_item.plot(this_abscissa, this_ordinate, pen=pen)) 

1927 

1928 # Remove extra lines 

1929 extra_lines = len(self.lines) - len(ordinate) 

1930 for i in range(extra_lines): 

1931 line = self.lines.pop() 

1932 self.plot_item.removeItem(line) 

1933 

1934 def clear(self): 

1935 """Clears all data from the plots""" 

1936 self.lines = [] 

1937 self.plot_item.clear() 

1938 

1939 

1940class IPAddress: 

1941 """Container for information about IPAddress, mainly used to make 

1942 sure each address has a values for relevant information""" 

1943 

1944 def __init__(self, host_name=None, ipv4_address=None, ipv6_address=None, valid_ip=False): 

1945 self.host_name = host_name 

1946 self.ipv4_address = ipv4_address 

1947 self.ipv6_address = ipv6_address 

1948 self.valid_ip = valid_ip 

1949 

1950 

1951this_path = os.path.split(__file__)[0] 

1952ip_manager_ui_path = os.path.join(this_path, "ip_manager.ui") 

1953 

1954 

1955class IPAddressManager(QtWidgets.QDialog): 

1956 """A class to manage IP addresses""" 

1957 

1958 def __init__(self, ip_addresses: list[IPAddress] = None, parent=None): 

1959 if ip_addresses is None: 

1960 ip_addresses = [] 

1961 super().__init__(parent) 

1962 uic.loadUi(ip_manager_ui_path, self) 

1963 

1964 self.ip_address_table.setColumnWidth(0, 200) 

1965 self.ip_address_table.setColumnWidth(1, 200) 

1966 self.ip_address_table.setColumnWidth(2, 250) 

1967 

1968 self.ip_addresses = [] 

1969 self.unique_indices = [] 

1970 for ind, address in enumerate(ip_addresses): 

1971 self.add_ip_address() 

1972 self.ip_addresses[ind] = address 

1973 

1974 self.validation_timeout = 0.5 

1975 self.selected_index = -1 

1976 

1977 self.refresh_ip_table() 

1978 self.loading_bar.hide() 

1979 

1980 self.connect_callbacks() 

1981 

1982 self.setWindowIcon(QtGui.QIcon("logo/Rattlesnake_Icon.png")) 

1983 

1984 def connect_callbacks(self): 

1985 """Connects callbacks to the widgets""" 

1986 self.add_ip_address_button.clicked.connect(self.add_ip_address) 

1987 self.remove_ip_address_button.clicked.connect(self.remove_ip_address) 

1988 self.validate_ip_address_button.clicked.connect(self.validate_button_pressed) 

1989 

1990 self.button_box.accepted.disconnect() 

1991 self.button_box.accepted.connect(self.accept) 

1992 self.button_box.rejected.connect(self.reject) 

1993 

1994 def set_row_count(self, row_count): 

1995 """Sets the number of rows in the table""" 

1996 while self.ip_address_table.rowCount() < row_count: 

1997 clicked = False 

1998 self.add_ip_address(clicked) 

1999 

2000 def add_ip_address(self, clicked=None, append_list=True): # pylint: disable=unused-argument 

2001 """Adds a new IP address to the manager""" 

2002 if append_list: 

2003 new_ip = IPAddress() 

2004 self.ip_addresses.append(new_ip) 

2005 

2006 unique_index = 0 

2007 while unique_index in self.unique_indices: 

2008 unique_index += 1 

2009 self.unique_indices.append(unique_index) 

2010 

2011 # Add new row to list 

2012 current_row = self.ip_address_table.rowCount() 

2013 self.ip_address_table.setRowCount(len(self.unique_indices)) 

2014 

2015 # Add a host name line edit with move up and move down buttons 

2016 host_name_widget = QtWidgets.QWidget() 

2017 host_name_layout = QtWidgets.QHBoxLayout(host_name_widget) 

2018 host_name_layout.setContentsMargins(0, 0, 0, 0) 

2019 host_name_layout.setSpacing(0) 

2020 

2021 move_button_layout = QtWidgets.QVBoxLayout() 

2022 move_button_layout.setContentsMargins(0, 0, 0, 0) 

2023 move_button_layout.setSpacing(0) 

2024 

2025 up_button = QtWidgets.QToolButton() 

2026 up_button.setArrowType(QtCore.Qt.UpArrow) 

2027 up_button.setFixedSize(30, 15) 

2028 up_button.clicked.connect(lambda: self.move_address_up(unique_index)) 

2029 

2030 down_button = QtWidgets.QToolButton() 

2031 down_button.setArrowType(QtCore.Qt.DownArrow) 

2032 down_button.setFixedSize(30, 15) 

2033 down_button.clicked.connect(lambda: self.move_address_down(unique_index)) 

2034 

2035 move_button_layout.addWidget(up_button) 

2036 move_button_layout.addWidget(down_button) 

2037 

2038 host_name_input = QtWidgets.QLineEdit() 

2039 host_name_input.setFixedHeight(45) 

2040 host_name_input.setPlaceholderText("BK<Type>-<Serial>") 

2041 host_name_input.textChanged.connect(lambda text: self.host_name_changed(text, unique_index)) 

2042 host_name_input.focusInEvent = lambda event: self.ip_input_focused(event, unique_index) 

2043 

2044 host_name_layout.addLayout(move_button_layout) 

2045 host_name_layout.addWidget(host_name_input) 

2046 

2047 self.ip_address_table.setCellWidget(current_row, 0, host_name_widget) 

2048 

2049 ipv4_input = QtWidgets.QLineEdit() 

2050 ipv4_input.setFixedHeight(45) 

2051 ipv4_input.setPlaceholderText("169.254.001.001") 

2052 ipv4_input.textChanged.connect(lambda text: self.ipv4_address_changed(text, unique_index)) 

2053 ipv4_input.focusInEvent = lambda event: self.ip_input_focused(event, unique_index) 

2054 

2055 self.ip_address_table.setCellWidget(current_row, 1, ipv4_input) 

2056 

2057 ipv6_input = QtWidgets.QLineEdit() 

2058 ipv6_input.setFixedHeight(45) 

2059 ipv6_input.setPlaceholderText("[<Unicast>%<Network>]") 

2060 ipv6_input.textChanged.connect(lambda text: self.ipv6_address_changed(text, unique_index)) 

2061 ipv6_input.focusInEvent = lambda event: self.ip_input_focused(event, unique_index) 

2062 

2063 self.ip_address_table.setCellWidget(current_row, 2, ipv6_input) 

2064 

2065 def host_name_changed(self, text: str, unique_index: int): 

2066 """Updates the host name""" 

2067 try: 

2068 current_row = self.unique_indices.index(unique_index) 

2069 except ValueError: 

2070 return 

2071 self.ip_addresses[current_row].host_name = text 

2072 self.ip_addresses[current_row].valid_ip = False 

2073 

2074 def ipv4_address_changed(self, text: str, unique_index: int): 

2075 """Updates the IPv4 Address""" 

2076 try: 

2077 current_row = self.unique_indices.index(unique_index) 

2078 except ValueError: 

2079 return 

2080 self.ip_addresses[current_row].ipv4_address = text 

2081 self.ip_addresses[current_row].valid_ip = False 

2082 

2083 def ipv6_address_changed(self, text: str, unique_index: int): 

2084 """Updates the IPv6 Address""" 

2085 try: 

2086 current_row = self.unique_indices.index(unique_index) 

2087 except ValueError: 

2088 return 

2089 self.ip_addresses[current_row].ipv6_address = text 

2090 self.ip_addresses[current_row].valid_ip = False 

2091 

2092 def ip_input_focused(self, event, unique_index: int): # pylint: disable=unused-argument 

2093 """Updates the selected index based on the window focus""" 

2094 self.selected_index = unique_index 

2095 

2096 def remove_ip_address(self): 

2097 """Removes the currently selected IP Address""" 

2098 try: 

2099 current_row = self.unique_indices.index(self.selected_index) 

2100 except ValueError: 

2101 return 

2102 if 0 <= current_row < len(self.unique_indices): 

2103 self.ip_address_table.removeRow( 

2104 current_row, 

2105 ) 

2106 self.ip_addresses.pop(current_row) 

2107 self.unique_indices.pop(current_row) 

2108 self.selected_index = -1 

2109 

2110 def move_address_up(self, ip_index): 

2111 """Shifts values up one line edit""" 

2112 # Just shifts text values up one LineEdit, unique_indices correspond 

2113 # to LineEdit objects which dont shift therefore the unique_indices dont change 

2114 try: 

2115 current_row = self.unique_indices.index(ip_index) 

2116 except ValueError: 

2117 return 

2118 if current_row > 0: 

2119 move_ip = self.ip_addresses.pop(current_row) 

2120 self.ip_addresses.insert(current_row - 1, move_ip) 

2121 self.refresh_ip_table([current_row - 1, current_row]) 

2122 

2123 def move_address_down(self, ip_index): 

2124 """Shifts the addresses down one line edit""" 

2125 # Just shifts text values down one LineEdit, unique_indices correspond 

2126 # to LineEdit objects which dont shift therefore the unique_indices dont change 

2127 try: 

2128 current_row = self.unique_indices.index(ip_index) 

2129 except ValueError: 

2130 return 

2131 if current_row < len(self.unique_indices) - 1: 

2132 move_ip = self.ip_addresses.pop(current_row) 

2133 self.ip_addresses.insert(current_row + 1, move_ip) 

2134 self.refresh_ip_table([current_row, current_row + 1]) 

2135 

2136 def refresh_ip_table(self, rows: list[int] = None): 

2137 """Refreshes the IP address table""" 

2138 if rows is None: 

2139 rows = range(len(self.unique_indices)) 

2140 

2141 # This is slower than just deleting widgets and refreshing them but I do 

2142 # this to keep a consistent unique indice corresponding to rows 

2143 for row_idx in rows: 

2144 if row_idx >= self.ip_address_table.rowCount(): 

2145 return 

2146 

2147 host_name = ( 

2148 str(self.ip_addresses[row_idx].host_name) 

2149 if self.ip_addresses[row_idx].host_name is not None 

2150 else "" 

2151 ) 

2152 host_name_widget = self.ip_address_table.cellWidget(row_idx, 0) 

2153 host_name_input = host_name_widget.findChild(QtWidgets.QLineEdit) 

2154 host_name_input.blockSignals(True) 

2155 host_name_input.setText(host_name) 

2156 host_name_input.blockSignals(False) 

2157 

2158 ipv4_address = ( 

2159 str(self.ip_addresses[row_idx].ipv4_address) 

2160 if self.ip_addresses[row_idx].ipv4_address is not None 

2161 else "" 

2162 ) 

2163 ipv4_input = self.ip_address_table.cellWidget(row_idx, 1) 

2164 ipv4_input.blockSignals(True) 

2165 ipv4_input.setText(ipv4_address) 

2166 ipv4_input.blockSignals(False) 

2167 

2168 ipv6_address = ( 

2169 str(self.ip_addresses[row_idx].ipv6_address) 

2170 if self.ip_addresses[row_idx].ipv6_address is not None 

2171 else "" 

2172 ) 

2173 ipv6_input = self.ip_address_table.cellWidget(row_idx, 2) 

2174 ipv6_input.blockSignals(True) 

2175 ipv6_input.setText(ipv6_address) 

2176 ipv6_input.blockSignals(False) 

2177 

2178 def get_ip_addresses(self, host_name: str = None): 

2179 """Gets valid IP Addresses given the host name""" 

2180 valid_host_name = False 

2181 ipv4_address = None 

2182 ipv6_address = None 

2183 try: 

2184 # Get the address info for the hostname 

2185 ipv4_info = socket.getaddrinfo(host_name, None, socket.AF_INET) 

2186 ipv6_info = socket.getaddrinfo(host_name, None, socket.AF_INET6) 

2187 ipv4 = ipv4_info[0] 

2188 ipv4_address = ipv4[4][0] 

2189 ipv6 = ipv6_info[0] 

2190 ipv6_address = f"[{ipv6[4][0]}%{ipv6[4][3]}]" 

2191 

2192 valid_host_name = self.validate_ip_address(ipv6_address) 

2193 except (socket.gaierror, IndexError): 

2194 # print(f'Error retrieving info') 

2195 pass 

2196 

2197 return (valid_host_name, ipv4_address, ipv6_address) 

2198 

2199 def get_host_name(self, ip_address: str = None): 

2200 """Gets the host name from an IP address""" 

2201 host_name = None 

2202 host = "http://" + ip_address 

2203 valid_ip = self.validate_ip_address(ip_address) 

2204 if valid_ip: 

2205 try: 

2206 response = requests.get(host + "/rest/rec/module/info", timeout=1) 

2207 info = response.json() 

2208 host_name = f"BK{info['module']['type']['number']}-{info['module']['serial']}" 

2209 except Exception: 

2210 valid_ip = False 

2211 host_name = None 

2212 

2213 return (valid_ip, host_name) 

2214 

2215 def validate_ip_address(self, ip_address: str = None): 

2216 """Checks if IP addresses are valid""" 

2217 valid_ip = False 

2218 host = "http://" + ip_address 

2219 try: 

2220 response = requests.put(host + "/rest/rec/open", timeout=self.validation_timeout) 

2221 if response.status_code == 200: 

2222 valid_ip = True 

2223 except requests.exceptions.Timeout: 

2224 pass 

2225 except requests.exceptions.ConnectionError: 

2226 pass 

2227 except requests.exceptions.RequestException: 

2228 pass 

2229 

2230 return valid_ip 

2231 

2232 def autofill_ip_addresses(self): 

2233 """This function validates the ip address and autofills the other values. 

2234 If multiple inputs are valid but correspond to different devices, the 

2235 priority is host_name > ipv4 > ipv6 

2236 Note: Having 2 of the same host names may not validate correctly due to weird 

2237 socket waiting requirements 

2238 """ 

2239 self.loading_bar.setValue(0) 

2240 self.loading_bar.show() 

2241 num_rows = len(self.unique_indices) 

2242 for row_idx in range(num_rows): 

2243 valid_row = self.ip_addresses[row_idx].valid_ip 

2244 percent_complete = round((row_idx + 1) / num_rows * 100) 

2245 self.loading_bar.setValue(percent_complete) 

2246 

2247 # Check if you can pull information from hostname 

2248 host_name = ( 

2249 str(self.ip_addresses[row_idx].host_name) 

2250 if self.ip_addresses[row_idx].host_name is not None 

2251 else "" 

2252 ) 

2253 if not valid_row and host_name != "": 

2254 valid_row, ipv4_address, ipv6_address = self.get_ip_addresses(host_name) 

2255 

2256 if valid_row: 

2257 self.ip_addresses[row_idx].ipv4_address = ipv4_address 

2258 self.ip_addresses[row_idx].ipv6_address = ipv6_address 

2259 self.ip_addresses[row_idx].valid_ip = valid_row 

2260 continue 

2261 

2262 ipv4_address = ( 

2263 str(self.ip_addresses[row_idx].ipv4_address) 

2264 if self.ip_addresses[row_idx].ipv4_address is not None 

2265 else "" 

2266 ) 

2267 if not valid_row and ipv4_address is not None: 

2268 valid_row, host_name = self.get_host_name(ipv4_address) 

2269 

2270 if valid_row: 

2271 self.ip_addresses[row_idx].host_name = host_name 

2272 (valid_row, _, ipv6_address) = self.get_ip_addresses(host_name) 

2273 self.ip_addresses[row_idx].ipv6_address = ipv6_address 

2274 self.ip_addresses[row_idx].valid_ip = valid_row 

2275 continue 

2276 

2277 ipv6_address = ( 

2278 str(self.ip_addresses[row_idx].ipv6_address) 

2279 if self.ip_addresses[row_idx].ipv6_address is not None 

2280 else "" 

2281 ) 

2282 if not valid_row and ipv6_address is not None: 

2283 valid_row, host_name = self.get_host_name(ipv6_address) 

2284 

2285 if valid_row: 

2286 self.ip_addresses[row_idx].host_name = host_name 

2287 (valid_row, ipv4_address, _) = self.get_ip_addresses(host_name) 

2288 self.ip_addresses[row_idx].ipv4_address = ipv4_address 

2289 self.ip_addresses[row_idx].valid_ip = valid_row 

2290 continue 

2291 

2292 self.loading_bar.hide() 

2293 

2294 def validate_button_pressed(self): 

2295 """Validates the IP Addresses""" 

2296 self.autofill_ip_addresses() 

2297 self.refresh_ip_table() 

2298 

2299 valid_ip_list = [ip.valid_ip for ip in self.ip_addresses] 

2300 if not all(valid_ip_list): 

2301 invalid_ip_rows = [ 

2302 row for row, valid_bool in enumerate(valid_ip_list) if not valid_bool 

2303 ] 

2304 message = ( 

2305 f"Invalid IP address at rows: {invalid_ip_rows}.\n\n " 

2306 f"If IPv4 connection is unstable, try inputting host name." 

2307 ) 

2308 reply = QtWidgets.QMessageBox.question( 

2309 self, 

2310 "Invalid IP Addresses", 

2311 message, 

2312 QtWidgets.QMessageBox.Ok, 

2313 QtWidgets.QMessageBox.Ok, 

2314 ) 

2315 

2316 def closeEvent(self, a0): # pylint: disable=unused-argument,invalid-name 

2317 """Returns the IP addresses""" 

2318 return self.ip_addresses