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

1142 statements  

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

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

2""" 

3This file defines a Random Vibration Environment where a specification is 

4defined and the controller solves for excitations that will cause the test 

5article to match the specified response. 

6 

7This environment has a number of subprocesses, including CPSD and FRF 

8computation, data analysis, and signal generation. 

9 

10Rattlesnake Vibration Control Software 

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

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

13Government retains certain rights in this software. 

14 

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

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

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

18(at your option) any later version. 

19 

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

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

22MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23GNU General Public License for more details. 

24 

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

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

27""" 

28 

29import datetime 

30import inspect 

31import multiprocessing as mp 

32import multiprocessing.sharedctypes # pylint: disable=unused-import 

33import time 

34from enum import Enum 

35from multiprocessing.queues import Queue 

36 

37import netCDF4 as nc4 

38import numpy as np 

39import openpyxl 

40from qtpy import QtWidgets, uic 

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

42from qtpy.QtGui import QColor # pylint: disable=no-name-in-module 

43 

44# %% Imports 

45from .abstract_sysid_environment import ( 

46 AbstractSysIdEnvironment, 

47 AbstractSysIdMetadata, 

48 AbstractSysIdUI, 

49) 

50from .environments import ( 

51 ControlTypes, 

52 environment_definition_ui_paths, 

53 environment_prediction_ui_paths, 

54 environment_run_ui_paths, 

55) 

56from .random_vibration_sys_id_utilities import _direction_map, load_specification 

57from .ui_utilities import PlotWindow, TransformationMatrixWindow, multiline_plotter 

58from .utilities import ( 

59 DataAcquisitionParameters, 

60 GlobalCommands, 

61 VerboseMessageQueue, 

62 db2scale, 

63 error_message_qt, 

64 load_python_module, 

65) 

66 

67# %% Global Variables 

68 

69CONTROL_TYPE = ControlTypes.RANDOM 

70MAXIMUM_NAME_LENGTH = 50 

71 

72 

73# %% Commands 

74class RandomVibrationCommands(Enum): 

75 """Valid random vibration commands""" 

76 

77 ADJUST_TEST_LEVEL = 0 

78 START_CONTROL = 1 

79 STOP_CONTROL = 2 

80 CHECK_FOR_COMPLETE_SHUTDOWN = 3 

81 RECOMPUTE_PREDICTION = 4 

82 # UPDATE_INTERACTIVE_CONTROL_PARAMETERS = 5 

83 

84 

85# %% Queues 

86 

87 

88class RandomVibrationQueues: 

89 """A container class for the queues that random vibration will manage.""" 

90 

91 def __init__( 

92 self, 

93 environment_name: str, 

94 environment_command_queue: VerboseMessageQueue, 

95 gui_update_queue: mp.queues.Queue, 

96 controller_communication_queue: VerboseMessageQueue, 

97 data_in_queue: mp.queues.Queue, 

98 data_out_queue: mp.queues.Queue, 

99 log_file_queue: VerboseMessageQueue, 

100 ): 

101 """A container class for the queues that random vibration will manage. 

102 

103 The environment uses many queues to pass data between the various pieces. 

104 This class organizes those queues into one common namespace. 

105 

106 

107 Parameters 

108 ---------- 

109 environment_name : str 

110 Name of the environment 

111 environment_command_queue : VerboseMessageQueue 

112 Queue that is read by the environment for environment commands 

113 gui_update_queue : mp.queues.Queue 

114 Queue where various subtasks put instructions for updating the 

115 widgets in the user interface 

116 controller_communication_queue : VerboseMessageQueue 

117 Queue that is read by the controller for global controller commands 

118 data_in_queue : mp.queues.Queue 

119 Multiprocessing queue that connects the acquisition subtask to the 

120 environment subtask. Each environment will retrieve acquired data 

121 from this queue. 

122 data_out_queue : mp.queues.Queue 

123 Multiprocessing queue that connects the output subtask to the 

124 environment subtask. Each environment will put data that it wants 

125 the controller to generate in this queue. 

126 log_file_queue : VerboseMessageQueue 

127 Queue for putting logging messages that will be read by the logging 

128 subtask and written to a file. 

129 """ 

130 self.environment_command_queue = environment_command_queue 

131 self.gui_update_queue = gui_update_queue 

132 self.data_analysis_command_queue = VerboseMessageQueue( 

133 log_file_queue, environment_name + " Data Analysis Command Queue" 

134 ) 

135 self.signal_generation_command_queue = VerboseMessageQueue( 

136 log_file_queue, environment_name + " Signal Generation Command Queue" 

137 ) 

138 self.spectral_command_queue = VerboseMessageQueue( 

139 log_file_queue, environment_name + " Spectral Computation Command Queue" 

140 ) 

141 self.collector_command_queue = VerboseMessageQueue( 

142 log_file_queue, environment_name + " Data Collector Command Queue" 

143 ) 

144 self.controller_communication_queue = controller_communication_queue 

145 self.data_in_queue = data_in_queue 

146 self.data_out_queue = data_out_queue 

147 self.data_for_spectral_computation_queue = mp.Queue() 

148 self.updated_spectral_quantities_queue = mp.Queue() 

149 self.cpsd_to_generate_queue = mp.Queue() 

150 self.log_file_queue = log_file_queue 

151 

152 

153# %% Metadata 

154 

155 

156class RandomVibrationMetadata(AbstractSysIdMetadata): 

157 """Container to hold the signal processing parameters of the environment""" 

158 

159 def __init__( 

160 self, 

161 number_of_channels, 

162 sample_rate, 

163 samples_per_frame, 

164 test_level_ramp_time, 

165 cola_window, 

166 cola_overlap, 

167 cola_window_exponent, 

168 sigma_clip, 

169 update_tf_during_control, 

170 frames_in_cpsd, 

171 cpsd_window, 

172 cpsd_overlap, 

173 percent_lines_out, 

174 allow_automatic_aborts, 

175 control_python_script, 

176 control_python_function, 

177 control_python_function_type, 

178 control_python_function_parameters, 

179 control_channel_indices, 

180 output_channel_indices, 

181 specification_frequency_lines, 

182 specification_cpsd_matrix, 

183 specification_warning_matrix, 

184 specification_abort_matrix, 

185 response_transformation_matrix, 

186 output_transformation_matrix, 

187 ): 

188 super().__init__() 

189 self.number_of_channels = number_of_channels 

190 self.sample_rate = sample_rate 

191 self.samples_per_frame = samples_per_frame 

192 self.test_level_ramp_time = test_level_ramp_time 

193 self.cpsd_overlap = cpsd_overlap 

194 self.update_tf_during_control = update_tf_during_control 

195 self.cola_window = cola_window 

196 self.cola_overlap = cola_overlap 

197 self.cola_window_exponent = cola_window_exponent 

198 self.sigma_clip = sigma_clip 

199 self.frames_in_cpsd = frames_in_cpsd 

200 self.cpsd_window = cpsd_window 

201 self.response_transformation_matrix = response_transformation_matrix 

202 self.reference_transformation_matrix = output_transformation_matrix 

203 self.control_python_script = control_python_script 

204 self.control_python_function = control_python_function 

205 self.control_python_function_type = control_python_function_type 

206 self.control_python_function_parameters = control_python_function_parameters 

207 self.control_channel_indices = control_channel_indices 

208 self.output_channel_indices = output_channel_indices 

209 self.specification_frequency_lines = specification_frequency_lines 

210 self.specification_cpsd_matrix = specification_cpsd_matrix 

211 self.specification_warning_matrix = specification_warning_matrix 

212 self.specification_abort_matrix = specification_abort_matrix 

213 self.percent_lines_out = percent_lines_out 

214 self.allow_automatic_aborts = allow_automatic_aborts 

215 

216 @property 

217 def sample_rate(self): 

218 return self._sample_rate 

219 

220 @sample_rate.setter 

221 def sample_rate(self, value): 

222 self._sample_rate = value 

223 

224 @property 

225 def number_of_channels(self): 

226 return self._number_of_channels 

227 

228 @number_of_channels.setter 

229 def number_of_channels(self, value): 

230 self._number_of_channels = value 

231 

232 @property 

233 def reference_channel_indices(self): 

234 return self.output_channel_indices 

235 

236 @property 

237 def response_channel_indices(self): 

238 return self.control_channel_indices 

239 

240 @property 

241 def response_transformation_matrix(self): 

242 return self._response_transformation_matrix 

243 

244 @response_transformation_matrix.setter 

245 def response_transformation_matrix(self, value): 

246 self._response_transformation_matrix = value 

247 

248 @property 

249 def reference_transformation_matrix(self): 

250 return self._reference_transformation_matrix 

251 

252 @reference_transformation_matrix.setter 

253 def reference_transformation_matrix(self, value): 

254 self._reference_transformation_matrix = value 

255 

256 @property 

257 def samples_per_acquire(self): 

258 """Property returning the samples per acquisition step given the overlap""" 

259 return int(self.samples_per_frame * (1 - self.cpsd_overlap)) 

260 

261 @property 

262 def frame_time(self): 

263 """Property returning the time per measurement frame""" 

264 return self.samples_per_frame / self.sample_rate 

265 

266 @property 

267 def nyquist_frequency(self): 

268 """Property returning half the sample rate""" 

269 return self.sample_rate / 2 

270 

271 @property 

272 def fft_lines(self): 

273 """Property returning the frequency lines given the sampling parameters""" 

274 return self.samples_per_frame // 2 + 1 

275 

276 @property 

277 def frequency_spacing(self): 

278 """Property returning frequency line spacing given the sampling parameters""" 

279 return self.sample_rate / self.samples_per_frame 

280 

281 @property 

282 def samples_per_output(self): 

283 """Property returning the samples per output given the COLA overlap""" 

284 return int(self.samples_per_frame * (1 - self.cola_overlap)) 

285 

286 @property 

287 def overlapped_output_samples(self): 

288 """Property returning the number of output samples that are overlapped.""" 

289 return self.samples_per_frame - self.samples_per_output 

290 

291 @property 

292 def skip_frames(self): 

293 """Property returning the number of frames to skip when changing levels""" 

294 return int( 

295 np.ceil( 

296 self.test_level_ramp_time 

297 * self.sample_rate 

298 / (self.samples_per_frame * (1 - self.cpsd_overlap)) 

299 ) 

300 ) 

301 

302 def store_to_netcdf( 

303 self, 

304 netcdf_group_handle: nc4._netCDF4.Group, # pylint: disable=c-extension-no-member 

305 ): 

306 """Store parameters to a group in a netCDF streaming file. 

307 

308 This function stores parameters from the environment into the netCDF 

309 file in a group with the environment's name as its name. The function 

310 will receive a reference to the group within the dataset and should 

311 store the environment's parameters into that group in the form of 

312 attributes, dimensions, or variables. 

313 

314 This function is the "write" counterpart to the retrieve_metadata 

315 function in the RandomVibrationUI class, which will read parameters from 

316 the netCDF file to populate the parameters in the user interface. 

317 

318 Parameters 

319 ---------- 

320 netcdf_group_handle : nc4._netCDF4.Group 

321 A reference to the Group within the netCDF dataset where the 

322 environment's metadata is stored. 

323 

324 """ 

325 super().store_to_netcdf(netcdf_group_handle) 

326 netcdf_group_handle.samples_per_frame = self.samples_per_frame 

327 netcdf_group_handle.test_level_ramp_time = self.test_level_ramp_time 

328 netcdf_group_handle.cpsd_overlap = self.cpsd_overlap 

329 netcdf_group_handle.update_tf_during_control = 1 if self.update_tf_during_control else 0 

330 netcdf_group_handle.cola_window = self.cola_window 

331 netcdf_group_handle.cola_overlap = self.cola_overlap 

332 netcdf_group_handle.cola_window_exponent = self.cola_window_exponent 

333 netcdf_group_handle.frames_in_cpsd = self.frames_in_cpsd 

334 netcdf_group_handle.cpsd_window = self.cpsd_window 

335 netcdf_group_handle.control_python_script = self.control_python_script 

336 netcdf_group_handle.control_python_function = self.control_python_function 

337 netcdf_group_handle.control_python_function_type = self.control_python_function_type 

338 netcdf_group_handle.control_python_function_parameters = ( 

339 self.control_python_function_parameters 

340 ) 

341 netcdf_group_handle.allow_automatic_aborts = 1 if self.allow_automatic_aborts else 0 

342 # Specifications 

343 netcdf_group_handle.createDimension("fft_lines", self.fft_lines) 

344 netcdf_group_handle.createDimension("two", 2) 

345 netcdf_group_handle.createDimension( 

346 "specification_channels", self.specification_cpsd_matrix.shape[-1] 

347 ) 

348 var = netcdf_group_handle.createVariable( 

349 "specification_frequency_lines", "f8", ("fft_lines",) 

350 ) 

351 var[...] = self.specification_frequency_lines 

352 var = netcdf_group_handle.createVariable( 

353 "specification_cpsd_matrix_real", 

354 "f8", 

355 ("fft_lines", "specification_channels", "specification_channels"), 

356 ) 

357 var[...] = self.specification_cpsd_matrix.real 

358 var = netcdf_group_handle.createVariable( 

359 "specification_cpsd_matrix_imag", 

360 "f8", 

361 ("fft_lines", "specification_channels", "specification_channels"), 

362 ) 

363 var[...] = self.specification_cpsd_matrix.imag 

364 var = netcdf_group_handle.createVariable( 

365 "specification_warning_matrix", 

366 "f8", 

367 ("two", "fft_lines", "specification_channels"), 

368 ) 

369 var[...] = self.specification_warning_matrix.real 

370 var = netcdf_group_handle.createVariable( 

371 "specification_abort_matrix", 

372 "f8", 

373 ("two", "fft_lines", "specification_channels"), 

374 ) 

375 var[...] = self.specification_abort_matrix.real 

376 # Transformation matrices 

377 if self.response_transformation_matrix is not None: 

378 netcdf_group_handle.createDimension( 

379 "response_transformation_rows", 

380 self.response_transformation_matrix.shape[0], 

381 ) 

382 netcdf_group_handle.createDimension( 

383 "response_transformation_cols", 

384 self.response_transformation_matrix.shape[1], 

385 ) 

386 var = netcdf_group_handle.createVariable( 

387 "response_transformation_matrix", 

388 "f8", 

389 ("response_transformation_rows", "response_transformation_cols"), 

390 ) 

391 var[...] = self.response_transformation_matrix 

392 if self.reference_transformation_matrix is not None: 

393 netcdf_group_handle.createDimension( 

394 "reference_transformation_rows", 

395 self.reference_transformation_matrix.shape[0], 

396 ) 

397 netcdf_group_handle.createDimension( 

398 "reference_transformation_cols", 

399 self.reference_transformation_matrix.shape[1], 

400 ) 

401 var = netcdf_group_handle.createVariable( 

402 "reference_transformation_matrix", 

403 "f8", 

404 ("reference_transformation_rows", "reference_transformation_cols"), 

405 ) 

406 var[...] = self.reference_transformation_matrix 

407 # Control channels 

408 netcdf_group_handle.createDimension("control_channels", len(self.control_channel_indices)) 

409 var = netcdf_group_handle.createVariable( 

410 "control_channel_indices", "i4", ("control_channels") 

411 ) 

412 var[...] = self.control_channel_indices 

413 

414 

415# %% UI 

416 

417from .abstract_interactive_control_law import ( # noqa: E402 pylint: disable=wrong-import-position 

418 AbstractControlLawComputation, 

419) 

420from .data_collector import ( # noqa: E402 pylint: disable=wrong-import-position 

421 Acceptance, 

422 AcquisitionType, 

423 CollectorMetadata, 

424 DataCollectorCommands, 

425 TriggerSlope, 

426 Window, 

427 data_collector_process, 

428) 

429from .random_vibration_sys_id_data_analysis import ( # noqa: E402 pylint: disable=wrong-import-position 

430 RandomVibrationDataAnalysisCommands, 

431 random_data_analysis_process, 

432) 

433from .signal_generation import ( # noqa: E402 pylint: disable=wrong-import-position 

434 CPSDSignalGenerator, 

435) 

436from .signal_generation_process import ( # noqa: E402 pylint: disable=wrong-import-position 

437 SignalGenerationCommands, 

438 SignalGenerationMetadata, 

439 signal_generation_process, 

440) 

441from .spectral_processing import ( # noqa: E402 pylint: disable=wrong-import-position 

442 AveragingTypes, 

443 Estimator, 

444 SpectralProcessingCommands, 

445 SpectralProcessingMetadata, 

446 spectral_processing_process, 

447) 

448 

449 

450class RandomVibrationUI(AbstractSysIdUI): 

451 """Class defining the user interface for a Random Vibration environment. 

452 

453 This class will contain four main UIs, the environment definition, 

454 system identification, test prediction, and run. The widgets corresponding 

455 to these interfaces are stored in TabWidgets in the main UI. 

456 

457 This class defines all the call backs and user interface operations required 

458 for the Random Vibration environment.""" 

459 

460 def __init__( 

461 self, 

462 environment_name: str, 

463 definition_tabwidget: QtWidgets.QTabWidget, 

464 system_id_tabwidget: QtWidgets.QTabWidget, 

465 test_predictions_tabwidget: QtWidgets.QTabWidget, 

466 run_tabwidget: QtWidgets.QTabWidget, 

467 environment_command_queue: VerboseMessageQueue, 

468 controller_communication_queue: VerboseMessageQueue, 

469 log_file_queue: Queue, 

470 ): 

471 """ 

472 Constructs a Random Vibration User Interface 

473 

474 Given the tab widgets from the main interface as well as communication 

475 queues, this class assembles the user interface components specific to 

476 the Random Vibration Environment 

477 

478 Parameters 

479 ---------- 

480 definition_tabwidget : QtWidgets.QTabWidget 

481 QTabWidget containing the environment subtabs on the Control 

482 Definition main tab 

483 system_id_tabwidget : QtWidgets.QTabWidget 

484 QTabWidget containing the environment subtabs on the System 

485 Identification main tab 

486 test_predictions_tabwidget : QtWidgets.QTabWidget 

487 QTabWidget containing the environment subtabs on the Test Predictions 

488 main tab 

489 run_tabwidget : QtWidgets.QTabWidget 

490 QTabWidget containing the environment subtabs on the Run 

491 main tab. 

492 environment_command_queue : VerboseMessageQueue 

493 Queue for sending commands to the Random Vibration Environment 

494 controller_communication_queue : VerboseMessageQueue 

495 Queue for sending global commands to the controller 

496 log_file_queue : Queue 

497 Queue where log file messages can be written. 

498 

499 """ 

500 super().__init__( 

501 environment_name, 

502 environment_command_queue, 

503 controller_communication_queue, 

504 log_file_queue, 

505 system_id_tabwidget, 

506 ) 

507 # Add the page to the control definition tabwidget 

508 self.definition_widget = QtWidgets.QWidget() 

509 uic.loadUi(environment_definition_ui_paths[CONTROL_TYPE], self.definition_widget) 

510 definition_tabwidget.addTab(self.definition_widget, self.environment_name) 

511 # Add the page to the control prediction tabwidget 

512 self.prediction_widget = QtWidgets.QWidget() 

513 uic.loadUi(environment_prediction_ui_paths[CONTROL_TYPE], self.prediction_widget) 

514 test_predictions_tabwidget.addTab(self.prediction_widget, self.environment_name) 

515 # Add the page to the run tabwidget 

516 self.run_widget = QtWidgets.QWidget() 

517 uic.loadUi(environment_run_ui_paths[CONTROL_TYPE], self.run_widget) 

518 run_tabwidget.addTab(self.run_widget, self.environment_name) 

519 

520 self.plot_data_items = {} 

521 self.plot_windows = [] 

522 self.run_start_time = None 

523 self.run_level_start_time = None 

524 self.run_timer = QTimer() 

525 self.response_transformation_matrix = None 

526 self.output_transformation_matrix = None 

527 self.python_control_module = None 

528 self.specification_frequency_lines = None 

529 self.specification_cpsd_matrix = None 

530 self.specification_warning_matrix = None 

531 self.specification_abort_matrix = None 

532 self.physical_channel_names = None 

533 self.physical_output_indices = None 

534 self.excitation_prediction = None 

535 self.response_prediction = None 

536 self.rms_voltage_prediction = None 

537 self.rms_db_error_prediction = None 

538 self.interactive_control_law_widget = None 

539 self.interactive_control_law_window = None 

540 self.control_selector_widgets = [ 

541 self.definition_widget.specification_row_selector, 

542 self.definition_widget.specification_column_selector, 

543 self.prediction_widget.response_row_selector, 

544 self.prediction_widget.response_column_selector, 

545 self.run_widget.control_channel_1_selector, 

546 self.run_widget.control_channel_2_selector, 

547 ] 

548 self.output_selector_widgets = [ 

549 self.prediction_widget.excitation_row_selector, 

550 self.prediction_widget.excitation_column_selector, 

551 ] 

552 self.system_id_widget.samplesPerFrameSpinBox.setReadOnly(True) 

553 self.system_id_widget.samplesPerFrameSpinBox.setButtonSymbols( 

554 QtWidgets.QAbstractSpinBox.ButtonSymbols.NoButtons 

555 ) 

556 self.system_id_widget.levelRampTimeDoubleSpinBox.setReadOnly(True) 

557 self.system_id_widget.levelRampTimeDoubleSpinBox.setButtonSymbols( 

558 QtWidgets.QAbstractSpinBox.ButtonSymbols.NoButtons 

559 ) 

560 

561 # Set common look and feel for plots 

562 plot_widgets = [ 

563 self.definition_widget.specification_single_plot, 

564 self.definition_widget.specification_sum_asds_plot, 

565 self.prediction_widget.excitation_display_plot, 

566 self.prediction_widget.response_display_plot, 

567 self.run_widget.global_test_performance_plot, 

568 ] 

569 for plot_widget in plot_widgets: 

570 plot_item = plot_widget.getPlotItem() 

571 plot_item.showGrid(True, True, 0.25) 

572 plot_item.enableAutoRange() 

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

574 logscale_plot_widgets = [ 

575 self.definition_widget.specification_single_plot, 

576 self.definition_widget.specification_sum_asds_plot, 

577 self.prediction_widget.excitation_display_plot, 

578 self.prediction_widget.response_display_plot, 

579 self.run_widget.global_test_performance_plot, 

580 ] 

581 for plot_widget in logscale_plot_widgets: 

582 plot_item = plot_widget.getPlotItem() 

583 plot_item.setLogMode(False, True) 

584 

585 self.connect_callbacks() 

586 

587 # Complete the profile commands 

588 self.command_map["Set Test Level"] = self.change_test_level_from_profile 

589 self.command_map["Change Specification"] = self.change_specification_from_profile 

590 self.command_map["Save Control Data"] = self.save_control_data_from_profile 

591 

592 def connect_callbacks(self): 

593 """Connects callback functions to the UI Widgets""" 

594 # Definition 

595 self.definition_widget.samples_per_frame_selector.valueChanged.connect( 

596 self.update_parameters_and_clear_spec 

597 ) 

598 self.definition_widget.cpsd_overlap_selector.valueChanged.connect(self.update_parameters) 

599 self.definition_widget.cola_overlap_percentage_selector.valueChanged.connect( 

600 self.update_parameters 

601 ) 

602 self.definition_widget.transformation_matrices_button.clicked.connect( 

603 self.define_transformation_matrices 

604 ) 

605 self.definition_widget.control_script_load_file_button.clicked.connect( 

606 self.select_python_module 

607 ) 

608 self.definition_widget.control_function_input.currentIndexChanged.connect( 

609 self.update_generator_selector 

610 ) 

611 self.definition_widget.load_spec_button.clicked.connect(self.select_spec_file) 

612 self.definition_widget.specification_row_selector.currentIndexChanged.connect( 

613 self.show_specification 

614 ) 

615 self.definition_widget.specification_column_selector.currentIndexChanged.connect( 

616 self.show_specification 

617 ) 

618 self.definition_widget.control_channels_selector.itemChanged.connect( 

619 self.update_control_channels 

620 ) 

621 self.definition_widget.check_selected_button.clicked.connect( 

622 self.check_selected_control_channels 

623 ) 

624 self.definition_widget.uncheck_selected_button.clicked.connect( 

625 self.uncheck_selected_control_channels 

626 ) 

627 # Prediction 

628 self.prediction_widget.excitation_row_selector.currentIndexChanged.connect( 

629 self.update_control_predictions 

630 ) 

631 self.prediction_widget.excitation_column_selector.currentIndexChanged.connect( 

632 self.update_control_predictions 

633 ) 

634 self.prediction_widget.response_row_selector.currentIndexChanged.connect( 

635 self.update_control_predictions 

636 ) 

637 self.prediction_widget.response_column_selector.currentIndexChanged.connect( 

638 self.update_control_predictions 

639 ) 

640 self.prediction_widget.maximum_voltage_button.clicked.connect( 

641 self.show_max_voltage_prediction 

642 ) 

643 self.prediction_widget.minimum_voltage_button.clicked.connect( 

644 self.show_min_voltage_prediction 

645 ) 

646 self.prediction_widget.maximum_error_button.clicked.connect(self.show_max_error_prediction) 

647 self.prediction_widget.minimum_error_button.clicked.connect(self.show_min_error_prediction) 

648 self.prediction_widget.response_error_list.itemClicked.connect( 

649 self.update_response_error_prediction_selector 

650 ) 

651 self.prediction_widget.excitation_voltage_list.itemClicked.connect( 

652 self.update_excitation_prediction_selector 

653 ) 

654 self.prediction_widget.recompute_prediction_button.clicked.connect( 

655 self.recompute_prediction 

656 ) 

657 # Run Test 

658 self.run_widget.current_test_level_selector.valueChanged.connect( 

659 self.change_control_test_level 

660 ) 

661 self.run_widget.start_test_button.clicked.connect(self.start_control) 

662 self.run_widget.stop_test_button.clicked.connect(self.stop_control) 

663 self.run_widget.create_window_button.clicked.connect(self.create_window) 

664 self.run_widget.show_all_asds_button.clicked.connect(self.show_all_asds) 

665 self.run_widget.show_all_csds_phscoh_button.clicked.connect(self.show_all_csds_phscoh) 

666 self.run_widget.show_all_csds_realimag_button.clicked.connect(self.show_all_csds_realimag) 

667 self.run_widget.tile_windows_button.clicked.connect(self.tile_windows) 

668 self.run_widget.close_windows_button.clicked.connect(self.close_windows) 

669 self.run_timer.timeout.connect(self.update_run_time) 

670 self.run_widget.test_response_error_list.itemDoubleClicked.connect( 

671 self.show_magnitude_window 

672 ) 

673 self.run_widget.save_current_spectral_data_button.clicked.connect(self.save_spectral_data) 

674 

675 # %% Initialize Data Aquisition 

676 

677 def initialize_data_acquisition(self, data_acquisition_parameters: DataAcquisitionParameters): 

678 """Update the user interface with data acquisition parameters 

679 

680 This function is called when the Data Acquisition parameters are 

681 initialized. This function should set up the environment user interface 

682 accordingly. 

683 

684 Parameters 

685 ---------- 

686 data_acquisition_parameters : DataAcquisitionParameters : 

687 Container containing the data acquisition parameters, including 

688 channel table and sampling information. 

689 

690 """ 

691 super().initialize_data_acquisition(data_acquisition_parameters) 

692 # Initialize the plots 

693 # Clear plots if there is anything on them 

694 self.definition_widget.specification_single_plot.getPlotItem().clear() 

695 self.definition_widget.specification_sum_asds_plot.getPlotItem().clear() 

696 self.run_widget.global_test_performance_plot.getPlotItem().clear() 

697 

698 # Now add initial lines that we can update later 

699 self.definition_widget.specification_single_plot.getPlotItem().addLegend() 

700 self.plot_data_items[ 

701 "specification_real" 

702 ] = self.definition_widget.specification_single_plot.getPlotItem().plot( 

703 np.array([0, data_acquisition_parameters.sample_rate / 2]), 

704 np.zeros(2), 

705 pen={"color": "b", "width": 1}, 

706 name="Real Part", 

707 ) 

708 self.plot_data_items[ 

709 "specification_imag" 

710 ] = self.definition_widget.specification_single_plot.getPlotItem().plot( 

711 np.array([0, data_acquisition_parameters.sample_rate / 2]), 

712 np.zeros(2), 

713 pen={"color": "r", "width": 1}, 

714 name="Imaginary Part", 

715 ) 

716 self.plot_data_items[ 

717 "specification_warning_upper" 

718 ] = self.definition_widget.specification_single_plot.getPlotItem().plot( 

719 np.array([0, data_acquisition_parameters.sample_rate / 2]), 

720 np.zeros(2), 

721 pen={"color": PlotWindow.WARNING_COLOR, "width": 0.25}, 

722 name="Warning", 

723 ) 

724 self.plot_data_items[ 

725 "specification_warning_lower" 

726 ] = self.definition_widget.specification_single_plot.getPlotItem().plot( 

727 np.array([0, data_acquisition_parameters.sample_rate / 2]), 

728 np.zeros(2), 

729 pen={"color": PlotWindow.WARNING_COLOR, "width": 0.25}, 

730 ) 

731 self.plot_data_items[ 

732 "specification_abort_upper" 

733 ] = self.definition_widget.specification_single_plot.getPlotItem().plot( 

734 np.array([0, data_acquisition_parameters.sample_rate / 2]), 

735 np.zeros(2), 

736 pen={"color": PlotWindow.ABORT_COLOR, "width": 0.25}, 

737 name="Abort", 

738 ) 

739 self.plot_data_items[ 

740 "specification_abort_lower" 

741 ] = self.definition_widget.specification_single_plot.getPlotItem().plot( 

742 np.array([0, data_acquisition_parameters.sample_rate / 2]), 

743 np.zeros(2), 

744 pen={"color": PlotWindow.ABORT_COLOR, "width": 0.25}, 

745 ) 

746 self.plot_data_items[ 

747 "specification_sum" 

748 ] = self.definition_widget.specification_sum_asds_plot.getPlotItem().plot( 

749 np.array([0, data_acquisition_parameters.sample_rate / 2]), 

750 np.zeros(2), 

751 pen={"color": "b", "width": 1}, 

752 ) 

753 self.run_widget.global_test_performance_plot.getPlotItem().addLegend() 

754 self.plot_data_items[ 

755 "specification_sum_control" 

756 ] = self.run_widget.global_test_performance_plot.getPlotItem().plot( 

757 np.array([0, data_acquisition_parameters.sample_rate / 2]), 

758 np.zeros(2), 

759 pen={"color": "b", "width": 1}, 

760 name="Specification", 

761 ) 

762 self.plot_data_items[ 

763 "sum_asds_control" 

764 ] = self.run_widget.global_test_performance_plot.getPlotItem().plot( 

765 np.array([0, data_acquisition_parameters.sample_rate / 2]), 

766 np.zeros(2), 

767 pen={"color": "r", "width": 1}, 

768 name="Response", 

769 ) 

770 

771 # Set up channel names 

772 self.physical_channel_names = [ 

773 ( 

774 f"{'' if channel.channel_type is None else channel.channel_type} " 

775 f"{channel.node_number} " 

776 f"{'' if channel.node_direction is None else channel.node_direction}" 

777 )[:MAXIMUM_NAME_LENGTH] 

778 for channel in data_acquisition_parameters.channel_list 

779 ] 

780 self.physical_output_indices = [ 

781 i 

782 for i, channel in enumerate(data_acquisition_parameters.channel_list) 

783 if channel.feedback_device 

784 ] 

785 # Set up widgets 

786 self.definition_widget.sample_rate_display.setValue(data_acquisition_parameters.sample_rate) 

787 self.definition_widget.samples_per_frame_selector.setValue( 

788 data_acquisition_parameters.sample_rate 

789 ) 

790 self.definition_widget.control_channels_selector.clear() 

791 for channel_name in self.physical_channel_names: 

792 item = QtWidgets.QListWidgetItem() 

793 item.setText(channel_name) 

794 item.setFlags(item.flags() | Qt.ItemIsUserCheckable) 

795 item.setCheckState(Qt.Unchecked) 

796 self.definition_widget.control_channels_selector.addItem(item) 

797 self.definition_widget.input_channels_display.setValue(len(self.physical_channel_names)) 

798 self.definition_widget.output_channels_display.setValue(len(self.physical_output_indices)) 

799 self.definition_widget.control_channels_display.setValue(0) 

800 self.response_transformation_matrix = None 

801 self.output_transformation_matrix = None 

802 self.define_transformation_matrices(None, False) 

803 

804 @property 

805 def physical_output_names(self): 

806 """Names of the physical output channels""" 

807 return [self.physical_channel_names[i] for i in self.physical_output_indices] 

808 

809 # %% Define Environments 

810 

811 @property 

812 def physical_control_indices(self): 

813 """Indices corresponding to the physical channels that are used as outputs""" 

814 return [ 

815 i 

816 for i in range(self.definition_widget.control_channels_selector.count()) 

817 if self.definition_widget.control_channels_selector.item(i).checkState() == Qt.Checked 

818 ] 

819 

820 @property 

821 def physical_control_names(self): 

822 """Names of the physical control channels""" 

823 return [self.physical_channel_names[i] for i in self.physical_control_indices] 

824 

825 @property 

826 def initialized_control_names(self): 

827 if self.environment_parameters.response_transformation_matrix is None: 

828 return [ 

829 self.physical_channel_names[i] 

830 for i in self.environment_parameters.control_channel_indices 

831 ] 

832 else: 

833 return [ 

834 f"Transformed Response {i + 1}" 

835 for i in range(self.environment_parameters.response_transformation_matrix.shape[0]) 

836 ] 

837 

838 @property 

839 def initialized_output_names(self): 

840 if self.environment_parameters.reference_transformation_matrix is None: 

841 return self.physical_output_names 

842 else: 

843 return [ 

844 f"Transformed Drive {i + 1}" 

845 for i in range(self.environment_parameters.reference_transformation_matrix.shape[0]) 

846 ] 

847 

848 def select_spec_file(self, clicked, filename=None): # pylint: disable=unused-argument 

849 """Loads a specification using a dialog or the specified filename 

850 

851 Parameters 

852 ---------- 

853 clicked : 

854 The clicked event that triggered the callback. 

855 filename : 

856 File name defining the specification for bypassing the callback when 

857 loading from a file (Default value = None). 

858 

859 """ 

860 if filename is None: 

861 filename, _ = QtWidgets.QFileDialog.getOpenFileName( 

862 self.definition_widget, 

863 "Select Specification File", 

864 filter="Numpyz or Mat (*.npz *.mat)", 

865 ) 

866 if filename == "": 

867 return 

868 self.definition_widget.specification_file_name_display.setText(filename) 

869 coord_dtype = np.dtype([("node", "<u8"), ("direction", "i1")]) 

870 if self.response_transformation_matrix is not None: 

871 control_coordinate = None 

872 else: 

873 control_coordinate = np.array( 

874 [ 

875 ( 

876 self.data_acquisition_parameters.channel_list[i].node_number, 

877 _direction_map[ 

878 self.data_acquisition_parameters.channel_list[i].node_direction 

879 ], 

880 ) 

881 for i in self.physical_control_indices 

882 ], 

883 dtype=coord_dtype, 

884 ) 

885 try: 

886 ( 

887 self.specification_frequency_lines, 

888 self.specification_cpsd_matrix, 

889 self.specification_warning_matrix, 

890 self.specification_abort_matrix, 

891 ) = load_specification( 

892 filename, 

893 self.definition_widget.fft_lines_display.value(), 

894 self.definition_widget.frequency_spacing_display.value(), 

895 control_coordinate, 

896 ) 

897 except ValueError as e: 

898 error_message_qt(type(e).__name__, str(e)) 

899 return 

900 

901 if np.all(np.isnan(self.specification_abort_matrix)): 

902 self.definition_widget.auto_abort_checkbox.setChecked(False) 

903 self.definition_widget.auto_abort_checkbox.setEnabled(False) 

904 else: 

905 self.definition_widget.auto_abort_checkbox.setEnabled(True) 

906 self.show_specification() 

907 

908 def select_python_module(self, clicked, filename=None): # pylint: disable=unused-argument 

909 """Loads a Python module using a dialog or the specified filename 

910 

911 Parameters 

912 ---------- 

913 clicked : 

914 The clicked event that triggered the callback. 

915 filename : 

916 File name defining the Python module for bypassing the callback when 

917 loading from a file (Default value = None). 

918 

919 """ 

920 if filename is None: 

921 filename, _ = QtWidgets.QFileDialog.getOpenFileName( 

922 self.definition_widget, 

923 "Select Python Module", 

924 filter="Python Modules (*.py)", 

925 ) 

926 if filename == "": 

927 return 

928 self.python_control_module = load_python_module(filename) 

929 functions = [ 

930 function 

931 for function in inspect.getmembers(self.python_control_module) 

932 if ( 

933 inspect.isfunction(function[1]) 

934 and len(inspect.signature(function[1]).parameters) >= 12 

935 ) 

936 or inspect.isgeneratorfunction(function[1]) 

937 or ( 

938 inspect.isclass(function[1]) 

939 and all( 

940 [ 

941 ( 

942 method in function[1].__dict__ 

943 and not ( 

944 hasattr(function[1].__dict__[method], "__isabstractmethod__") 

945 and function[1].__dict__[method].__isabstractmethod__ 

946 ) 

947 ) 

948 for method in ["system_id_update", "control"] 

949 ] 

950 ) 

951 ) 

952 ] 

953 self.log( 

954 f"Loaded module {self.python_control_module.__name__} with " 

955 f"functions {[function[0] for function in functions]}" 

956 ) 

957 self.definition_widget.control_function_input.clear() 

958 self.definition_widget.control_script_file_path_input.setText(filename) 

959 for function in functions: 

960 self.definition_widget.control_function_input.addItem(function[0]) 

961 

962 def update_generator_selector(self): 

963 """Updates the function/generator selector based on the function selected""" 

964 if self.python_control_module is None: 

965 return 

966 try: 

967 function = getattr( 

968 self.python_control_module, 

969 self.definition_widget.control_function_input.itemText( 

970 self.definition_widget.control_function_input.currentIndex() 

971 ), 

972 ) 

973 except AttributeError: 

974 return 

975 if inspect.isgeneratorfunction(function): 

976 self.definition_widget.control_function_generator_selector.setCurrentIndex(1) 

977 elif inspect.isclass(function) and issubclass(function, AbstractControlLawComputation): 

978 self.definition_widget.control_function_generator_selector.setCurrentIndex(3) 

979 elif inspect.isclass(function): 

980 self.definition_widget.control_function_generator_selector.setCurrentIndex(2) 

981 else: 

982 self.definition_widget.control_function_generator_selector.setCurrentIndex(0) 

983 

984 def show_specification(self): 

985 """Show the specification on the GUI""" 

986 if self.specification_cpsd_matrix is None: 

987 self.plot_data_items["specification_real"].setData( 

988 np.array([0, self.definition_widget.sample_rate_display.value() / 2]), 

989 np.zeros(2), 

990 ) 

991 self.plot_data_items["specification_imag"].setData( 

992 np.array([0, self.definition_widget.sample_rate_display.value() / 2]), 

993 np.zeros(2), 

994 ) 

995 self.plot_data_items["specification_sum"].setData( 

996 np.array([0, self.definition_widget.sample_rate_display.value() / 2]), 

997 np.zeros(2), 

998 ) 

999 self.plot_data_items["specification_warning_upper"].setData( 

1000 np.array([0, self.definition_widget.sample_rate_display.value() / 2]), 

1001 np.zeros(2), 

1002 ) 

1003 self.plot_data_items["specification_warning_lower"].setData( 

1004 np.array([0, self.definition_widget.sample_rate_display.value() / 2]), 

1005 np.zeros(2), 

1006 ) 

1007 self.plot_data_items["specification_abort_upper"].setData( 

1008 np.array([0, self.definition_widget.sample_rate_display.value() / 2]), 

1009 np.zeros(2), 

1010 ) 

1011 self.plot_data_items["specification_abort_lower"].setData( 

1012 np.array([0, self.definition_widget.sample_rate_display.value() / 2]), 

1013 np.zeros(2), 

1014 ) 

1015 # enabled_state = self.run_widget.isEnabled() 

1016 # self.run_widget.setEnabled(True) 

1017 self.plot_data_items["specification_sum_control"].setData( 

1018 np.array([0, self.definition_widget.sample_rate_display.value() / 2]), 

1019 np.zeros(2), 

1020 ) 

1021 # self.run_widget.setEnabled(enabled_state) 

1022 else: 

1023 row = self.definition_widget.specification_row_selector.currentIndex() 

1024 column = self.definition_widget.specification_column_selector.currentIndex() 

1025 spec_real = abs(self.specification_cpsd_matrix[:, row, column].real) 

1026 spec_imag = abs(self.specification_cpsd_matrix[:, row, column].imag) 

1027 spec_sum = abs( 

1028 np.nansum( 

1029 self.specification_cpsd_matrix[ 

1030 :, 

1031 np.arange(self.specification_cpsd_matrix.shape[-1]), 

1032 np.arange(self.specification_cpsd_matrix.shape[-1]), 

1033 ], 

1034 axis=-1, 

1035 ) 

1036 ) 

1037 self.plot_data_items["specification_real"].setData( 

1038 self.specification_frequency_lines[spec_real > 0.0], 

1039 spec_real[spec_real > 0.0], 

1040 ) 

1041 self.plot_data_items["specification_imag"].setData( 

1042 self.specification_frequency_lines[spec_imag > 0.0], 

1043 spec_imag[spec_imag > 0.0], 

1044 ) 

1045 if row == column: 

1046 warning_upper = abs(self.specification_warning_matrix[1, :, row]) 

1047 warning_lower = abs(self.specification_warning_matrix[0, :, row]) 

1048 abort_upper = abs(self.specification_abort_matrix[1, :, row]) 

1049 abort_lower = abs(self.specification_abort_matrix[0, :, row]) 

1050 self.plot_data_items["specification_warning_upper"].setData( 

1051 self.specification_frequency_lines, warning_upper 

1052 ) 

1053 self.plot_data_items["specification_warning_lower"].setData( 

1054 self.specification_frequency_lines, warning_lower 

1055 ) 

1056 self.plot_data_items["specification_abort_upper"].setData( 

1057 self.specification_frequency_lines, abort_upper 

1058 ) 

1059 self.plot_data_items["specification_abort_lower"].setData( 

1060 self.specification_frequency_lines, abort_lower 

1061 ) 

1062 else: 

1063 self.plot_data_items["specification_warning_upper"].setData( 

1064 np.array([0, self.definition_widget.sample_rate_display.value() / 2]), 

1065 np.zeros(2), 

1066 ) 

1067 self.plot_data_items["specification_warning_lower"].setData( 

1068 np.array([0, self.definition_widget.sample_rate_display.value() / 2]), 

1069 np.zeros(2), 

1070 ) 

1071 self.plot_data_items["specification_abort_upper"].setData( 

1072 np.array([0, self.definition_widget.sample_rate_display.value() / 2]), 

1073 np.zeros(2), 

1074 ) 

1075 self.plot_data_items["specification_abort_lower"].setData( 

1076 np.array([0, self.definition_widget.sample_rate_display.value() / 2]), 

1077 np.zeros(2), 

1078 ) 

1079 self.plot_data_items["specification_sum"].setData( 

1080 self.specification_frequency_lines[spec_sum > 0.0], 

1081 spec_sum[spec_sum > 0.0], 

1082 ) 

1083 # enabled_state = self.run_widget.isEnabled() 

1084 # self.run_widget.setEnabled(True) 

1085 self.plot_data_items["specification_sum_control"].setData( 

1086 self.specification_frequency_lines[spec_sum > 0.0], 

1087 spec_sum[spec_sum > 0.0], 

1088 ) 

1089 # self.run_widget.setEnabled(enabled_state) 

1090 

1091 def check_selected_control_channels(self): 

1092 """Checks the selected channels to make them control channels""" 

1093 for item in self.definition_widget.control_channels_selector.selectedItems(): 

1094 item.setCheckState(Qt.Checked) 

1095 

1096 def uncheck_selected_control_channels(self): 

1097 """Unchecks the selected channels to make them no longer control channels""" 

1098 for item in self.definition_widget.control_channels_selector.selectedItems(): 

1099 item.setCheckState(Qt.Unchecked) 

1100 

1101 def update_control_channels(self): 

1102 """Resets the definition UI when the number of control channels has changed""" 

1103 self.response_transformation_matrix = None 

1104 self.output_transformation_matrix = None 

1105 self.specification_abort_matrix = None 

1106 self.specification_warning_matrix = None 

1107 self.specification_cpsd_matrix = None 

1108 self.specification_frequency_lines = None 

1109 self.definition_widget.control_channels_display.setValue(len(self.physical_control_indices)) 

1110 self.definition_widget.specification_row_selector.blockSignals(True) 

1111 self.definition_widget.specification_column_selector.blockSignals(True) 

1112 self.definition_widget.specification_row_selector.clear() 

1113 self.definition_widget.specification_column_selector.clear() 

1114 for i, control_name in enumerate(self.physical_control_names): 

1115 self.definition_widget.specification_row_selector.addItem(f"{i + 1}: {control_name}") 

1116 self.definition_widget.specification_column_selector.addItem(f"{i + 1}: {control_name}") 

1117 self.definition_widget.specification_row_selector.blockSignals(False) 

1118 self.definition_widget.specification_column_selector.blockSignals(False) 

1119 self.define_transformation_matrices(None, False) 

1120 self.show_specification() 

1121 

1122 def define_transformation_matrices( 

1123 self, clicked, dialog=True 

1124 ): # pylint: disable=unused-argument 

1125 """Defines the transformation matrices using the dialog box""" 

1126 if dialog: 

1127 (response_transformation, output_transformation, result) = ( 

1128 TransformationMatrixWindow.define_transformation_matrices( 

1129 self.response_transformation_matrix, 

1130 self.definition_widget.control_channels_display.value(), 

1131 self.output_transformation_matrix, 

1132 self.definition_widget.output_channels_display.value(), 

1133 self.definition_widget, 

1134 ) 

1135 ) 

1136 else: 

1137 response_transformation = self.response_transformation_matrix 

1138 output_transformation = self.output_transformation_matrix 

1139 result = True 

1140 if result: 

1141 # Update the control names 

1142 for widget in self.control_selector_widgets: 

1143 widget.blockSignals(True) 

1144 widget.clear() 

1145 if response_transformation is None: 

1146 for i, control_name in enumerate(self.physical_control_names): 

1147 for widget in self.control_selector_widgets: 

1148 widget.addItem(f"{i + 1}: {control_name}") 

1149 self.definition_widget.transform_channels_display.setValue( 

1150 len(self.physical_control_names) 

1151 ) 

1152 else: 

1153 for i in range(response_transformation.shape[0]): 

1154 for widget in self.control_selector_widgets: 

1155 widget.addItem(f"{i + 1}: Transformed Response") 

1156 self.definition_widget.transform_channels_display.setValue( 

1157 response_transformation.shape[0] 

1158 ) 

1159 for widget in self.control_selector_widgets: 

1160 widget.blockSignals(False) 

1161 # Update the output names 

1162 for widget in self.output_selector_widgets: 

1163 widget.blockSignals(True) 

1164 widget.clear() 

1165 if output_transformation is None: 

1166 for i, drive_name in enumerate(self.physical_output_names): 

1167 for widget in self.output_selector_widgets: 

1168 widget.addItem(f"{i + 1}: {drive_name}") 

1169 self.definition_widget.transform_outputs_display.setValue( 

1170 len(self.physical_output_names) 

1171 ) 

1172 else: 

1173 for i in range(output_transformation.shape[0]): 

1174 for widget in self.output_selector_widgets: 

1175 widget.addItem(f"{i + 1}: Transformed Drive") 

1176 self.definition_widget.transform_outputs_display.setValue( 

1177 output_transformation.shape[0] 

1178 ) 

1179 for widget in self.output_selector_widgets: 

1180 widget.blockSignals(False) 

1181 

1182 self.response_transformation_matrix = response_transformation 

1183 self.output_transformation_matrix = output_transformation 

1184 self.update_parameters_and_clear_spec() 

1185 

1186 def update_parameters(self): 

1187 """Recompute derived parameters from updated sampling parameters""" 

1188 data = self.collect_environment_definition_parameters() 

1189 self.definition_widget.samples_per_acquire_display.setValue(data.samples_per_acquire) 

1190 self.definition_widget.frame_time_display.setValue(data.frame_time) 

1191 self.definition_widget.nyquist_frequency_display.setValue(data.nyquist_frequency) 

1192 self.definition_widget.fft_lines_display.setValue(data.fft_lines) 

1193 self.definition_widget.frequency_spacing_display.setValue(data.frequency_spacing) 

1194 self.definition_widget.samples_per_write_display.setValue(data.samples_per_output) 

1195 

1196 def update_parameters_and_clear_spec(self): 

1197 """Clears the specification data and updates parameters""" 

1198 samples_per_frame = self.definition_widget.samples_per_frame_selector.value() 

1199 if samples_per_frame % 2 != 0: 

1200 self.definition_widget.samples_per_frame_selector.blockSignals(True) 

1201 self.definition_widget.samples_per_frame_selector.setValue(samples_per_frame + 1) 

1202 self.definition_widget.samples_per_frame_selector.blockSignals(False) 

1203 self.specification_frequency_lines = None 

1204 self.specification_cpsd_matrix = None 

1205 self.specification_warning_matrix = None 

1206 self.specification_abort_matrix = None 

1207 self.definition_widget.specification_file_name_display.setText("") 

1208 self.show_specification() 

1209 self.update_parameters() 

1210 

1211 def collect_environment_definition_parameters(self) -> RandomVibrationMetadata: 

1212 """ 

1213 Collect the parameters from the user interface defining the environment 

1214 

1215 Returns 

1216 ------- 

1217 RandomVibrationMetadata 

1218 A metadata or parameters object containing the parameters defining 

1219 the corresponding environment. 

1220 

1221 """ 

1222 if self.python_control_module is None: 

1223 control_module = None 

1224 control_function = None 

1225 control_function_type = None 

1226 control_function_parameters = None 

1227 else: 

1228 control_module = self.definition_widget.control_script_file_path_input.text() 

1229 control_function = self.definition_widget.control_function_input.itemText( 

1230 self.definition_widget.control_function_input.currentIndex() 

1231 ) 

1232 control_function_type = ( 

1233 self.definition_widget.control_function_generator_selector.currentIndex() 

1234 ) 

1235 control_function_parameters = ( 

1236 self.definition_widget.control_parameters_text_input.toPlainText() 

1237 ) 

1238 return RandomVibrationMetadata( 

1239 number_of_channels=len(self.data_acquisition_parameters.channel_list), 

1240 sample_rate=self.definition_widget.sample_rate_display.value(), 

1241 samples_per_frame=self.definition_widget.samples_per_frame_selector.value(), 

1242 test_level_ramp_time=self.definition_widget.ramp_time_spinbox.value(), 

1243 cola_window=self.definition_widget.cola_window_selector.itemText( 

1244 self.definition_widget.cola_window_selector.currentIndex() 

1245 ), 

1246 cola_overlap=self.definition_widget.cola_overlap_percentage_selector.value() / 100, 

1247 cola_window_exponent=self.definition_widget.cola_exponent_selector.value(), 

1248 sigma_clip=self.definition_widget.sigma_clipping_selector.value(), 

1249 update_tf_during_control=self.definition_widget.update_transfer_function_during_control_selector.isChecked(), 

1250 frames_in_cpsd=self.definition_widget.cpsd_frames_selector.value(), 

1251 cpsd_window=self.definition_widget.cpsd_computation_window_selector.itemText( 

1252 self.definition_widget.cpsd_computation_window_selector.currentIndex() 

1253 ), 

1254 cpsd_overlap=self.definition_widget.cpsd_overlap_selector.value() / 100, 

1255 response_transformation_matrix=self.response_transformation_matrix, 

1256 output_transformation_matrix=self.output_transformation_matrix, 

1257 control_python_script=control_module, 

1258 control_python_function=control_function, 

1259 control_python_function_type=control_function_type, 

1260 control_python_function_parameters=control_function_parameters, 

1261 control_channel_indices=self.physical_control_indices, 

1262 output_channel_indices=self.physical_output_indices, 

1263 specification_frequency_lines=self.specification_frequency_lines, 

1264 specification_cpsd_matrix=self.specification_cpsd_matrix, 

1265 specification_warning_matrix=self.specification_warning_matrix, 

1266 specification_abort_matrix=self.specification_abort_matrix, 

1267 percent_lines_out=self.definition_widget.frequency_lines_out_spinbox.value(), 

1268 allow_automatic_aborts=self.definition_widget.auto_abort_checkbox.isChecked(), 

1269 ) 

1270 

1271 def initialize_environment(self) -> RandomVibrationMetadata: 

1272 """ 

1273 Update the user interface with environment parameters 

1274 

1275 This function is called when the Environment parameters are initialized. 

1276 This function should set up the user interface accordingly. It must 

1277 return the parameters class of the environment that inherits from 

1278 AbstractMetadata. 

1279 

1280 Returns 

1281 ------- 

1282 AbstractMetadata 

1283 An AbstractMetadata-inheriting object that contains the parameters 

1284 defining the environment. 

1285 

1286 """ 

1287 self.system_id_widget.samplesPerFrameSpinBox.setMaximum( 

1288 self.definition_widget.samples_per_frame_selector.value() 

1289 ) 

1290 self.system_id_widget.samplesPerFrameSpinBox.setValue( 

1291 self.definition_widget.samples_per_frame_selector.value() 

1292 ) 

1293 self.system_id_widget.levelRampTimeDoubleSpinBox.setValue( 

1294 self.definition_widget.ramp_time_spinbox.value() 

1295 ) 

1296 super().initialize_environment() 

1297 for widget in [ 

1298 self.prediction_widget.response_row_selector, 

1299 self.prediction_widget.response_column_selector, 

1300 self.run_widget.control_channel_1_selector, 

1301 self.run_widget.control_channel_2_selector, 

1302 ]: 

1303 widget.blockSignals(True) 

1304 widget.clear() 

1305 for i, control_name in enumerate(self.initialized_control_names): 

1306 widget.addItem(f"{i + 1}: {control_name}") 

1307 widget.blockSignals(False) 

1308 for widget in [ 

1309 self.prediction_widget.excitation_row_selector, 

1310 self.prediction_widget.excitation_column_selector, 

1311 ]: 

1312 widget.blockSignals(True) 

1313 widget.clear() 

1314 for i, drive_name in enumerate(self.initialized_output_names): 

1315 widget.addItem(f"{i + 1}: {drive_name}") 

1316 widget.blockSignals(False) 

1317 # Set up the prediction plots 

1318 self.prediction_widget.excitation_display_plot.getPlotItem().clear() 

1319 self.prediction_widget.response_display_plot.getPlotItem().clear() 

1320 self.prediction_widget.excitation_display_plot.getPlotItem().addLegend() 

1321 self.prediction_widget.response_display_plot.getPlotItem().addLegend() 

1322 self.plot_data_items["response_prediction"] = multiline_plotter( 

1323 np.arange(self.environment_parameters.fft_lines) 

1324 * self.environment_parameters.frequency_spacing, 

1325 np.zeros((4, self.environment_parameters.fft_lines)), 

1326 widget=self.prediction_widget.response_display_plot, 

1327 other_pen_options={"width": 2}, 

1328 names=["Real Prediction", "Real Spec", "Imag Prediction", "Imag Spec"], 

1329 ) 

1330 self.plot_data_items[ 

1331 "prediction_warning_upper" 

1332 ] = self.prediction_widget.response_display_plot.getPlotItem().plot( 

1333 np.array([0, self.data_acquisition_parameters.sample_rate / 2]), 

1334 np.zeros(2), 

1335 pen={ 

1336 "color": PlotWindow.WARNING_COLOR, 

1337 "width": PlotWindow.WARNING_LINEWIDTH, 

1338 "style": PlotWindow.WARNING_LINESTYLE, 

1339 }, 

1340 name="Warning", 

1341 ) 

1342 self.plot_data_items[ 

1343 "prediction_warning_lower" 

1344 ] = self.prediction_widget.response_display_plot.getPlotItem().plot( 

1345 np.array([0, self.data_acquisition_parameters.sample_rate / 2]), 

1346 np.zeros(2), 

1347 pen={ 

1348 "color": PlotWindow.WARNING_COLOR, 

1349 "width": PlotWindow.WARNING_LINEWIDTH, 

1350 "style": PlotWindow.WARNING_LINESTYLE, 

1351 }, 

1352 ) 

1353 self.plot_data_items[ 

1354 "prediction_abort_upper" 

1355 ] = self.prediction_widget.response_display_plot.getPlotItem().plot( 

1356 np.array([0, self.data_acquisition_parameters.sample_rate / 2]), 

1357 np.zeros(2), 

1358 pen={ 

1359 "color": PlotWindow.ABORT_COLOR, 

1360 "width": PlotWindow.ABORT_LINEWIDTH, 

1361 "style": PlotWindow.ABORT_LINESTYLE, 

1362 }, 

1363 name="Abort", 

1364 ) 

1365 self.plot_data_items[ 

1366 "prediction_abort_lower" 

1367 ] = self.prediction_widget.response_display_plot.getPlotItem().plot( 

1368 np.array([0, self.data_acquisition_parameters.sample_rate / 2]), 

1369 np.zeros(2), 

1370 pen={ 

1371 "color": PlotWindow.ABORT_COLOR, 

1372 "width": PlotWindow.ABORT_LINEWIDTH, 

1373 "style": PlotWindow.ABORT_LINESTYLE, 

1374 }, 

1375 ) 

1376 self.plot_data_items["excitation_prediction"] = multiline_plotter( 

1377 np.arange(self.environment_parameters.fft_lines) 

1378 * self.environment_parameters.frequency_spacing, 

1379 np.zeros((2, self.environment_parameters.fft_lines)), 

1380 widget=self.prediction_widget.excitation_display_plot, 

1381 other_pen_options={"width": 1}, 

1382 names=["Real Prediction", "Imag Prediction"], 

1383 ) 

1384 # Create the interactive control law if necessary 

1385 if self.definition_widget.control_function_generator_selector.currentIndex() == 3: 

1386 control_class = getattr( 

1387 self.python_control_module, 

1388 self.definition_widget.control_function_input.itemText( 

1389 self.definition_widget.control_function_input.currentIndex() 

1390 ), 

1391 ) 

1392 self.log(f"Building Interactive UI for class {control_class.__name__}") 

1393 ui_class = control_class.get_ui_class() 

1394 if ui_class == self.interactive_control_law_widget.__class__: 

1395 print("initializing data acquisition and environment parameters") 

1396 self.interactive_control_law_widget.initialize_parameters( 

1397 self.data_acquisition_parameters, self.environment_parameters 

1398 ) 

1399 else: 

1400 if self.interactive_control_law_widget is not None: 

1401 self.interactive_control_law_widget.close() 

1402 self.interactive_control_law_window = QtWidgets.QDialog(self.definition_widget) 

1403 self.interactive_control_law_widget = ui_class( 

1404 self.log_name, 

1405 self.environment_command_queue, 

1406 self.interactive_control_law_window, 

1407 self, 

1408 self.data_acquisition_parameters, 

1409 self.environment_parameters, 

1410 ) 

1411 self.interactive_control_law_window.show() 

1412 return self.environment_parameters 

1413 

1414 # %% Test Predictions 

1415 

1416 def show_max_voltage_prediction(self): 

1417 """Shows the prediction with the largest RMS voltage""" 

1418 widget = self.prediction_widget.excitation_voltage_list 

1419 index = np.argmax([float(widget.item(v).text()) for v in range(widget.count())]) 

1420 self.prediction_widget.excitation_row_selector.setCurrentIndex(index) 

1421 self.prediction_widget.excitation_column_selector.setCurrentIndex(index) 

1422 

1423 def show_min_voltage_prediction(self): 

1424 """Shows the prediction with the smallest RMS voltage""" 

1425 widget = self.prediction_widget.excitation_voltage_list 

1426 index = np.argmin([float(widget.item(v).text()) for v in range(widget.count())]) 

1427 self.prediction_widget.excitation_row_selector.setCurrentIndex(index) 

1428 self.prediction_widget.excitation_column_selector.setCurrentIndex(index) 

1429 

1430 def show_max_error_prediction(self): 

1431 """Shows the prediction with the largest error""" 

1432 widget = self.prediction_widget.response_error_list 

1433 index = np.argmax([float(widget.item(v).text()) for v in range(widget.count())]) 

1434 self.prediction_widget.response_row_selector.setCurrentIndex(index) 

1435 self.prediction_widget.response_column_selector.setCurrentIndex(index) 

1436 

1437 def show_min_error_prediction(self): 

1438 """Shows the prediction with the smallest error""" 

1439 widget = self.prediction_widget.response_error_list 

1440 index = np.argmin([float(widget.item(v).text()) for v in range(widget.count())]) 

1441 self.prediction_widget.response_row_selector.setCurrentIndex(index) 

1442 self.prediction_widget.response_column_selector.setCurrentIndex(index) 

1443 

1444 def update_response_error_prediction_selector(self, item): 

1445 """Updates the selection when an item is double-clicked""" 

1446 index = self.prediction_widget.response_error_list.row(item) 

1447 self.prediction_widget.response_row_selector.setCurrentIndex(index) 

1448 self.prediction_widget.response_column_selector.setCurrentIndex(index) 

1449 

1450 def update_excitation_prediction_selector(self, item): 

1451 """Updates the selection when an item is double-clicked""" 

1452 index = self.prediction_widget.excitation_voltage_list.row(item) 

1453 self.prediction_widget.excitation_row_selector.setCurrentIndex(index) 

1454 self.prediction_widget.excitation_column_selector.setCurrentIndex(index) 

1455 

1456 def update_control_predictions(self): 

1457 """Updates the control prediction with new data""" 

1458 excite_row_index = self.prediction_widget.excitation_row_selector.currentIndex() 

1459 excite_column_index = self.prediction_widget.excitation_column_selector.currentIndex() 

1460 self.plot_data_items["excitation_prediction"][0].setData( 

1461 self.frequencies, 

1462 np.abs(np.real(self.excitation_prediction[:, excite_row_index, excite_column_index])), 

1463 ) 

1464 row_index = self.prediction_widget.response_row_selector.currentIndex() 

1465 column_index = self.prediction_widget.response_column_selector.currentIndex() 

1466 self.plot_data_items["response_prediction"][0].setData( 

1467 self.frequencies, 

1468 np.abs(np.real(self.response_prediction[:, row_index, column_index])), 

1469 ) 

1470 if row_index == column_index: 

1471 warning_upper = abs( 

1472 self.environment_parameters.specification_warning_matrix[1, :, row_index] 

1473 ) 

1474 warning_lower = abs( 

1475 self.environment_parameters.specification_warning_matrix[0, :, row_index] 

1476 ) 

1477 abort_upper = abs( 

1478 self.environment_parameters.specification_abort_matrix[1, :, row_index] 

1479 ) 

1480 abort_lower = abs( 

1481 self.environment_parameters.specification_abort_matrix[0, :, row_index] 

1482 ) 

1483 self.plot_data_items["prediction_warning_upper"].setData( 

1484 self.specification_frequency_lines, warning_upper 

1485 ) 

1486 self.plot_data_items["prediction_warning_lower"].setData( 

1487 self.specification_frequency_lines, warning_lower 

1488 ) 

1489 self.plot_data_items["prediction_abort_upper"].setData( 

1490 self.specification_frequency_lines, abort_upper 

1491 ) 

1492 self.plot_data_items["prediction_abort_lower"].setData( 

1493 self.specification_frequency_lines, abort_lower 

1494 ) 

1495 self.plot_data_items["excitation_prediction"][1].setData( 

1496 self.frequencies, np.zeros(self.frequencies.shape) 

1497 ) 

1498 self.plot_data_items["response_prediction"][2].setData( 

1499 self.frequencies, np.zeros(self.frequencies.shape) 

1500 ) 

1501 self.plot_data_items["response_prediction"][3].setData( 

1502 self.frequencies, np.zeros(self.frequencies.shape) 

1503 ) 

1504 else: 

1505 self.plot_data_items["prediction_warning_upper"].setData( 

1506 np.array([0, self.definition_widget.sample_rate_display.value() / 2]), 

1507 np.zeros(2), 

1508 ) 

1509 self.plot_data_items["prediction_warning_lower"].setData( 

1510 np.array([0, self.definition_widget.sample_rate_display.value() / 2]), 

1511 np.zeros(2), 

1512 ) 

1513 self.plot_data_items["prediction_abort_upper"].setData( 

1514 np.array([0, self.definition_widget.sample_rate_display.value() / 2]), 

1515 np.zeros(2), 

1516 ) 

1517 self.plot_data_items["prediction_abort_lower"].setData( 

1518 np.array([0, self.definition_widget.sample_rate_display.value() / 2]), 

1519 np.zeros(2), 

1520 ) 

1521 self.plot_data_items["excitation_prediction"][1].setData( 

1522 self.frequencies, 

1523 np.abs( 

1524 np.imag(self.excitation_prediction[:, excite_row_index, excite_column_index]) 

1525 ), 

1526 ) 

1527 self.plot_data_items["response_prediction"][2].setData( 

1528 self.frequencies, 

1529 np.abs(np.imag(self.response_prediction[:, row_index, column_index])), 

1530 ) 

1531 self.plot_data_items["response_prediction"][3].setData( 

1532 self.frequencies, 

1533 np.abs( 

1534 np.imag( 

1535 self.environment_parameters.specification_cpsd_matrix[ 

1536 :, row_index, column_index 

1537 ] 

1538 ) 

1539 ), 

1540 ) 

1541 self.plot_data_items["response_prediction"][1].setData( 

1542 self.frequencies, 

1543 np.abs( 

1544 np.real( 

1545 self.environment_parameters.specification_cpsd_matrix[ 

1546 :, row_index, column_index 

1547 ] 

1548 ) 

1549 ), 

1550 ) 

1551 

1552 def recompute_prediction(self): 

1553 """Sends a message to the environment process to recompute the prediction""" 

1554 self.environment_command_queue.put( 

1555 self.log_name, (RandomVibrationCommands.RECOMPUTE_PREDICTION, None) 

1556 ) 

1557 

1558 # %% Run Control 

1559 

1560 def start_control(self): 

1561 """Runs the corresponding environment in the controller""" 

1562 self.enable_control(False) 

1563 self.controller_communication_queue.put( 

1564 self.log_name, (GlobalCommands.START_ENVIRONMENT, self.environment_name) 

1565 ) 

1566 self.environment_command_queue.put( 

1567 self.log_name, 

1568 ( 

1569 RandomVibrationCommands.START_CONTROL, 

1570 db2scale(self.run_widget.current_test_level_selector.value()), 

1571 ), 

1572 ) 

1573 self.run_timer.start(250) 

1574 self.run_start_time = time.time() 

1575 self.run_level_start_time = self.run_start_time 

1576 self.run_widget.test_progress_bar.setValue(0) 

1577 if ( 

1578 self.run_widget.current_test_level_selector.value() 

1579 >= self.run_widget.target_test_level_selector.value() 

1580 ): 

1581 self.controller_communication_queue.put( 

1582 self.log_name, (GlobalCommands.AT_TARGET_LEVEL, self.environment_name) 

1583 ) 

1584 

1585 def stop_control(self): 

1586 """Stops the corresponding environment in the controller""" 

1587 self.run_widget.stop_test_button.setEnabled(False) 

1588 self.environment_command_queue.put( 

1589 self.log_name, (RandomVibrationCommands.STOP_CONTROL, None) 

1590 ) 

1591 self.run_timer.stop() 

1592 

1593 def enable_control(self, enabled): 

1594 """Enables or disables widgets to start or stop control if the control is running or not""" 

1595 for widget in [ 

1596 self.run_widget.test_time_selector, 

1597 self.run_widget.time_test_at_target_level_checkbox, 

1598 self.run_widget.timed_test_radiobutton, 

1599 self.run_widget.continuous_test_radiobutton, 

1600 self.run_widget.target_test_level_selector, 

1601 self.run_widget.start_test_button, 

1602 ]: 

1603 widget.setEnabled(enabled) 

1604 for widget in [self.run_widget.stop_test_button]: 

1605 widget.setEnabled(not enabled) 

1606 if enabled: 

1607 self.run_timer.stop() 

1608 

1609 def update_run_time(self): 

1610 """Updates the time that the control has been running on the GUI""" 

1611 # Update the total run time 

1612 current_time = time.time() 

1613 time_elapsed = current_time - self.run_start_time 

1614 time_at_level_elapsed = current_time - self.run_level_start_time 

1615 self.run_widget.total_test_time_display.setText( 

1616 str(datetime.timedelta(seconds=time_elapsed)).split(".", maxsplit=1)[0] 

1617 ) 

1618 self.run_widget.time_at_level_display.setText( 

1619 str(datetime.timedelta(seconds=time_at_level_elapsed)).split(".", maxsplit=1)[0] 

1620 ) 

1621 # Check if we need to stop the test due to timeout 

1622 if self.run_widget.timed_test_radiobutton.isChecked(): 

1623 check_time = self.run_widget.test_time_selector.time() 

1624 check_time_seconds = ( 

1625 check_time.hour() * 3600 + check_time.minute() * 60 + check_time.second() 

1626 ) 

1627 if self.run_widget.time_test_at_target_level_checkbox.isChecked(): 

1628 if ( 

1629 self.run_widget.current_test_level_selector.value() 

1630 >= self.run_widget.target_test_level_selector.value() 

1631 ): 

1632 self.run_widget.test_progress_bar.setValue( 

1633 int(time_at_level_elapsed / check_time_seconds * 100) 

1634 ) 

1635 if time_at_level_elapsed > check_time_seconds: 

1636 self.run_widget.test_progress_bar.setValue(100) 

1637 self.stop_control() 

1638 else: 

1639 self.run_widget.test_progress_bar.setValue(0) 

1640 else: 

1641 self.run_widget.test_progress_bar.setValue( 

1642 int(time_elapsed / check_time_seconds * 100) 

1643 ) 

1644 if time_elapsed > check_time_seconds: 

1645 self.stop_control() 

1646 

1647 def change_control_test_level(self): 

1648 """Updates the test level of the control.""" 

1649 self.environment_command_queue.put( 

1650 self.log_name, 

1651 ( 

1652 RandomVibrationCommands.ADJUST_TEST_LEVEL, 

1653 db2scale(self.run_widget.current_test_level_selector.value()), 

1654 ), 

1655 ) 

1656 self.run_level_start_time = time.time() 

1657 # Check and see if we need to start streaming data 

1658 if ( 

1659 self.run_widget.current_test_level_selector.value() 

1660 >= self.run_widget.target_test_level_selector.value() 

1661 ): 

1662 self.controller_communication_queue.put( 

1663 self.log_name, (GlobalCommands.AT_TARGET_LEVEL, self.environment_name) 

1664 ) 

1665 

1666 def change_test_level_from_profile(self, test_level): 

1667 """Sets the test level from a profile instruction 

1668 

1669 Parameters 

1670 ---------- 

1671 test_level : 

1672 Value to set the test level to. 

1673 """ 

1674 self.run_widget.current_test_level_selector.setValue(float(test_level)) 

1675 

1676 def change_specification_from_profile(self, new_specification_file): 

1677 """ 

1678 Loads in a new specification and starts controlling to it 

1679 

1680 Parameters 

1681 ---------- 

1682 new_specification_file : str 

1683 File path to a new specification file 

1684 

1685 """ 

1686 self.select_spec_file(None, new_specification_file) 

1687 environment_parameters = self.initialize_environment() 

1688 self.environment_command_queue.put( 

1689 self.log_name, 

1690 (GlobalCommands.INITIALIZE_ENVIRONMENT_PARAMETERS, environment_parameters), 

1691 ) 

1692 

1693 def show_magnitude_window(self, item): 

1694 """Creates a window showing the magnitude of a signal when an item is double-clicked""" 

1695 index = self.run_widget.test_response_error_list.row(item) 

1696 self.create_window(None, index, index, 0) 

1697 

1698 def create_window( 

1699 self, event, row_index=None, column_index=None, datatype_index=None 

1700 ): # pylint: disable=unused-argument 

1701 """Creates a subwindow to show a specific channel information 

1702 

1703 Parameters 

1704 ---------- 

1705 event : 

1706 

1707 row_index : 

1708 Row index in the CPSD matrix to display (Default value = None) 

1709 column_index : 

1710 Column index in the CPSD matrix to display (Default value = None) 

1711 datatype_index : 

1712 Data type to display (real,imag,mag,phase,etc) (Default value = None) 

1713 

1714 """ 

1715 if row_index is None: 

1716 row_index = self.run_widget.control_channel_1_selector.currentIndex() 

1717 if column_index is None: 

1718 column_index = self.run_widget.control_channel_2_selector.currentIndex() 

1719 if datatype_index is None: 

1720 datatype_index = self.run_widget.data_type_selector.currentIndex() 

1721 self.plot_windows.append( 

1722 PlotWindow( 

1723 None, 

1724 row_index, 

1725 column_index, 

1726 datatype_index, 

1727 (self.specification_frequency_lines, self.specification_cpsd_matrix), 

1728 self.run_widget.control_channel_1_selector.itemText(row_index), 

1729 self.run_widget.control_channel_2_selector.itemText(column_index), 

1730 self.run_widget.data_type_selector.itemText(datatype_index), 

1731 ( 

1732 self.specification_warning_matrix 

1733 if row_index == column_index and datatype_index == 0 

1734 else None 

1735 ), 

1736 ( 

1737 self.specification_abort_matrix 

1738 if row_index == column_index and datatype_index == 0 

1739 else None 

1740 ), 

1741 ) 

1742 ) 

1743 

1744 def show_all_asds(self): 

1745 """Creates a subwindow for each ASD in the CPSD matrix""" 

1746 for i in range(self.specification_cpsd_matrix.shape[-1]): 

1747 self.create_window(None, i, i, 0) 

1748 self.tile_windows() 

1749 

1750 def show_all_csds_phscoh(self): 

1751 """Creates a subwindow for each entry in the CPSD matrix showing phase and coherence""" 

1752 for i in range(self.specification_cpsd_matrix.shape[-1]): 

1753 for j in range(self.specification_cpsd_matrix.shape[-1]): 

1754 if i == j: 

1755 datatype_index = 0 

1756 elif i < j: 

1757 datatype_index = 1 

1758 elif i > j: 

1759 datatype_index = 2 

1760 else: 

1761 raise ValueError("Invalid situation. How did you get here?!") 

1762 self.create_window(None, i, j, datatype_index) 

1763 self.tile_windows() 

1764 

1765 def show_all_csds_realimag(self): 

1766 """Creates a subwindow for each entry in the CPSD matrix showing real and imaginary""" 

1767 for i in range(self.specification_cpsd_matrix.shape[-1]): 

1768 for j in range(self.specification_cpsd_matrix.shape[-1]): 

1769 if i == j: 

1770 datatype_index = 0 

1771 elif i < j: 

1772 datatype_index = 3 

1773 elif i > j: 

1774 datatype_index = 4 

1775 else: 

1776 raise ValueError("Invalid situation. How did you get here?!") 

1777 self.create_window(None, i, j, datatype_index) 

1778 self.tile_windows() 

1779 

1780 def tile_windows(self): 

1781 """Tile subwindow equally across the screen""" 

1782 screen_rect = QtWidgets.QApplication.desktop().screenGeometry() 

1783 # Go through and remove any closed windows 

1784 self.plot_windows = [window for window in self.plot_windows if window.isVisible()] 

1785 num_windows = len(self.plot_windows) 

1786 ncols = int(np.ceil(np.sqrt(num_windows))) 

1787 nrows = int(np.ceil(num_windows / ncols)) 

1788 window_width = int(screen_rect.width() / ncols) 

1789 window_height = int(screen_rect.height() / nrows) 

1790 for index, window in enumerate(self.plot_windows): 

1791 window.resize(window_width, window_height) 

1792 row_ind = index // ncols 

1793 col_ind = index % ncols 

1794 window.move(col_ind * window_width, row_ind * window_height) 

1795 

1796 def close_windows(self): 

1797 """Close all subwindows""" 

1798 for window in self.plot_windows: 

1799 window.close() 

1800 

1801 def save_control_data_from_profile(self, filename): 

1802 """Saves the control data to a file when requested by a profile argument""" 

1803 self.save_spectral_data(None, filename) 

1804 

1805 def save_spectral_data(self, clicked, filename=None): # pylint: disable=unused-argument 

1806 """Save Spectral Data from the Controller""" 

1807 if filename is None: 

1808 filename, _ = QtWidgets.QFileDialog.getSaveFileName( 

1809 self.definition_widget, 

1810 "Select File to Save Spectral Data", 

1811 filter="NetCDF File (*.nc4)", 

1812 ) 

1813 if filename == "": 

1814 return 

1815 labels = [ 

1816 ["node_number", str], 

1817 ["node_direction", str], 

1818 ["comment", str], 

1819 ["serial_number", str], 

1820 ["triax_dof", str], 

1821 ["sensitivity", str], 

1822 ["unit", str], 

1823 ["make", str], 

1824 ["model", str], 

1825 ["expiration", str], 

1826 ["physical_device", str], 

1827 ["physical_channel", str], 

1828 ["channel_type", str], 

1829 ["minimum_value", str], 

1830 ["maximum_value", str], 

1831 ["coupling", str], 

1832 ["excitation_source", str], 

1833 ["excitation", str], 

1834 ["feedback_device", str], 

1835 ["feedback_channel", str], 

1836 ["warning_level", str], 

1837 ["abort_level", str], 

1838 ] 

1839 global_data_parameters: DataAcquisitionParameters 

1840 global_data_parameters = self.data_acquisition_parameters 

1841 netcdf_handle = nc4.Dataset( # pylint: disable=no-member 

1842 filename, "w", format="NETCDF4", clobber=True 

1843 ) 

1844 # Create dimensions 

1845 netcdf_handle.createDimension("response_channels", len(global_data_parameters.channel_list)) 

1846 netcdf_handle.createDimension( 

1847 "output_channels", 

1848 len( 

1849 [ 

1850 channel 

1851 for channel in global_data_parameters.channel_list 

1852 if channel.feedback_device is not None 

1853 ] 

1854 ), 

1855 ) 

1856 netcdf_handle.createDimension("time_samples", None) 

1857 netcdf_handle.createDimension( 

1858 "num_environments", len(global_data_parameters.environment_names) 

1859 ) 

1860 # Create attributes 

1861 netcdf_handle.file_version = "3.0.0" 

1862 netcdf_handle.sample_rate = global_data_parameters.sample_rate 

1863 netcdf_handle.time_per_write = ( 

1864 global_data_parameters.samples_per_write / global_data_parameters.output_sample_rate 

1865 ) 

1866 netcdf_handle.time_per_read = ( 

1867 global_data_parameters.samples_per_read / global_data_parameters.sample_rate 

1868 ) 

1869 netcdf_handle.hardware = global_data_parameters.hardware 

1870 netcdf_handle.hardware_file = ( 

1871 "None" 

1872 if global_data_parameters.hardware_file is None 

1873 else global_data_parameters.hardware_file 

1874 ) 

1875 netcdf_handle.output_oversample = global_data_parameters.output_oversample 

1876 for key, value in global_data_parameters.extra_parameters.items(): 

1877 setattr(netcdf_handle, key, value) 

1878 # Create Variables 

1879 var = netcdf_handle.createVariable("environment_names", str, ("num_environments",)) 

1880 this_environment_index = None 

1881 for i, name in enumerate(global_data_parameters.environment_names): 

1882 var[i] = name 

1883 if name == self.environment_name: 

1884 this_environment_index = i 

1885 var = netcdf_handle.createVariable( 

1886 "environment_active_channels", 

1887 "i1", 

1888 ("response_channels", "num_environments"), 

1889 ) 

1890 var[...] = global_data_parameters.environment_active_channels.astype("int8")[ 

1891 global_data_parameters.environment_active_channels[:, this_environment_index], 

1892 :, 

1893 ] 

1894 # Create channel table variables 

1895 for label, netcdf_datatype in labels: 

1896 var = netcdf_handle.createVariable( 

1897 "/channels/" + label, netcdf_datatype, ("response_channels",) 

1898 ) 

1899 channel_data = [ 

1900 getattr(channel, label) for channel in global_data_parameters.channel_list 

1901 ] 

1902 if netcdf_datatype == "i1": 

1903 channel_data = np.array([1 if val else 0 for val in channel_data]) 

1904 else: 

1905 channel_data = ["" if val is None else val for val in channel_data] 

1906 for i, cd in enumerate(channel_data): 

1907 var[i] = cd 

1908 group_handle = netcdf_handle.createGroup(self.environment_name) 

1909 self.environment_parameters.store_to_netcdf(group_handle) 

1910 # Create Variables for Spectral Data 

1911 group_handle.createDimension("drive_channels", self.last_transfer_function.shape[2]) 

1912 var = group_handle.createVariable( 

1913 "frf_data_real", 

1914 "f8", 

1915 ("fft_lines", "specification_channels", "drive_channels"), 

1916 ) 

1917 var[...] = self.last_transfer_function.real 

1918 var = group_handle.createVariable( 

1919 "frf_data_imag", 

1920 "f8", 

1921 ("fft_lines", "specification_channels", "drive_channels"), 

1922 ) 

1923 var[...] = self.last_transfer_function.imag 

1924 var = group_handle.createVariable( 

1925 "frf_coherence", "f8", ("fft_lines", "specification_channels") 

1926 ) 

1927 var[...] = self.last_coherence.real 

1928 var = group_handle.createVariable( 

1929 "response_cpsd_real", 

1930 "f8", 

1931 ("fft_lines", "specification_channels", "specification_channels"), 

1932 ) 

1933 var[...] = self.last_response_cpsd.real 

1934 var = group_handle.createVariable( 

1935 "response_cpsd_imag", 

1936 "f8", 

1937 ("fft_lines", "specification_channels", "specification_channels"), 

1938 ) 

1939 var[...] = self.last_response_cpsd.imag 

1940 var = group_handle.createVariable( 

1941 "drive_cpsd_real", "f8", ("fft_lines", "drive_channels", "drive_channels") 

1942 ) 

1943 var[...] = self.last_reference_cpsd.real 

1944 var = group_handle.createVariable( 

1945 "drive_cpsd_imag", "f8", ("fft_lines", "drive_channels", "drive_channels") 

1946 ) 

1947 var[...] = self.last_reference_cpsd.imag 

1948 var = group_handle.createVariable( 

1949 "response_noise_cpsd_real", 

1950 "f8", 

1951 ("fft_lines", "specification_channels", "specification_channels"), 

1952 ) 

1953 var[...] = self.last_response_noise.real 

1954 var = group_handle.createVariable( 

1955 "response_noise_cpsd_imag", 

1956 "f8", 

1957 ("fft_lines", "specification_channels", "specification_channels"), 

1958 ) 

1959 var[...] = self.last_response_noise.imag 

1960 var = group_handle.createVariable( 

1961 "drive_noise_cpsd_real", 

1962 "f8", 

1963 ("fft_lines", "drive_channels", "drive_channels"), 

1964 ) 

1965 var[...] = self.last_reference_noise.real 

1966 var = group_handle.createVariable( 

1967 "drive_noise_cpsd_imag", 

1968 "f8", 

1969 ("fft_lines", "drive_channels", "drive_channels"), 

1970 ) 

1971 var[...] = self.last_reference_noise.imag 

1972 netcdf_handle.close() 

1973 

1974 # %% Miscellaneous 

1975 

1976 def retrieve_metadata( 

1977 self, 

1978 netcdf_handle: nc4._netCDF4.Dataset = None, # pylint: disable=c-extension-no-member 

1979 environment_name: str = None, 

1980 ): 

1981 """Collects environment parameters from a netCDF dataset. 

1982 

1983 This function retrieves parameters from a netCDF dataset that was written 

1984 by the controller during streaming. It must populate the widgets 

1985 in the user interface with the proper information. 

1986 

1987 This function is the "read" counterpart to the store_to_netcdf 

1988 function in the AbstractMetadata class, which will write parameters to 

1989 the netCDF file to document the metadata. 

1990 

1991 Note that the entire dataset is passed to this function, so the function 

1992 should collect parameters pertaining to the environment from a Group 

1993 in the dataset sharing the environment's name, e.g. 

1994 

1995 ``group = netcdf_handle.groups[self.environment_name]`` 

1996 ``self.definition_widget.parameter_selector.setValue(group.parameter)`` 

1997 

1998 Parameters 

1999 ---------- 

2000 netcdf_handle : nc4._netCDF4.Dataset 

2001 The netCDF dataset from which the data will be read. It should have 

2002 a group name with the enviroment's name. 

2003 environment_name : str (optional) 

2004 name of environment from which to retrieve metadata. Only needed if 

2005 different from current environment. 

2006 

2007 """ 

2008 group = super().retrieve_metadata(netcdf_handle, environment_name) 

2009 

2010 # Control channels 

2011 try: 

2012 for i in group.variables["control_channel_indices"][...]: 

2013 item = self.definition_widget.control_channels_selector.item(i) 

2014 item.setCheckState(Qt.Checked) 

2015 except KeyError: 

2016 print("no variable control_channel_indices, please select control channels manually") 

2017 # Other data 

2018 try: 

2019 self.response_transformation_matrix = group.variables["response_transformation_matrix"][ 

2020 ... 

2021 ].data 

2022 except KeyError: 

2023 self.response_transformation_matrix = None 

2024 try: 

2025 self.output_transformation_matrix = group.variables["reference_transformation_matrix"][ 

2026 ... 

2027 ].data 

2028 except KeyError: 

2029 self.output_transformation_matrix = None 

2030 self.define_transformation_matrices(None, dialog=False) 

2031 

2032 # environment_name is passed when the saved environment doesn't match the 

2033 # current environment 

2034 if environment_name is None: 

2035 # Spinboxes 

2036 self.definition_widget.samples_per_frame_selector.setValue(group.samples_per_frame) 

2037 self.definition_widget.ramp_time_spinbox.setValue(group.test_level_ramp_time) 

2038 self.definition_widget.cola_overlap_percentage_selector.setValue( 

2039 group.cola_overlap * 100 

2040 ) 

2041 self.definition_widget.cola_exponent_selector.setValue(group.cola_window_exponent) 

2042 self.definition_widget.cpsd_overlap_selector.setValue(group.cpsd_overlap * 100) 

2043 self.definition_widget.cpsd_frames_selector.setValue(group.frames_in_cpsd) 

2044 # Checkboxes 

2045 self.definition_widget.update_transfer_function_during_control_selector.setChecked( 

2046 bool(group.update_tf_during_control) 

2047 ) 

2048 self.definition_widget.auto_abort_checkbox.setChecked( 

2049 bool(group.allow_automatic_aborts) 

2050 ) 

2051 # Comboboxes 

2052 self.definition_widget.cola_window_selector.setCurrentIndex( 

2053 self.definition_widget.cola_window_selector.findText(group.cola_window) 

2054 ) 

2055 self.definition_widget.cpsd_computation_window_selector.setCurrentIndex( 

2056 self.definition_widget.cpsd_computation_window_selector.findText(group.cpsd_window) 

2057 ) 

2058 # Specification 

2059 self.specification_frequency_lines = group.variables["specification_frequency_lines"][ 

2060 ... 

2061 ].data 

2062 self.specification_cpsd_matrix = ( 

2063 group.variables["specification_cpsd_matrix_real"][...].data 

2064 + 1j * group.variables["specification_cpsd_matrix_imag"][...].data 

2065 ) 

2066 self.specification_warning_matrix = group.variables["specification_warning_matrix"][ 

2067 ... 

2068 ].data 

2069 self.specification_abort_matrix = group.variables["specification_abort_matrix"][ 

2070 ... 

2071 ].data 

2072 self.select_python_module(None, group.control_python_script) 

2073 index = self.definition_widget.control_function_input.findText( 

2074 group.control_python_function 

2075 ) 

2076 if ( 

2077 index == -1 

2078 ): # error handling (older revisions of rattlesnake may be missing newer control laws) 

2079 index = 0 

2080 default = self.definition_widget.control_function_input.itemText(index) 

2081 print( 

2082 f'Warning: control function "{group.control_python_function}" not found, ' 

2083 f'defaulting to "{default}"' 

2084 ) 

2085 self.definition_widget.control_function_input.setCurrentIndex(index) 

2086 self.definition_widget.control_parameters_text_input.setText( 

2087 group.control_python_function_parameters 

2088 ) 

2089 self.show_specification() 

2090 

2091 def update_gui(self, queue_data: tuple): 

2092 """Update the environment's graphical user interface 

2093 

2094 This function will receive data from the gui_update_queue that 

2095 specifies how the user interface should be updated. Data will usually 

2096 be received as ``(instruction,data)`` pairs, where the ``instruction`` notes 

2097 what operation should be taken or which widget should be modified, and 

2098 the ``data`` notes what data should be used in the update. 

2099 

2100 Parameters 

2101 ---------- 

2102 queue_data : tuple 

2103 A tuple containing ``(instruction,data)`` pairs where ``instruction`` 

2104 defines and operation or widget to be modified and ``data`` contains 

2105 the data used to perform the operation. 

2106 """ 

2107 if super().update_gui(queue_data): 

2108 return 

2109 message, data = queue_data 

2110 if message == "control_predictions": 

2111 ( 

2112 _, 

2113 self.excitation_prediction, 

2114 self.response_prediction, 

2115 _, 

2116 rms_voltage_prediction, 

2117 rms_db_error_prediction, 

2118 ) = data 

2119 self.update_control_predictions() 

2120 for widget, widget_data in zip( 

2121 [ 

2122 self.prediction_widget.excitation_voltage_list, 

2123 self.prediction_widget.response_error_list, 

2124 ], 

2125 [rms_voltage_prediction, rms_db_error_prediction], 

2126 ): 

2127 widget.clear() 

2128 widget.addItems([f"{d:.3f}" for d in widget_data]) 

2129 # Now compute if any channels are erroring or not 

2130 with np.errstate(invalid="ignore"): 

2131 lines_out = ( 

2132 self.environment_parameters.percent_lines_out / 100 

2133 ) * self.environment_parameters.fft_lines 

2134 for i in range(self.prediction_widget.response_error_list.count()): 

2135 item = self.prediction_widget.response_error_list.item(i) 

2136 if ( 

2137 sum( 

2138 self.response_prediction[:, i, i] 

2139 > self.environment_parameters.specification_abort_matrix[1, :, i] 

2140 ) 

2141 > lines_out 

2142 ): 

2143 item.setBackground(QColor(255, 125, 125)) 

2144 elif ( 

2145 sum( 

2146 self.response_prediction[:, i, i] 

2147 < self.environment_parameters.specification_abort_matrix[0, :, i] 

2148 ) 

2149 > lines_out 

2150 ): 

2151 item.setBackground(QColor(255, 125, 125)) 

2152 elif ( 

2153 sum( 

2154 self.response_prediction[:, i, i] 

2155 > self.environment_parameters.specification_warning_matrix[1, :, i] 

2156 ) 

2157 > lines_out 

2158 ): 

2159 item.setBackground(QColor(255, 255, 125)) 

2160 elif ( 

2161 sum( 

2162 self.response_prediction[:, i, i] 

2163 < self.environment_parameters.specification_warning_matrix[0, :, i] 

2164 ) 

2165 > lines_out 

2166 ): 

2167 item.setBackground(QColor(255, 255, 125)) 

2168 else: 

2169 item.setBackground(QColor(255, 255, 255)) 

2170 elif message == "control_update": 

2171 ( 

2172 frames, 

2173 total_frames, 

2174 self.frequencies, 

2175 self.last_transfer_function, 

2176 self.last_coherence, 

2177 self.last_response_cpsd, 

2178 self.last_reference_cpsd, 

2179 self.last_condition, 

2180 ) = data 

2181 self.update_sysid_plots( 

2182 update_time=False, update_transfer_function=True, update_noise=True 

2183 ) 

2184 self.system_id_widget.current_frames_spinbox.setValue(frames) 

2185 self.system_id_widget.total_frames_spinbox.setValue(total_frames) 

2186 self.system_id_widget.progressBar.setValue(int(frames / total_frames * 100)) 

2187 self.plot_data_items["sum_asds_control"].setData( 

2188 self.frequencies, np.einsum("ijj", self.last_response_cpsd).real 

2189 ) 

2190 # Go through and remove any closed windows 

2191 self.plot_windows = [window for window in self.plot_windows if window.isVisible()] 

2192 for window in self.plot_windows: 

2193 window.update_plot(self.last_response_cpsd) 

2194 elif message == "interactive_control_sysid_update": 

2195 if self.interactive_control_law_widget is not None: 

2196 self.interactive_control_law_widget.update_ui_sysid(*data) 

2197 elif message == "interactive_control_update": 

2198 if self.interactive_control_law_widget is not None: 

2199 self.interactive_control_law_widget.update_ui_control(data) 

2200 elif message == "update_test_response_error_list": 

2201 rms_db_error, warning_channels, abort_channels = data 

2202 self.run_widget.test_response_error_list.clear() 

2203 self.run_widget.test_response_error_list.addItems([f"{d:.3f}" for d in rms_db_error]) 

2204 for index in warning_channels: 

2205 item = self.run_widget.test_response_error_list.item(index) 

2206 item.setBackground(QColor(255, 255, 125)) 

2207 for index in abort_channels: 

2208 item = self.run_widget.test_response_error_list.item(index) 

2209 item.setBackground(QColor(255, 125, 125)) 

2210 elif message == "enable_control": 

2211 self.enable_control(True) 

2212 elif message == "enable": 

2213 widget = None 

2214 for parent in [ 

2215 self.definition_widget, 

2216 self.system_id_widget, 

2217 self.prediction_widget, 

2218 self.run_widget, 

2219 ]: 

2220 try: 

2221 widget = getattr(parent, data) 

2222 break 

2223 except AttributeError: 

2224 continue 

2225 if widget is None: 

2226 raise ValueError(f"Cannot Enable Widget {data}: not found in UI") 

2227 widget.setEnabled(True) 

2228 elif message == "disable": 

2229 widget = None 

2230 for parent in [ 

2231 self.definition_widget, 

2232 self.system_id_widget, 

2233 self.prediction_widget, 

2234 self.run_widget, 

2235 ]: 

2236 try: 

2237 widget = getattr(parent, data) 

2238 break 

2239 except AttributeError: 

2240 continue 

2241 if widget is None: 

2242 raise ValueError(f"Cannot Disable Widget {data}: not found in UI") 

2243 widget.setEnabled(False) 

2244 else: 

2245 widget = None 

2246 for parent in [ 

2247 self.definition_widget, 

2248 self.system_id_widget, 

2249 self.prediction_widget, 

2250 self.run_widget, 

2251 ]: 

2252 try: 

2253 widget = getattr(parent, message) 

2254 break 

2255 except AttributeError: 

2256 continue 

2257 if widget is None: 

2258 raise ValueError(f"Cannot Update Widget {message}: not found in UI") 

2259 if isinstance(widget, QtWidgets.QDoubleSpinBox): 

2260 widget.setValue(data) 

2261 elif isinstance(widget, QtWidgets.QSpinBox): 

2262 widget.setValue(data) 

2263 elif isinstance(widget, QtWidgets.QLineEdit): 

2264 widget.setText(data) 

2265 elif isinstance(widget, QtWidgets.QListWidget): 

2266 widget.clear() 

2267 widget.addItems([f"{d:.3f}" for d in data]) 

2268 

2269 @staticmethod 

2270 def create_environment_template( 

2271 environment_name: str, workbook: openpyxl.workbook.workbook.Workbook 

2272 ): 

2273 """Creates a template worksheet in an Excel workbook defining the 

2274 environment. 

2275 

2276 This function creates a template worksheet in an Excel workbook that 

2277 when filled out could be read by the controller to re-create the 

2278 environment. 

2279 

2280 This function is the "write" counterpart to the 

2281 ``set_parameters_from_template`` function in the ``RandomVibrationUI`` class, 

2282 which reads the values from the template file to populate the user 

2283 interface. 

2284 

2285 Parameters 

2286 ---------- 

2287 environment_name : str : 

2288 The name of the environment that will specify the worksheet's name 

2289 workbook : openpyxl.workbook.workbook.Workbook : 

2290 A reference to an ``openpyxl`` workbook. 

2291 

2292 """ 

2293 worksheet = workbook.create_sheet(environment_name) 

2294 worksheet.cell(1, 1, "Control Type") 

2295 worksheet.cell(1, 2, "Random") 

2296 worksheet.cell(2, 1, "Samples Per Frame:") 

2297 worksheet.cell(2, 2, "# Number of Samples per Measurement Frame") 

2298 worksheet.cell(3, 1, "Test Level Ramp Time:") 

2299 worksheet.cell(3, 2, "# Time taken to Ramp between test levels") 

2300 worksheet.cell(4, 1, "COLA Window:") 

2301 worksheet.cell(4, 2, "# Window used for Constant Overlap and Add process") 

2302 worksheet.cell(5, 1, "COLA Overlap %:") 

2303 worksheet.cell(5, 2, "# Overlap used in Constant Overlap and Add process") 

2304 worksheet.cell(6, 1, "COLA Window Exponent:") 

2305 worksheet.cell( 

2306 6, 

2307 2, 

2308 "# Exponent Applied to the COLA Window (use 0.5 unless you " 

2309 "are sure you don't want to!)", 

2310 ) 

2311 worksheet.cell(7, 1, "Update System ID During Control:") 

2312 worksheet.cell( 

2313 7, 

2314 2, 

2315 "# Continue updating transfer function while the controller is controlling (Y/N)", 

2316 ) 

2317 worksheet.cell(8, 1, "Frames in CPSD:") 

2318 worksheet.cell(8, 2, "# Frames used to compute the CPSD matrix") 

2319 worksheet.cell(9, 1, "CPSD Window:") 

2320 worksheet.cell(9, 2, "# Window used to compute the CPSD matrix") 

2321 worksheet.cell(10, 1, "CPSD Overlap %:") 

2322 worksheet.cell(10, 2, "# Overlap percentage for CPSD calculations") 

2323 worksheet.cell(11, 1, "Allow Automatic Aborts") 

2324 worksheet.cell(12, 1, "Control Python Script:") 

2325 worksheet.cell(12, 2, "# Path to the Python script containing the control law") 

2326 worksheet.cell(13, 1, "Control Python Function:") 

2327 worksheet.cell( 

2328 13, 

2329 2, 

2330 "# Function or class name within the Python Script that will serve as the control law", 

2331 ) 

2332 worksheet.cell(14, 1, "Control Parameters:") 

2333 worksheet.cell(14, 2, "# Extra parameters used in the control law") 

2334 worksheet.cell(15, 1, "Control Channels (1-based):") 

2335 worksheet.cell(16, 1, "System ID Averaging:") 

2336 worksheet.cell( 

2337 16, 

2338 2, 

2339 "# Averaging Type used for system ID. Should be Linear or Exponential", 

2340 ) 

2341 worksheet.cell(17, 1, "Noise Averages:") 

2342 worksheet.cell(17, 2, "# Number of Averages used when characterizing noise") 

2343 worksheet.cell(18, 1, "System ID Averages:") 

2344 worksheet.cell(18, 2, "# Number of Averages used when computing the FRF") 

2345 worksheet.cell(19, 1, "Exponential Averaging Coefficient:") 

2346 worksheet.cell(19, 2, "# Averaging Coefficient for Exponential Averaging (if used)") 

2347 worksheet.cell(20, 1, "System ID Estimator:") 

2348 worksheet.cell( 

2349 20, 

2350 2, 

2351 "# Technique used to compute system ID. Should be one of H1, H2, H3, or Hv.", 

2352 ) 

2353 worksheet.cell(21, 1, "System ID Level (V RMS):") 

2354 worksheet.cell( 

2355 21, 

2356 2, 

2357 "# RMS Value of Flat Voltage Spectrum used for System Identification.", 

2358 ) 

2359 worksheet.cell(22, 1, "System ID Signal Type:") 

2360 worksheet.cell(23, 1, "System ID Window:") 

2361 worksheet.cell( 

2362 23, 

2363 2, 

2364 "# Window used to compute FRFs during system ID. Should be one of Hann or None", 

2365 ) 

2366 worksheet.cell(24, 1, "System ID Overlap %:") 

2367 worksheet.cell(24, 2, "# Overlap to use in the system identification") 

2368 worksheet.cell(25, 1, "System ID Burst On %:") 

2369 worksheet.cell(25, 2, "# Percentage of a frame that the burst random is on for") 

2370 worksheet.cell(26, 1, "System ID Burst Pretrigger %:") 

2371 worksheet.cell( 

2372 26, 

2373 2, 

2374 "# Percentage of a frame that occurs before the burst starts in a burst random signal", 

2375 ) 

2376 worksheet.cell(27, 1, "System ID Ramp Fraction %:") 

2377 worksheet.cell( 

2378 27, 

2379 2, 

2380 '# Percentage of the "System ID Burst On %" that will be used to ramp up to full level', 

2381 ) 

2382 worksheet.cell(28, 1, "Specification File:") 

2383 worksheet.cell(28, 2, "# Path to the file containing the Specification") 

2384 worksheet.cell(29, 1, "Response Transformation Matrix:") 

2385 worksheet.cell( 

2386 29, 

2387 2, 

2388 "# Transformation matrix to apply to the response channels. Type None if there " 

2389 "is none. Otherwise, make this a 2D array in the spreadsheet and move the Output " 

2390 "Transformation Matrix line down so it will fit. The number of columns should be the " 

2391 "number of physical control channels.", 

2392 ) 

2393 worksheet.cell(30, 1, "Output Transformation Matrix:") 

2394 worksheet.cell( 

2395 30, 

2396 2, 

2397 "# Transformation matrix to apply to the outputs. Type None if there is none. " 

2398 "Otherwise, make this a 2D array in the spreadsheet. The number of columns should be " 

2399 "the number of physical output channels in the environment.", 

2400 ) 

2401 

2402 def set_parameters_from_template(self, worksheet: openpyxl.worksheet.worksheet.Worksheet): 

2403 """ 

2404 Collects parameters for the user interface from the Excel template file 

2405 

2406 This function reads a filled out template worksheet to create an 

2407 environment. Cells on this worksheet contain parameters needed to 

2408 specify the environment, so this function should read those cells and 

2409 update the UI widgets with those parameters. 

2410 

2411 This function is the "read" counterpart to the 

2412 ``create_environment_template`` function in the ``RandomVibrationUI`` class, 

2413 which writes a template file that can be filled out by a user. 

2414 

2415 

2416 Parameters 

2417 ---------- 

2418 worksheet : openpyxl.worksheet.worksheet.Worksheet 

2419 An openpyxl worksheet that contains the environment template. 

2420 Cells on this worksheet should contain the parameters needed for the 

2421 user interface. 

2422 

2423 """ 

2424 self.definition_widget.samples_per_frame_selector.setValue(int(worksheet.cell(2, 2).value)) 

2425 self.definition_widget.ramp_time_spinbox.setValue(float(worksheet.cell(3, 2).value)) 

2426 self.definition_widget.cola_window_selector.setCurrentIndex( 

2427 self.definition_widget.cola_window_selector.findText(worksheet.cell(4, 2).value) 

2428 ) 

2429 self.definition_widget.cola_overlap_percentage_selector.setValue( 

2430 float(worksheet.cell(5, 2).value) 

2431 ) 

2432 self.definition_widget.cola_exponent_selector.setValue(float(worksheet.cell(6, 2).value)) 

2433 self.definition_widget.update_transfer_function_during_control_selector.setChecked( 

2434 worksheet.cell(7, 2).value.upper() == "Y" 

2435 ) 

2436 self.definition_widget.cpsd_frames_selector.setValue(int(worksheet.cell(8, 2).value)) 

2437 self.definition_widget.cpsd_computation_window_selector.setCurrentIndex( 

2438 self.definition_widget.cpsd_computation_window_selector.findText( 

2439 worksheet.cell(9, 2).value 

2440 ) 

2441 ) 

2442 self.definition_widget.cpsd_overlap_selector.setValue(float(worksheet.cell(10, 2).value)) 

2443 self.definition_widget.auto_abort_checkbox.setChecked( 

2444 worksheet.cell(11, 2).value.upper() == "Y" 

2445 ) 

2446 self.select_python_module(None, worksheet.cell(12, 2).value) 

2447 self.definition_widget.control_function_input.setCurrentIndex( 

2448 self.definition_widget.control_function_input.findText(worksheet.cell(13, 2).value) 

2449 ) 

2450 self.definition_widget.control_parameters_text_input.setText( 

2451 "" if worksheet.cell(14, 2).value is None else str(worksheet.cell(14, 2).value) 

2452 ) 

2453 column_index = 2 

2454 while True: 

2455 value = worksheet.cell(15, column_index).value 

2456 if value is None or (isinstance(value, str) and value.strip() == ""): 

2457 break 

2458 item = self.definition_widget.control_channels_selector.item(int(value) - 1) 

2459 item.setCheckState(Qt.Checked) 

2460 column_index += 1 

2461 self.system_id_widget.averagingTypeComboBox.setCurrentIndex( 

2462 self.system_id_widget.averagingTypeComboBox.findText(worksheet.cell(16, 2).value) 

2463 ) 

2464 self.system_id_widget.noiseAveragesSpinBox.setValue(int(worksheet.cell(17, 2).value)) 

2465 self.system_id_widget.systemIDAveragesSpinBox.setValue(int(worksheet.cell(18, 2).value)) 

2466 self.system_id_widget.averagingCoefficientDoubleSpinBox.setValue( 

2467 float(worksheet.cell(19, 2).value) 

2468 ) 

2469 self.system_id_widget.estimatorComboBox.setCurrentIndex( 

2470 self.system_id_widget.estimatorComboBox.findText(worksheet.cell(20, 2).value) 

2471 ) 

2472 self.system_id_widget.levelDoubleSpinBox.setValue(float(worksheet.cell(21, 2).value)) 

2473 # this should be a temporary solution - template file rework needed 

2474 low, high = worksheet.cell(21, 3).value, worksheet.cell(21, 4).value 

2475 sigma = worksheet.cell(21, 5).value 

2476 if low is not None: 

2477 self.system_id_widget.lowFreqCutoffSpinBox.setValue(int(low)) 

2478 if high is not None: 

2479 self.system_id_widget.highFreqCutoffSpinBox.setValue(int(high)) 

2480 if sigma is not None: 

2481 self.definition_widget.sigma_clipping_selector.setValue( 

2482 float(sigma) 

2483 ) # TODO: sigma clipping and bandwidths should get 

2484 # their own rows, but how to maintain backward compatibility? 

2485 self.system_id_widget.signalTypeComboBox.setCurrentIndex( 

2486 self.system_id_widget.signalTypeComboBox.findText(worksheet.cell(22, 2).value) 

2487 ) 

2488 self.system_id_widget.windowComboBox.setCurrentIndex( 

2489 self.system_id_widget.windowComboBox.findText(worksheet.cell(23, 2).value) 

2490 ) 

2491 self.system_id_widget.overlapDoubleSpinBox.setValue(float(worksheet.cell(24, 2).value)) 

2492 self.system_id_widget.onFractionDoubleSpinBox.setValue(float(worksheet.cell(25, 2).value)) 

2493 self.system_id_widget.pretriggerDoubleSpinBox.setValue(float(worksheet.cell(26, 2).value)) 

2494 self.system_id_widget.rampFractionDoubleSpinBox.setValue(float(worksheet.cell(27, 2).value)) 

2495 

2496 # Now we need to find the transformation matrices' sizes 

2497 response_channels = self.definition_widget.control_channels_display.value() 

2498 output_channels = self.definition_widget.output_channels_display.value() 

2499 output_transform_row = 30 

2500 if ( 

2501 isinstance(worksheet.cell(29, 2).value, str) 

2502 and worksheet.cell(29, 2).value.lower() == "none" 

2503 ): 

2504 self.response_transformation_matrix = None 

2505 else: 

2506 while True: 

2507 if worksheet.cell(output_transform_row, 1).value == "Output Transformation Matrix:": 

2508 break 

2509 output_transform_row += 1 

2510 response_size = output_transform_row - 29 

2511 response_transformation = [] 

2512 for i in range(response_size): 

2513 response_transformation.append([]) 

2514 for j in range(response_channels): 

2515 response_transformation[-1].append(float(worksheet.cell(29 + i, 2 + j).value)) 

2516 self.response_transformation_matrix = np.array(response_transformation) 

2517 if ( 

2518 isinstance(worksheet.cell(output_transform_row, 2).value, str) 

2519 and worksheet.cell(output_transform_row, 2).value.lower() == "none" 

2520 ): 

2521 self.output_transformation_matrix = None 

2522 else: 

2523 output_transformation = [] 

2524 i = 0 

2525 while True: 

2526 if worksheet.cell(output_transform_row + i, 2).value is None or ( 

2527 isinstance(worksheet.cell(output_transform_row + i, 2).value, str) 

2528 and worksheet.cell(output_transform_row + i, 2).value.strip() == "" 

2529 ): 

2530 break 

2531 output_transformation.append([]) 

2532 for j in range(output_channels): 

2533 output_transformation[-1].append( 

2534 float(worksheet.cell(output_transform_row + i, 2 + j).value) 

2535 ) 

2536 i += 1 

2537 self.output_transformation_matrix = np.array(output_transformation) 

2538 self.define_transformation_matrices(None, dialog=False) 

2539 self.select_spec_file(None, worksheet.cell(28, 2).value) 

2540 

2541 

2542# %% Environment 

2543 

2544 

2545class RandomVibrationEnvironment(AbstractSysIdEnvironment): 

2546 """Random Environment class defining the interface with the controller""" 

2547 

2548 def __init__( 

2549 self, 

2550 environment_name: str, 

2551 queue_container: RandomVibrationQueues, 

2552 acquisition_active: mp.sharedctypes.Synchronized, 

2553 output_active: mp.sharedctypes.Synchronized, 

2554 ): 

2555 """ 

2556 Random Vibration Environment Constructor that fills out the ``command_map`` 

2557 

2558 Parameters 

2559 ---------- 

2560 environment_name : str 

2561 Name of the environment. 

2562 queue_container : RandomVibrationQueues 

2563 Container of queues used by the Random Vibration Environment. 

2564 

2565 """ 

2566 super().__init__( 

2567 environment_name, 

2568 queue_container.environment_command_queue, 

2569 queue_container.gui_update_queue, 

2570 queue_container.controller_communication_queue, 

2571 queue_container.log_file_queue, 

2572 queue_container.collector_command_queue, 

2573 queue_container.signal_generation_command_queue, 

2574 queue_container.spectral_command_queue, 

2575 queue_container.data_analysis_command_queue, 

2576 queue_container.data_in_queue, 

2577 queue_container.data_out_queue, 

2578 acquisition_active, 

2579 output_active, 

2580 ) 

2581 self.map_command(RandomVibrationCommands.START_CONTROL, self.start_control) 

2582 self.map_command(RandomVibrationCommands.STOP_CONTROL, self.stop_environment) 

2583 self.map_command(RandomVibrationCommands.ADJUST_TEST_LEVEL, self.adjust_test_level) 

2584 self.map_command( 

2585 RandomVibrationCommands.CHECK_FOR_COMPLETE_SHUTDOWN, 

2586 self.check_for_control_shutdown, 

2587 ) 

2588 self.map_command(RandomVibrationCommands.RECOMPUTE_PREDICTION, self.recompute_prediction) 

2589 self.map_command( 

2590 GlobalCommands.UPDATE_INTERACTIVE_CONTROL_PARAMETERS, 

2591 self.update_interactive_control_parameters, 

2592 ) 

2593 self.map_command(GlobalCommands.SEND_INTERACTIVE_COMMAND, self.send_interactive_command) 

2594 self.queue_container = queue_container 

2595 

2596 def initialize_environment_test_parameters( 

2597 self, environment_parameters: RandomVibrationMetadata 

2598 ): 

2599 """ 

2600 Initialize the environment parameters specific to this environment 

2601 

2602 The environment will recieve parameters defining itself from the 

2603 user interface and must set itself up accordingly. 

2604 

2605 Parameters 

2606 ---------- 

2607 environment_parameters : RandomVibrationMetadata 

2608 A container containing the parameters defining the environment 

2609 

2610 """ 

2611 super().initialize_environment_test_parameters(environment_parameters) 

2612 

2613 # Set up the collector 

2614 self.queue_container.collector_command_queue.put( 

2615 self.environment_name, 

2616 ( 

2617 DataCollectorCommands.INITIALIZE_COLLECTOR, 

2618 self.get_data_collector_metadata(), 

2619 ), 

2620 ) 

2621 # Set up the signal generation 

2622 self.queue_container.signal_generation_command_queue.put( 

2623 self.environment_name, 

2624 ( 

2625 SignalGenerationCommands.INITIALIZE_PARAMETERS, 

2626 self.get_signal_generation_metadata(), 

2627 ), 

2628 ) 

2629 # Set up the spectral processing 

2630 self.queue_container.spectral_command_queue.put( 

2631 self.environment_name, 

2632 ( 

2633 SpectralProcessingCommands.INITIALIZE_PARAMETERS, 

2634 self.get_spectral_processing_metadata(), 

2635 ), 

2636 ) 

2637 # Set up the data analysis 

2638 self.queue_container.data_analysis_command_queue.put( 

2639 self.environment_name, 

2640 ( 

2641 RandomVibrationDataAnalysisCommands.INITIALIZE_PARAMETERS, 

2642 self.environment_parameters, 

2643 ), 

2644 ) 

2645 

2646 def update_interactive_control_parameters(self, parameters): 

2647 """Sends updated parameters to the interactive control law on the data analysis process""" 

2648 self.queue_container.data_analysis_command_queue.put( 

2649 self.environment_name, 

2650 (GlobalCommands.UPDATE_INTERACTIVE_CONTROL_PARAMETERS, parameters), 

2651 ) 

2652 

2653 def send_interactive_command(self, command): 

2654 """General method that can be used by an interactive UI object to pass commands and data to 

2655 its corresponding computation object""" 

2656 if self.environment_parameters.control_python_function_type == 3: # Interactive 

2657 self.queue_container.data_analysis_command_queue.put( 

2658 self.environment_name, 

2659 (GlobalCommands.SEND_INTERACTIVE_COMMAND, command), 

2660 ) 

2661 else: 

2662 raise ValueError( 

2663 "Received an SEND_INTERACTIVE_COMMAND signal without an interactive control law. " 

2664 "How did this happen?" 

2665 ) 

2666 

2667 def system_id_complete(self, data): 

2668 """Triggered when system identification has been completed, starting control predictions""" 

2669 super().system_id_complete(data) 

2670 self.queue_container.data_analysis_command_queue.put( 

2671 self.environment_name, 

2672 (RandomVibrationDataAnalysisCommands.PERFORM_CONTROL_PREDICTION, None), 

2673 ) 

2674 

2675 def get_data_collector_metadata(self): 

2676 """Gets relevant metadata for the data collector process""" 

2677 num_channels = self.environment_parameters.number_of_channels 

2678 response_channel_indices = self.environment_parameters.response_channel_indices 

2679 reference_channel_indices = self.environment_parameters.reference_channel_indices 

2680 acquisition_type = AcquisitionType.FREE_RUN 

2681 acceptance = Acceptance.AUTOMATIC 

2682 acceptance_function = None 

2683 overlap_fraction = self.environment_parameters.cpsd_overlap 

2684 trigger_channel_index = 0 

2685 trigger_slope = TriggerSlope.POSITIVE 

2686 trigger_level = 0 

2687 trigger_hysteresis = 0 

2688 trigger_hysteresis_samples = 0 

2689 pretrigger_fraction = 0 

2690 frame_size = self.environment_parameters.samples_per_frame 

2691 window = Window.HANN if self.environment_parameters.cpsd_window == "Hann" else None 

2692 # use number of sysid averages as kurtosis buffer size 

2693 # (could maybe make this match the test duration if user is using the "Time at Level" 

2694 # function, would need to pass info from the RandomVibrationUI object) 

2695 kurtosis_buffer_length = self.environment_parameters.sysid_averages 

2696 

2697 return CollectorMetadata( 

2698 num_channels, 

2699 response_channel_indices, 

2700 reference_channel_indices, 

2701 acquisition_type, 

2702 acceptance, 

2703 acceptance_function, 

2704 overlap_fraction, 

2705 trigger_channel_index, 

2706 trigger_slope, 

2707 trigger_level, 

2708 trigger_hysteresis, 

2709 trigger_hysteresis_samples, 

2710 pretrigger_fraction, 

2711 frame_size, 

2712 window, 

2713 kurtosis_buffer_length=kurtosis_buffer_length, 

2714 response_transformation_matrix=self.environment_parameters.response_transformation_matrix, 

2715 reference_transformation_matrix=self.environment_parameters.reference_transformation_matrix, 

2716 ) 

2717 

2718 def get_signal_generation_metadata(self): 

2719 """Gets relevant metadata for the signal generation process""" 

2720 return SignalGenerationMetadata( 

2721 samples_per_write=self.data_acquisition_parameters.samples_per_write, 

2722 level_ramp_samples=self.environment_parameters.test_level_ramp_time 

2723 * self.environment_parameters.sample_rate 

2724 * self.data_acquisition_parameters.output_oversample, 

2725 output_transformation_matrix=self.environment_parameters.reference_transformation_matrix, 

2726 ) 

2727 

2728 def get_signal_generator(self): 

2729 """Gets the signal generator object that will generate signals for the environment""" 

2730 return CPSDSignalGenerator( 

2731 self.environment_parameters.sample_rate, 

2732 self.environment_parameters.samples_per_frame, 

2733 self.environment_parameters.num_reference_channels, 

2734 None, 

2735 self.environment_parameters.cola_overlap, 

2736 self.environment_parameters.cola_window, 

2737 self.environment_parameters.cola_window_exponent, 

2738 self.environment_parameters.sigma_clip, 

2739 self.data_acquisition_parameters.output_oversample, 

2740 ) 

2741 

2742 def get_spectral_processing_metadata(self): 

2743 """Gets the required metadata for the spectral processing process""" 

2744 averaging_type = AveragingTypes.LINEAR 

2745 averages = self.environment_parameters.frames_in_cpsd 

2746 exponential_averaging_coefficient = 0 

2747 if self.environment_parameters.sysid_estimator == "H1": 

2748 frf_estimator = Estimator.H1 

2749 elif self.environment_parameters.sysid_estimator == "H2": 

2750 frf_estimator = Estimator.H2 

2751 elif self.environment_parameters.sysid_estimator == "H3": 

2752 frf_estimator = Estimator.H3 

2753 elif self.environment_parameters.sysid_estimator == "Hv": 

2754 frf_estimator = Estimator.HV 

2755 else: 

2756 raise ValueError(f"Invalid FRF Estimator {self.environment_parameters.sysid_estimator}") 

2757 num_response_channels = self.environment_parameters.num_response_channels 

2758 num_reference_channels = self.environment_parameters.num_reference_channels 

2759 frequency_spacing = self.environment_parameters.frequency_spacing 

2760 sample_rate = self.environment_parameters.sample_rate 

2761 num_frequency_lines = self.environment_parameters.fft_lines 

2762 return SpectralProcessingMetadata( 

2763 averaging_type, 

2764 averages, 

2765 exponential_averaging_coefficient, 

2766 frf_estimator, 

2767 num_response_channels, 

2768 num_reference_channels, 

2769 frequency_spacing, 

2770 sample_rate, 

2771 num_frequency_lines, 

2772 ) 

2773 

2774 def recompute_prediction(self, data): # pylint: disable=unused-argument 

2775 """Sends a signal to the data analysis process to recompute test predictions""" 

2776 self.queue_container.data_analysis_command_queue.put( 

2777 self.environment_name, 

2778 (RandomVibrationDataAnalysisCommands.PERFORM_CONTROL_PREDICTION, None), 

2779 ) 

2780 

2781 def start_control(self, data): 

2782 """Starts the environment at the specified test level""" 

2783 self.log("Starting Control") 

2784 self.siggen_shutdown_achieved = False 

2785 self.collector_shutdown_achieved = False 

2786 self.spectral_shutdown_achieved = False 

2787 self.analysis_shutdown_achieved = False 

2788 self.queue_container.controller_communication_queue.put( 

2789 self.environment_name, 

2790 (GlobalCommands.START_ENVIRONMENT, self.environment_name), 

2791 ) 

2792 # Set up the collector 

2793 self.queue_container.collector_command_queue.put( 

2794 self.environment_name, 

2795 ( 

2796 DataCollectorCommands.INITIALIZE_COLLECTOR, 

2797 self.get_data_collector_metadata(), 

2798 ), 

2799 ) 

2800 

2801 self.queue_container.collector_command_queue.put( 

2802 self.environment_name, 

2803 ( 

2804 DataCollectorCommands.SET_TEST_LEVEL, 

2805 (self.environment_parameters.skip_frames, data), 

2806 ), 

2807 ) 

2808 time.sleep(0.01) 

2809 

2810 # Set up the signal generation 

2811 self.queue_container.signal_generation_command_queue.put( 

2812 self.environment_name, 

2813 ( 

2814 SignalGenerationCommands.INITIALIZE_PARAMETERS, 

2815 self.get_signal_generation_metadata(), 

2816 ), 

2817 ) 

2818 

2819 self.queue_container.signal_generation_command_queue.put( 

2820 self.environment_name, 

2821 ( 

2822 SignalGenerationCommands.INITIALIZE_SIGNAL_GENERATOR, 

2823 self.get_signal_generator(), 

2824 ), 

2825 ) 

2826 

2827 self.queue_container.signal_generation_command_queue.put( 

2828 self.environment_name, (SignalGenerationCommands.MUTE, None) 

2829 ) 

2830 

2831 self.queue_container.signal_generation_command_queue.put( 

2832 self.environment_name, (SignalGenerationCommands.ADJUST_TEST_LEVEL, data) 

2833 ) 

2834 

2835 # Tell the collector to start acquiring data 

2836 self.queue_container.collector_command_queue.put( 

2837 self.environment_name, (DataCollectorCommands.ACQUIRE, None) 

2838 ) 

2839 

2840 # Tell the signal generation to start generating signals 

2841 self.queue_container.signal_generation_command_queue.put( 

2842 self.environment_name, (SignalGenerationCommands.GENERATE_SIGNALS, None) 

2843 ) 

2844 

2845 # # Set up the data analysis 

2846 # self.queue_container.data_analysis_command_queue.put( 

2847 # self.environment_name, 

2848 # (RandomVibrationDataAnalysisCommands.INITIALIZE_PARAMETERS, 

2849 # self.environment_parameters)) 

2850 

2851 # Start the data analysis running 

2852 self.queue_container.data_analysis_command_queue.put( 

2853 self.environment_name, 

2854 (RandomVibrationDataAnalysisCommands.RUN_CONTROL, None), 

2855 ) 

2856 

2857 # Set up the spectral processing 

2858 self.queue_container.spectral_command_queue.put( 

2859 self.environment_name, 

2860 ( 

2861 SpectralProcessingCommands.INITIALIZE_PARAMETERS, 

2862 self.get_spectral_processing_metadata(), 

2863 ), 

2864 ) 

2865 

2866 # Tell the spectral analysis to clear and start acquiring 

2867 self.queue_container.spectral_command_queue.put( 

2868 self.environment_name, 

2869 (SpectralProcessingCommands.CLEAR_SPECTRAL_PROCESSING, None), 

2870 ) 

2871 

2872 self.queue_container.spectral_command_queue.put( 

2873 self.environment_name, 

2874 (SpectralProcessingCommands.RUN_SPECTRAL_PROCESSING, None), 

2875 ) 

2876 

2877 def stop_environment(self, data): 

2878 """Stop the environment gracefully 

2879 

2880 This function defines the operations to shut down the environment 

2881 gracefully so there is no hard stop that might damage test equipment 

2882 or parts. 

2883 

2884 Parameters 

2885 ---------- 

2886 data : Ignored 

2887 This parameter is not used by the function but must be present 

2888 due to the calling signature of functions called through the 

2889 ``command_map`` 

2890 

2891 """ 

2892 self.log("Stopping Control") 

2893 self.queue_container.collector_command_queue.put( 

2894 self.environment_name, 

2895 ( 

2896 DataCollectorCommands.SET_TEST_LEVEL, 

2897 (self.environment_parameters.skip_frames * 10, 1), 

2898 ), 

2899 ) 

2900 self.queue_container.signal_generation_command_queue.put( 

2901 self.environment_name, (SignalGenerationCommands.START_SHUTDOWN, None) 

2902 ) 

2903 self.queue_container.spectral_command_queue.put( 

2904 self.environment_name, 

2905 (SpectralProcessingCommands.STOP_SPECTRAL_PROCESSING, None), 

2906 ) 

2907 self.queue_container.data_analysis_command_queue.put( 

2908 self.environment_name, 

2909 (RandomVibrationDataAnalysisCommands.STOP_CONTROL, None), 

2910 ) 

2911 self.queue_container.environment_command_queue.put( 

2912 self.environment_name, 

2913 (RandomVibrationCommands.CHECK_FOR_COMPLETE_SHUTDOWN, None), 

2914 ) 

2915 

2916 def check_for_control_shutdown(self, data): # pylint: disable=unused-argument 

2917 """Checks the different processes to see if the controller has shut down gracefully""" 

2918 if ( 

2919 self.siggen_shutdown_achieved 

2920 and self.collector_shutdown_achieved 

2921 and self.spectral_shutdown_achieved 

2922 and self.analysis_shutdown_achieved 

2923 ): 

2924 self.log("Shutdown Achieved") 

2925 self.gui_update_queue.put((self.environment_name, ("enable_control", None))) 

2926 else: 

2927 # Recheck some time later 

2928 time.sleep(1) 

2929 self.environment_command_queue.put( 

2930 self.environment_name, 

2931 (RandomVibrationCommands.CHECK_FOR_COMPLETE_SHUTDOWN, None), 

2932 ) 

2933 

2934 def adjust_test_level(self, data): 

2935 """Adjusts the test level of the environment to the specified level""" 

2936 self.queue_container.signal_generation_command_queue.put( 

2937 self.environment_name, (SignalGenerationCommands.ADJUST_TEST_LEVEL, data) 

2938 ) 

2939 self.queue_container.collector_command_queue.put( 

2940 self.environment_name, 

2941 ( 

2942 DataCollectorCommands.SET_TEST_LEVEL, 

2943 (self.environment_parameters.skip_frames, data), 

2944 ), 

2945 ) 

2946 

2947 def quit(self, data): 

2948 """Closes down the environment permanently as the software is exiting""" 

2949 for queue in [ 

2950 self.queue_container.spectral_command_queue, 

2951 self.queue_container.data_analysis_command_queue, 

2952 self.queue_container.signal_generation_command_queue, 

2953 self.queue_container.collector_command_queue, 

2954 ]: 

2955 queue.put(self.environment_name, (GlobalCommands.QUIT, None)) 

2956 # Return true to stop the task 

2957 return True 

2958 

2959 

2960# %% Process 

2961 

2962 

2963def random_vibration_process( 

2964 environment_name: str, 

2965 input_queue: VerboseMessageQueue, 

2966 gui_update_queue: Queue, 

2967 controller_communication_queue: VerboseMessageQueue, 

2968 log_file_queue: Queue, 

2969 data_in_queue: Queue, 

2970 data_out_queue: Queue, 

2971 acquisition_active: mp.sharedctypes.Synchronized, 

2972 output_active: mp.sharedctypes.Synchronized, 

2973): 

2974 """Random vibration environment process function called by multiprocessing 

2975 

2976 This function defines the Random Vibration Environment process that 

2977 gets run by the multiprocessing module when it creates a new process. It 

2978 creates a RandomVibrationEnvironment object and runs it. 

2979 

2980 Parameters 

2981 ---------- 

2982 environment_name : str : 

2983 Name of the environment 

2984 input_queue : VerboseMessageQueue : 

2985 Queue containing instructions for the environment 

2986 gui_update_queue : Queue : 

2987 Queue where GUI updates are put 

2988 controller_communication_queue : Queue : 

2989 Queue for global communications with the controller 

2990 log_file_queue : Queue : 

2991 Queue for writing log file messages 

2992 data_in_queue : Queue : 

2993 Queue from which data will be read by the environment 

2994 data_out_queue : Queue : 

2995 Queue to which data will be written that will be output by the hardware. 

2996 acquisition_active : mp.sharedctypes.Synchronized 

2997 A synchronized value that indicates when the acquisition is active 

2998 output_active : mp.sharedctypes.Synchronized 

2999 A synchronized value that indicates when the output is active 

3000 """ 

3001 # Create vibration queues 

3002 queue_container = RandomVibrationQueues( 

3003 environment_name, 

3004 input_queue, 

3005 gui_update_queue, 

3006 controller_communication_queue, 

3007 data_in_queue, 

3008 data_out_queue, 

3009 log_file_queue, 

3010 ) 

3011 

3012 spectral_proc = mp.Process( 

3013 target=spectral_processing_process, 

3014 args=( 

3015 environment_name, 

3016 queue_container.spectral_command_queue, 

3017 queue_container.data_for_spectral_computation_queue, 

3018 queue_container.updated_spectral_quantities_queue, 

3019 queue_container.environment_command_queue, 

3020 queue_container.gui_update_queue, 

3021 queue_container.log_file_queue, 

3022 ), 

3023 ) 

3024 spectral_proc.start() 

3025 analysis_proc = mp.Process( 

3026 target=random_data_analysis_process, 

3027 args=( 

3028 environment_name, 

3029 queue_container.data_analysis_command_queue, 

3030 queue_container.updated_spectral_quantities_queue, 

3031 queue_container.cpsd_to_generate_queue, 

3032 queue_container.environment_command_queue, 

3033 queue_container.gui_update_queue, 

3034 queue_container.log_file_queue, 

3035 ), 

3036 ) 

3037 analysis_proc.start() 

3038 siggen_proc = mp.Process( 

3039 target=signal_generation_process, 

3040 args=( 

3041 environment_name, 

3042 queue_container.signal_generation_command_queue, 

3043 queue_container.cpsd_to_generate_queue, 

3044 queue_container.data_out_queue, 

3045 queue_container.environment_command_queue, 

3046 queue_container.log_file_queue, 

3047 queue_container.gui_update_queue, 

3048 ), 

3049 ) 

3050 siggen_proc.start() 

3051 collection_proc = mp.Process( 

3052 target=data_collector_process, 

3053 args=( 

3054 environment_name, 

3055 queue_container.collector_command_queue, 

3056 queue_container.data_in_queue, 

3057 [queue_container.data_for_spectral_computation_queue], 

3058 queue_container.environment_command_queue, 

3059 queue_container.log_file_queue, 

3060 queue_container.gui_update_queue, 

3061 ), 

3062 ) 

3063 

3064 collection_proc.start() 

3065 process_class = RandomVibrationEnvironment( 

3066 environment_name, queue_container, acquisition_active, output_active 

3067 ) 

3068 process_class.run() 

3069 

3070 # Rejoin all the processes 

3071 process_class.log("Joining Subprocesses") 

3072 process_class.log("Joining Spectral Computation") 

3073 spectral_proc.join() 

3074 process_class.log("Joining Data Analysis") 

3075 analysis_proc.join() 

3076 process_class.log("Joining Signal Generation") 

3077 siggen_proc.join() 

3078 process_class.log("Joining Data Collection") 

3079 collection_proc.join()