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

1029 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 transient environment that utilizes system 

4identification. 

5 

6Rattlesnake Vibration Control Software 

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

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

9Government retains certain rights in this software. 

10 

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

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

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

14(at your option) any later version. 

15 

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

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

18MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

19GNU General Public License for more details. 

20 

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

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

23""" 

24 

25import importlib 

26import inspect 

27import multiprocessing as mp 

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

29import os 

30import traceback 

31from enum import Enum 

32from multiprocessing.queues import Queue 

33 

34import netCDF4 as nc4 

35import numpy as np 

36import scipy.signal as sig 

37from qtpy import QtCore, QtWidgets, uic 

38from qtpy.QtCore import Qt 

39 

40from .abstract_sysid_environment import ( 

41 AbstractSysIdEnvironment, 

42 AbstractSysIdMetadata, 

43 AbstractSysIdUI, 

44) 

45from .environments import ( 

46 ControlTypes, 

47 environment_definition_ui_paths, 

48 environment_prediction_ui_paths, 

49 environment_run_ui_paths, 

50) 

51from .ui_utilities import ( 

52 PlotTimeWindow, 

53 TransformationMatrixWindow, 

54 colororder, 

55 load_time_history, 

56 multiline_plotter, 

57) 

58from .utilities import ( 

59 DataAcquisitionParameters, 

60 GlobalCommands, 

61 VerboseMessageQueue, 

62 align_signals, 

63 db2scale, 

64 load_python_module, 

65 rms_time, 

66 shift_signal, 

67 trac, 

68) 

69 

70# %% Global Variables 

71CONTROL_TYPE = ControlTypes.TRANSIENT 

72MAXIMUM_NAME_LENGTH = 50 

73BUFFER_SIZE_SAMPLES_PER_READ_MULTIPLIER = 2 

74 

75 

76# %% Commands 

77class TransientCommands(Enum): 

78 """Valid commands for the transient environment""" 

79 

80 START_CONTROL = 0 

81 STOP_CONTROL = 1 

82 PERFORM_CONTROL_PREDICTION = 3 

83 # UPDATE_INTERACTIVE_CONTROL_PARAMETERS = 4 

84 

85 

86# %% Queues 

87 

88 

89class TransientQueues: 

90 """A container class for the queues that this environment will manage.""" 

91 

92 def __init__( 

93 self, 

94 environment_name: str, 

95 environment_command_queue: VerboseMessageQueue, 

96 gui_update_queue: Queue, 

97 controller_communication_queue: VerboseMessageQueue, 

98 data_in_queue: Queue, 

99 data_out_queue: Queue, 

100 log_file_queue: VerboseMessageQueue, 

101 ): 

102 """A container class for the queues that transient will manage. 

103 

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

105 This class organizes those queues into one common namespace. 

106 

107 

108 Parameters 

109 ---------- 

110 environment_name : str 

111 Name of the environment 

112 environment_command_queue : VerboseMessageQueue 

113 Queue that is read by the environment for environment commands 

114 gui_update_queue : mp.queues.Queue 

115 Queue where various subtasks put instructions for updating the 

116 widgets in the user interface 

117 controller_communication_queue : VerboseMessageQueue 

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

119 data_in_queue : mp.queues.Queue 

120 Multiprocessing queue that connects the acquisition subtask to the 

121 environment subtask. Each environment will retrieve acquired data 

122 from this queue. 

123 data_out_queue : mp.queues.Queue 

124 Multiprocessing queue that connects the output subtask to the 

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

126 the controller to generate in this queue. 

127 log_file_queue : VerboseMessageQueue 

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

129 subtask and written to a file. 

130 """ 

131 self.environment_command_queue = environment_command_queue 

132 self.gui_update_queue = gui_update_queue 

133 self.data_analysis_command_queue = VerboseMessageQueue( 

134 log_file_queue, environment_name + " Data Analysis Command Queue" 

135 ) 

136 self.signal_generation_command_queue = VerboseMessageQueue( 

137 log_file_queue, environment_name + " Signal Generation Command Queue" 

138 ) 

139 self.spectral_command_queue = VerboseMessageQueue( 

140 log_file_queue, environment_name + " Spectral Computation Command Queue" 

141 ) 

142 self.collector_command_queue = VerboseMessageQueue( 

143 log_file_queue, environment_name + " Data Collector Command Queue" 

144 ) 

145 self.controller_communication_queue = controller_communication_queue 

146 self.data_in_queue = data_in_queue 

147 self.data_out_queue = data_out_queue 

148 self.data_for_spectral_computation_queue = mp.Queue() 

149 self.updated_spectral_quantities_queue = mp.Queue() 

150 self.time_history_to_generate_queue = mp.Queue() 

151 self.log_file_queue = log_file_queue 

152 

153 

154# %% Metadata 

155 

156 

157class TransientMetadata(AbstractSysIdMetadata): 

158 """Metadata required to define a transient control law in rattlesnake.""" 

159 

160 def __init__( 

161 self, 

162 number_of_channels, 

163 sample_rate, 

164 control_signal, 

165 ramp_time, 

166 control_python_script, 

167 control_python_function, 

168 control_python_function_type, 

169 control_python_function_parameters, 

170 control_channel_indices, 

171 output_channel_indices, 

172 response_transformation_matrix, 

173 output_transformation_matrix, 

174 ): 

175 super().__init__() 

176 self.number_of_channels = number_of_channels 

177 self.sample_rate = sample_rate 

178 self.control_signal = control_signal 

179 self.test_level_ramp_time = ramp_time 

180 self.control_python_script = control_python_script 

181 self.control_python_function = control_python_function 

182 self.control_python_function_type = control_python_function_type 

183 self.control_python_function_parameters = control_python_function_parameters 

184 self.control_channel_indices = control_channel_indices 

185 self.output_channel_indices = output_channel_indices 

186 self.response_transformation_matrix = response_transformation_matrix 

187 self.reference_transformation_matrix = output_transformation_matrix 

188 

189 @property 

190 def ramp_samples(self): 

191 """Number of samples to ramp down to zero when aborting a test""" 

192 return int(self.test_level_ramp_time * self.sample_rate) 

193 

194 @property 

195 def number_of_channels(self): 

196 """Total number of channels in the environment""" 

197 return self._number_of_channels 

198 

199 @number_of_channels.setter 

200 def number_of_channels(self, value): 

201 """Sets the total number of channels in the environment""" 

202 self._number_of_channels = value 

203 

204 @property 

205 def response_channel_indices(self): 

206 """Indices identifying which channels are control channels""" 

207 return self.control_channel_indices 

208 

209 @property 

210 def reference_channel_indices(self): 

211 """Indices identifying which channels are reference or excitation channels""" 

212 return self.output_channel_indices 

213 

214 @property 

215 def response_transformation_matrix(self): 

216 """Transformation matrix applied to the control channels""" 

217 return self._response_transformation_matrix 

218 

219 @response_transformation_matrix.setter 

220 def response_transformation_matrix(self, value): 

221 """Sets the transformation matrix for the control channels""" 

222 self._response_transformation_matrix = value 

223 

224 @property 

225 def reference_transformation_matrix(self): 

226 """Transformation matrix applied to the excitation channels""" 

227 return self._reference_transformation_matrix 

228 

229 @reference_transformation_matrix.setter 

230 def reference_transformation_matrix(self, value): 

231 """Sets the transformation matrix applied to the excitation channels""" 

232 self._reference_transformation_matrix = value 

233 

234 @property 

235 def sample_rate(self): 

236 """Gets the sample rate of the data acquisition system""" 

237 return self._sample_rate 

238 

239 @sample_rate.setter 

240 def sample_rate(self, value): 

241 """Sets the sample rate of the data acquisition system""" 

242 self._sample_rate = value 

243 

244 @property 

245 def signal_samples(self): 

246 """Gets the number of samples in the signal that is being controlled to""" 

247 return self.control_signal.shape[-1] 

248 

249 def store_to_netcdf( 

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

251 ): 

252 """Stores the metadata in a netcdf group 

253 

254 Parameters 

255 ---------- 

256 netcdf_group_handle : nc4._netCDF4.Group 

257 A group in a NetCDF4 group defining the environment's medatadata 

258 """ 

259 super().store_to_netcdf(netcdf_group_handle) 

260 netcdf_group_handle.test_level_ramp_time = self.test_level_ramp_time 

261 netcdf_group_handle.control_python_script = self.control_python_script 

262 netcdf_group_handle.control_python_function = self.control_python_function 

263 netcdf_group_handle.control_python_function_type = self.control_python_function_type 

264 netcdf_group_handle.control_python_function_parameters = ( 

265 self.control_python_function_parameters 

266 ) 

267 # Save the output signal 

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

269 netcdf_group_handle.createDimension("specification_channels", self.control_signal.shape[0]) 

270 netcdf_group_handle.createDimension("signal_samples", self.signal_samples) 

271 var = netcdf_group_handle.createVariable( 

272 "control_signal", "f8", ("specification_channels", "signal_samples") 

273 ) 

274 var[...] = self.control_signal 

275 # Control Channels 

276 var = netcdf_group_handle.createVariable( 

277 "control_channel_indices", "i4", ("control_channels") 

278 ) 

279 var[...] = self.control_channel_indices 

280 # Transformation Matrix 

281 if self.response_transformation_matrix is not None: 

282 netcdf_group_handle.createDimension( 

283 "response_transformation_rows", 

284 self.response_transformation_matrix.shape[0], 

285 ) 

286 netcdf_group_handle.createDimension( 

287 "response_transformation_cols", 

288 self.response_transformation_matrix.shape[1], 

289 ) 

290 var = netcdf_group_handle.createVariable( 

291 "response_transformation_matrix", 

292 "f8", 

293 ("response_transformation_rows", "response_transformation_cols"), 

294 ) 

295 var[...] = self.response_transformation_matrix 

296 if self.reference_transformation_matrix is not None: 

297 netcdf_group_handle.createDimension( 

298 "reference_transformation_rows", 

299 self.reference_transformation_matrix.shape[0], 

300 ) 

301 netcdf_group_handle.createDimension( 

302 "reference_transformation_cols", 

303 self.reference_transformation_matrix.shape[1], 

304 ) 

305 var = netcdf_group_handle.createVariable( 

306 "reference_transformation_matrix", 

307 "f8", 

308 ("reference_transformation_rows", "reference_transformation_cols"), 

309 ) 

310 var[...] = self.reference_transformation_matrix 

311 

312 

313# %% UI 

314 

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

316 AbstractControlLawComputation, 

317) 

318from .abstract_sysid_data_analysis import ( # noqa: E402 pylint: disable=wrong-import-position 

319 sysid_data_analysis_process, 

320) 

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

322 FrameBuffer, 

323 data_collector_process, 

324) 

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

326 TransientSignalGenerator, 

327) 

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

329 SignalGenerationCommands, 

330 SignalGenerationMetadata, 

331 signal_generation_process, 

332) 

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

334 spectral_processing_process, 

335) 

336 

337 

338class TransientUI(AbstractSysIdUI): 

339 """Class defining the user interface for the transient environment""" 

340 

341 def __init__( 

342 self, 

343 environment_name: str, 

344 definition_tabwidget: QtWidgets.QTabWidget, 

345 system_id_tabwidget: QtWidgets.QTabWidget, 

346 test_predictions_tabwidget: QtWidgets.QTabWidget, 

347 run_tabwidget: QtWidgets.QTabWidget, 

348 environment_command_queue: VerboseMessageQueue, 

349 controller_communication_queue: VerboseMessageQueue, 

350 log_file_queue: Queue, 

351 ): 

352 super().__init__( 

353 environment_name, 

354 environment_command_queue, 

355 controller_communication_queue, 

356 log_file_queue, 

357 system_id_tabwidget, 

358 ) 

359 # Add the page to the control definition tabwidget 

360 self.definition_widget = QtWidgets.QWidget() 

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

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

363 # Add the page to the control prediction tabwidget 

364 self.prediction_widget = QtWidgets.QWidget() 

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

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

367 # Add the page to the run tabwidget 

368 self.run_widget = QtWidgets.QWidget() 

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

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

371 

372 self.specification_signal = None 

373 self.show_signal_checkboxes = None 

374 self.plot_data_items = {} 

375 self.plot_windows = [] 

376 self.response_transformation_matrix = None 

377 self.output_transformation_matrix = None 

378 self.python_control_module = None 

379 self.physical_channel_names = None 

380 self.physical_output_indices = None 

381 self.excitation_prediction = None 

382 self.response_prediction = None 

383 self.last_control_data = None 

384 self.last_output_data = None 

385 self.interactive_control_law_widget = None 

386 self.interactive_control_law_window = None 

387 self.max_plot_samples = None 

388 

389 self.control_selector_widgets = [ 

390 self.prediction_widget.response_selector, 

391 self.run_widget.control_channel_selector, 

392 ] 

393 self.output_selector_widgets = [ 

394 self.prediction_widget.excitation_selector, 

395 ] 

396 

397 # Set common look and feel for plots 

398 plot_widgets = [ 

399 self.definition_widget.signal_display_plot, 

400 self.prediction_widget.excitation_display_plot, 

401 self.prediction_widget.response_display_plot, 

402 self.run_widget.output_signal_plot, 

403 self.run_widget.response_signal_plot, 

404 ] 

405 for plot_widget in plot_widgets: 

406 plot_item = plot_widget.getPlotItem() 

407 plot_item.showGrid(True, True, 0.25) 

408 plot_item.enableAutoRange() 

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

410 

411 self.connect_callbacks() 

412 

413 # Complete the profile commands 

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

415 self.command_map["Set Repeat"] = self.set_repeat_from_profile 

416 self.command_map["Set No Repeat"] = self.set_norepeat_from_profile 

417 

418 def connect_callbacks(self): 

419 """Connects the callbacks to the transient UI widgets""" 

420 # Definition 

421 self.definition_widget.load_signal_button.clicked.connect(self.load_signal) 

422 self.definition_widget.transformation_matrices_button.clicked.connect( 

423 self.define_transformation_matrices 

424 ) 

425 self.definition_widget.show_all_button.clicked.connect(self.show_all_signals) 

426 self.definition_widget.show_none_button.clicked.connect(self.show_no_signals) 

427 self.definition_widget.control_channels_selector.itemChanged.connect( 

428 self.update_control_channels 

429 ) 

430 self.definition_widget.control_script_load_file_button.clicked.connect( 

431 self.select_python_module 

432 ) 

433 self.definition_widget.control_function_input.currentIndexChanged.connect( 

434 self.update_generator_selector 

435 ) 

436 self.definition_widget.check_selected_button.clicked.connect( 

437 self.check_selected_control_channels 

438 ) 

439 self.definition_widget.uncheck_selected_button.clicked.connect( 

440 self.uncheck_selected_control_channels 

441 ) 

442 # Prediction 

443 self.prediction_widget.excitation_selector.currentIndexChanged.connect( 

444 self.plot_predictions 

445 ) 

446 self.prediction_widget.response_selector.currentIndexChanged.connect(self.plot_predictions) 

447 self.prediction_widget.response_error_list.itemClicked.connect( 

448 self.update_response_error_prediction_selector 

449 ) 

450 self.prediction_widget.excitation_voltage_list.itemClicked.connect( 

451 self.update_excitation_prediction_selector 

452 ) 

453 self.prediction_widget.maximum_voltage_button.clicked.connect( 

454 self.show_max_voltage_prediction 

455 ) 

456 self.prediction_widget.minimum_voltage_button.clicked.connect( 

457 self.show_min_voltage_prediction 

458 ) 

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

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

461 self.prediction_widget.recompute_predictions_button.clicked.connect( 

462 self.recompute_predictions 

463 ) 

464 # Run Test 

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

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

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

468 self.run_widget.show_all_channels_button.clicked.connect(self.show_all_channels) 

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

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

471 self.run_widget.control_response_error_list.itemDoubleClicked.connect(self.show_window) 

472 self.run_widget.save_current_control_data_button.clicked.connect(self.save_control_data) 

473 self.run_widget.display_duration_spinbox.valueChanged.connect(self.set_display_duration) 

474 

475 # %% Data Acquisition 

476 

477 def initialize_data_acquisition(self, data_acquisition_parameters): 

478 super().initialize_data_acquisition(data_acquisition_parameters) 

479 # Initialize the plots 

480 for plot in [ 

481 self.definition_widget.signal_display_plot, 

482 self.prediction_widget.excitation_display_plot, 

483 self.prediction_widget.response_display_plot, 

484 self.run_widget.output_signal_plot, 

485 self.run_widget.response_signal_plot, 

486 ]: 

487 plot.getPlotItem().clear() 

488 

489 # Set up channel names 

490 self.physical_channel_names = [ 

491 ( 

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

493 f"{channel.node_number} " 

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

495 )[:MAXIMUM_NAME_LENGTH] 

496 for channel in data_acquisition_parameters.channel_list 

497 ] 

498 self.physical_output_indices = [ 

499 i 

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

501 if channel.feedback_device 

502 ] 

503 # Set up widgets 

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

505 self.system_id_widget.samplesPerFrameSpinBox.setValue( 

506 data_acquisition_parameters.sample_rate 

507 ) 

508 self.definition_widget.control_channels_selector.clear() 

509 for channel_name in self.physical_channel_names: 

510 item = QtWidgets.QListWidgetItem() 

511 item.setText(channel_name) 

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

513 item.setCheckState(Qt.Unchecked) 

514 self.definition_widget.control_channels_selector.addItem(item) 

515 self.response_transformation_matrix = None 

516 self.output_transformation_matrix = None 

517 self.define_transformation_matrices(None, False) 

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

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

520 self.definition_widget.control_channels_display.setValue(0) 

521 

522 @property 

523 def physical_output_names(self): 

524 """Names of the physical drive channels""" 

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

526 

527 # %% Environment 

528 

529 @property 

530 def physical_control_indices(self): 

531 """Indices of the control channels""" 

532 return [ 

533 i 

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

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

536 ] 

537 

538 @property 

539 def physical_control_names(self): 

540 """Names of the selected control channels""" 

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

542 

543 @property 

544 def initialized_control_names(self): 

545 """Names of the control channels that have been initialized""" 

546 if self.environment_parameters.response_transformation_matrix is None: 

547 return [ 

548 self.physical_channel_names[i] 

549 for i in self.environment_parameters.control_channel_indices 

550 ] 

551 else: 

552 return [ 

553 f"Transformed Response {i + 1}" 

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

555 ] 

556 

557 @property 

558 def initialized_output_names(self): 

559 """Names of the drive channels that have been initialized""" 

560 if self.environment_parameters.reference_transformation_matrix is None: 

561 return self.physical_output_names 

562 else: 

563 return [ 

564 f"Transformed Drive {i + 1}" 

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

566 ] 

567 

568 def update_control_channels(self): 

569 """Callback called when control channels are updated in the UI""" 

570 self.response_transformation_matrix = None 

571 self.output_transformation_matrix = None 

572 self.specification_signal = None 

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

574 self.define_transformation_matrices(None, False) 

575 self.show_signal() 

576 

577 def collect_environment_definition_parameters(self): 

578 """Collects the metadata defining the environment from the UI widgets""" 

579 if self.python_control_module is None: 

580 control_module = None 

581 control_function = None 

582 control_function_type = None 

583 control_function_parameters = None 

584 else: 

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

586 control_function = self.definition_widget.control_function_input.itemText( 

587 self.definition_widget.control_function_input.currentIndex() 

588 ) 

589 control_function_type = ( 

590 self.definition_widget.control_function_generator_selector.currentIndex() 

591 ) 

592 control_function_parameters = ( 

593 self.definition_widget.control_parameters_text_input.toPlainText() 

594 ) 

595 return TransientMetadata( 

596 len(self.data_acquisition_parameters.channel_list), 

597 self.definition_widget.sample_rate_display.value(), 

598 self.specification_signal, 

599 self.definition_widget.ramp_selector.value(), 

600 control_module, 

601 control_function, 

602 control_function_type, 

603 control_function_parameters, 

604 self.physical_control_indices, 

605 self.physical_output_indices, 

606 self.response_transformation_matrix, 

607 self.output_transformation_matrix, 

608 ) 

609 

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

611 """Loads a time signal using a dialog or the specified filename 

612 

613 Parameters 

614 ---------- 

615 clicked : 

616 The clicked event that triggered the callback. 

617 filename : 

618 File name defining the specification for bypassing the callback when 

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

620 

621 """ 

622 if filename is None: 

623 filename, _ = QtWidgets.QFileDialog.getOpenFileName( 

624 self.definition_widget, 

625 "Select Signal File", 

626 filter="Numpy or Mat (*.npy *.npz *.mat)", 

627 ) 

628 if filename == "": 

629 return 

630 self.definition_widget.signal_file_name_display.setText(filename) 

631 self.specification_signal = load_time_history( 

632 filename, self.definition_widget.sample_rate_display.value() 

633 ) 

634 self.setup_specification_table() 

635 self.show_signal() 

636 

637 def setup_specification_table(self): 

638 """Sets up the specification table for the Transient Environment 

639 

640 This function computes the RMS and max values for the signals and then 

641 creates entries in the table for each signal""" 

642 self.definition_widget.signal_samples_display.setValue(self.specification_signal.shape[-1]) 

643 self.definition_widget.signal_time_display.setValue( 

644 self.specification_signal.shape[-1] / self.definition_widget.sample_rate_display.value() 

645 ) 

646 maxs = np.max(np.abs(self.specification_signal), axis=-1) 

647 rmss = rms_time(self.specification_signal, axis=-1) 

648 # Add rows to the signal table 

649 self.definition_widget.signal_information_table.setRowCount( 

650 self.specification_signal.shape[0] 

651 ) 

652 self.show_signal_checkboxes = [] 

653 for i, (name, mx, rms) in enumerate(zip(self.physical_control_names, maxs, rmss)): 

654 item = QtWidgets.QTableWidgetItem() 

655 item.setText(name) 

656 item.setFlags(item.flags() ^ QtCore.Qt.ItemIsEditable) 

657 self.definition_widget.signal_information_table.setItem(i, 1, item) 

658 checkbox = QtWidgets.QCheckBox() 

659 checkbox.setChecked(True) 

660 checkbox.stateChanged.connect(self.show_signal) 

661 self.show_signal_checkboxes.append(checkbox) 

662 self.definition_widget.signal_information_table.setCellWidget(i, 0, checkbox) 

663 item = QtWidgets.QTableWidgetItem() 

664 item.setText(f"{mx:0.2f}") 

665 item.setFlags(item.flags() ^ QtCore.Qt.ItemIsEditable) 

666 self.definition_widget.signal_information_table.setItem(i, 2, item) 

667 item = QtWidgets.QTableWidgetItem() 

668 item.setText(f"{rms:0.2f}") 

669 item.setFlags(item.flags() ^ QtCore.Qt.ItemIsEditable) 

670 self.definition_widget.signal_information_table.setItem(i, 3, item) 

671 

672 def show_signal(self): 

673 """Shows the signal on the user interface""" 

674 pi = self.definition_widget.signal_display_plot.getPlotItem() 

675 pi.clear() 

676 if self.specification_signal is None: 

677 self.definition_widget.signal_information_table.setRowCount(0) 

678 return 

679 abscissa = ( 

680 np.arange(self.specification_signal.shape[-1]) 

681 / self.definition_widget.sample_rate_display.value() 

682 ) 

683 for i, (curve, checkbox) in enumerate( 

684 zip(self.specification_signal, self.show_signal_checkboxes) 

685 ): 

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

687 if checkbox.isChecked(): 

688 pi.plot(abscissa, curve, pen=pen) 

689 else: 

690 pi.plot((0, 0), (0, 0), pen=pen) 

691 

692 def show_all_signals(self): 

693 """Callback to show all signals in the specification""" 

694 # print('Showing All Signals') 

695 for checkbox in self.show_signal_checkboxes: 

696 checkbox.blockSignals(True) 

697 checkbox.setChecked(True) 

698 checkbox.blockSignals(False) 

699 self.show_signal() 

700 

701 def show_no_signals(self): 

702 """Callback to hide all signals in the specification""" 

703 # print('Showing No Signals') 

704 for checkbox in self.show_signal_checkboxes: 

705 checkbox.blockSignals(True) 

706 checkbox.setChecked(False) 

707 checkbox.blockSignals(False) 

708 self.show_signal() 

709 

710 def define_transformation_matrices( # pylint: disable=unused-argument 

711 self, clicked, dialog=True 

712 ): 

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

714 if dialog: 

715 (response_transformation, output_transformation, result) = ( 

716 TransformationMatrixWindow.define_transformation_matrices( 

717 self.response_transformation_matrix, 

718 self.definition_widget.control_channels_display.value(), 

719 self.output_transformation_matrix, 

720 self.definition_widget.output_channels_display.value(), 

721 self.definition_widget, 

722 ) 

723 ) 

724 else: 

725 response_transformation = self.response_transformation_matrix 

726 output_transformation = self.output_transformation_matrix 

727 result = True 

728 if result: 

729 # Update the control names 

730 for widget in self.control_selector_widgets: 

731 widget.blockSignals(True) 

732 widget.clear() 

733 if response_transformation is None: 

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

735 for widget in self.control_selector_widgets: 

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

737 self.definition_widget.transform_channels_display.setValue( 

738 len(self.physical_control_names) 

739 ) 

740 else: 

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

742 for widget in self.control_selector_widgets: 

743 widget.addItem(f"{i + 1}: Virtual Response") 

744 self.definition_widget.transform_channels_display.setValue( 

745 response_transformation.shape[0] 

746 ) 

747 for widget in self.control_selector_widgets: 

748 widget.blockSignals(False) 

749 # Update the output names 

750 for widget in self.output_selector_widgets: 

751 widget.blockSignals(True) 

752 widget.clear() 

753 if output_transformation is None: 

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

755 for widget in self.output_selector_widgets: 

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

757 self.definition_widget.transform_outputs_display.setValue( 

758 len(self.physical_output_names) 

759 ) 

760 else: 

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

762 for widget in self.output_selector_widgets: 

763 widget.addItem(f"{i + 1}: Virtual Drive") 

764 self.definition_widget.transform_outputs_display.setValue( 

765 output_transformation.shape[0] 

766 ) 

767 for widget in self.output_selector_widgets: 

768 widget.blockSignals(False) 

769 # Clear the signals 

770 self.definition_widget.signal_information_table.clear() 

771 self.definition_widget.signal_display_plot.clear() 

772 self.definition_widget.signal_file_name_display.clear() 

773 self.definition_widget.signal_information_table.setRowCount(0) 

774 self.show_signal_checkboxes = None 

775 self.response_transformation_matrix = response_transformation 

776 self.output_transformation_matrix = output_transformation 

777 

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

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

780 

781 Parameters 

782 ---------- 

783 clicked : 

784 The clicked event that triggered the callback. 

785 filename : 

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

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

788 

789 """ 

790 if filename is None or not os.path.isfile(filename): 

791 filename, _ = QtWidgets.QFileDialog.getOpenFileName( 

792 self.definition_widget, 

793 "Select Python Module", 

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

795 ) 

796 if filename == "": 

797 return 

798 self.python_control_module = load_python_module(filename) 

799 functions = [ 

800 function 

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

802 if ( 

803 inspect.isfunction(function[1]) 

804 and len(inspect.signature(function[1]).parameters) >= 6 

805 ) 

806 or inspect.isgeneratorfunction(function[1]) 

807 or ( 

808 inspect.isclass(function[1]) 

809 and all( 

810 [ 

811 ( 

812 method in function[1].__dict__ 

813 and not ( 

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

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

816 ) 

817 ) 

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

819 ] 

820 ) 

821 ) 

822 ] 

823 self.log( 

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

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

826 ) 

827 self.definition_widget.control_function_input.clear() 

828 self.definition_widget.control_script_file_path_input.setText(filename) 

829 for function in functions: 

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

831 

832 def update_generator_selector(self): 

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

834 if self.python_control_module is None: 

835 return 

836 try: 

837 function = getattr( 

838 self.python_control_module, 

839 self.definition_widget.control_function_input.itemText( 

840 self.definition_widget.control_function_input.currentIndex() 

841 ), 

842 ) 

843 except AttributeError: 

844 return 

845 if inspect.isgeneratorfunction(function): 

846 self.definition_widget.control_function_generator_selector.setCurrentIndex(1) 

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

848 self.definition_widget.control_function_generator_selector.setCurrentIndex(3) 

849 elif inspect.isclass(function): 

850 self.definition_widget.control_function_generator_selector.setCurrentIndex(2) 

851 else: 

852 self.definition_widget.control_function_generator_selector.setCurrentIndex(0) 

853 

854 def initialize_environment(self): 

855 super().initialize_environment() 

856 # Make sure everything is defined 

857 if self.environment_parameters.control_signal is None: 

858 raise ValueError(f"Control Signal is not defined for {self.environment_name}!") 

859 if self.environment_parameters.control_python_script is None: 

860 raise ValueError(f"Control function has not been loaded for {self.environment_name}") 

861 self.system_id_widget.samplesPerFrameSpinBox.setMaximum(self.specification_signal.shape[-1]) 

862 for widget in [ 

863 self.prediction_widget.response_selector, 

864 self.run_widget.control_channel_selector, 

865 ]: 

866 widget.blockSignals(True) 

867 widget.clear() 

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

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

870 widget.blockSignals(False) 

871 for widget in [self.prediction_widget.excitation_selector]: 

872 widget.blockSignals(True) 

873 widget.clear() 

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

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

876 widget.blockSignals(False) 

877 # Set up the prediction plots 

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

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

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

881 np.arange(self.environment_parameters.control_signal.shape[-1]) 

882 / self.environment_parameters.sample_rate, 

883 np.zeros((2, self.environment_parameters.control_signal.shape[-1])), 

884 widget=self.prediction_widget.response_display_plot, 

885 other_pen_options={"width": 1}, 

886 names=["Prediction", "Spec"], 

887 downsample={"auto": True}, 

888 clip_to_view=True, 

889 ) 

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

891 np.arange(self.environment_parameters.control_signal.shape[-1]) 

892 / self.environment_parameters.sample_rate, 

893 np.zeros((1, self.environment_parameters.control_signal.shape[-1])), 

894 widget=self.prediction_widget.excitation_display_plot, 

895 other_pen_options={"width": 1}, 

896 names=["Prediction"], 

897 downsample={"auto": True}, 

898 clip_to_view=True, 

899 ) 

900 # Set up the run plots 

901 self.run_widget.output_signal_plot.getPlotItem().clear() 

902 self.run_widget.response_signal_plot.getPlotItem().clear() 

903 self.max_plot_samples = ( 

904 self.data_acquisition_parameters.sample_rate 

905 * self.run_widget.display_duration_spinbox.value() 

906 ) 

907 self.plot_data_items["output_signal_measurement"] = multiline_plotter( 

908 (np.array([])), 

909 np.zeros((len(self.initialized_control_names), 0)), 

910 widget=self.run_widget.output_signal_plot, 

911 other_pen_options={"width": 1}, 

912 names=self.initialized_control_names, 

913 downsample={"auto": True}, 

914 clip_to_view=True, 

915 ) 

916 self.plot_data_items[ 

917 "signal_range" 

918 ] = self.run_widget.response_signal_plot.getPlotItem().plot( 

919 np.zeros(5), 

920 np.zeros(5), 

921 pen={"color": "k", "width": 1}, 

922 name="Signal Lower Bound", 

923 ) 

924 self.plot_data_items["control_signal_measurement"] = multiline_plotter( 

925 (np.array([])), 

926 np.zeros((len(self.initialized_output_names), 0)), 

927 widget=self.run_widget.response_signal_plot, 

928 other_pen_options={"width": 1}, 

929 names=self.initialized_output_names, 

930 downsample={"auto": True}, 

931 clip_to_view=True, 

932 ) 

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

934 control_class = getattr( 

935 self.python_control_module, 

936 self.definition_widget.control_function_input.itemText( 

937 self.definition_widget.control_function_input.currentIndex() 

938 ), 

939 ) 

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

941 ui_class = control_class.get_ui_class() 

942 if ui_class == self.interactive_control_law_widget.__class__: 

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

944 self.interactive_control_law_widget.initialize_parameters( 

945 self.data_acquisition_parameters, self.environment_parameters 

946 ) 

947 else: 

948 if self.interactive_control_law_widget is not None: 

949 self.interactive_control_law_widget.close() 

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

951 self.interactive_control_law_widget = ui_class( 

952 self.log_name, 

953 self.environment_command_queue, 

954 self.interactive_control_law_window, 

955 self, 

956 self.data_acquisition_parameters, 

957 self.environment_parameters, 

958 ) 

959 self.interactive_control_law_window.show() 

960 return self.environment_parameters 

961 

962 def check_selected_control_channels(self): 

963 """Callback to check control channels that are selected""" 

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

965 item.setCheckState(Qt.Checked) 

966 

967 def uncheck_selected_control_channels(self): 

968 """Callback to uncheck control channels that are selected""" 

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

970 item.setCheckState(Qt.Unchecked) 

971 

972 # %% Predictions 

973 def plot_predictions(self): 

974 """Plots the control predictions based on the currently selected item""" 

975 times = ( 

976 np.arange(self.specification_signal.shape[-1]) 

977 / self.data_acquisition_parameters.sample_rate 

978 ) 

979 index = self.prediction_widget.excitation_selector.currentIndex() 

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

981 times, self.excitation_prediction[index] 

982 ) 

983 index = self.prediction_widget.response_selector.currentIndex() 

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

985 times, self.response_prediction[index] 

986 ) 

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

988 times, self.specification_signal[index] 

989 ) 

990 

991 def show_max_voltage_prediction(self): 

992 """Callback to find and plot the time history showing the maximum drive voltage required""" 

993 widget = self.prediction_widget.excitation_voltage_list 

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

995 self.prediction_widget.excitation_selector.setCurrentIndex(index) 

996 

997 def show_min_voltage_prediction(self): 

998 """Callback to find and plot the time history showing the minimum drive voltage required""" 

999 widget = self.prediction_widget.excitation_voltage_list 

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

1001 self.prediction_widget.excitation_selector.setCurrentIndex(index) 

1002 

1003 def show_max_error_prediction(self): 

1004 """Callback to find and plot the time history with the largest error compared to spec""" 

1005 widget = self.prediction_widget.response_error_list 

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

1007 self.prediction_widget.response_selector.setCurrentIndex(index) 

1008 

1009 def show_min_error_prediction(self): 

1010 """Callback to find and plot the time history with the smallest error compared to spec""" 

1011 widget = self.prediction_widget.response_error_list 

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

1013 self.prediction_widget.response_selector.setCurrentIndex(index) 

1014 

1015 def update_response_error_prediction_selector(self, item): 

1016 """Callback to update the response prediction selector when an item is doubleclicked""" 

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

1018 self.prediction_widget.response_selector.setCurrentIndex(index) 

1019 

1020 def update_excitation_prediction_selector(self, item): 

1021 """Callback to update the drive predition selector when an item is doubleclicked""" 

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

1023 self.prediction_widget.excitation_selector.setCurrentIndex(index) 

1024 

1025 def recompute_predictions(self): 

1026 """Recomputes the control predictions""" 

1027 self.environment_command_queue.put( 

1028 self.log_name, (TransientCommands.PERFORM_CONTROL_PREDICTION, False) 

1029 ) 

1030 

1031 # %% Control 

1032 

1033 def start_control(self): 

1034 """Starts the chain of events to start the environment""" 

1035 self.enable_control(False) 

1036 self.controller_communication_queue.put( 

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

1038 ) 

1039 self.environment_command_queue.put( 

1040 self.log_name, 

1041 ( 

1042 TransientCommands.START_CONTROL, 

1043 ( 

1044 db2scale(self.run_widget.test_level_selector.value()), 

1045 self.run_widget.repeat_signal_checkbox.isChecked(), 

1046 ), 

1047 ), 

1048 ) 

1049 if self.run_widget.test_level_selector.value() >= 0: 

1050 self.controller_communication_queue.put( 

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

1052 ) 

1053 for item in self.plot_data_items["control_signal_measurement"]: 

1054 item.clear() 

1055 for item in self.plot_data_items["output_signal_measurement"]: 

1056 item.clear() 

1057 

1058 def stop_control(self): 

1059 """Starts the sequence of events to stop the controller prematurely""" 

1060 self.environment_command_queue.put(self.log_name, (TransientCommands.STOP_CONTROL, None)) 

1061 

1062 def enable_control(self, enabled): 

1063 """Enables or disables the buttons to start control if it's already running""" 

1064 for widget in [ 

1065 self.run_widget.test_level_selector, 

1066 self.run_widget.repeat_signal_checkbox, 

1067 self.run_widget.start_test_button, 

1068 ]: 

1069 widget.setEnabled(enabled) 

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

1071 widget.setEnabled(not enabled) 

1072 

1073 def change_test_level_from_profile(self, test_level): 

1074 """Updates the test level based on a profile event""" 

1075 self.run_widget.test_level_selector.setValue(int(test_level)) 

1076 

1077 def set_repeat_from_profile(self, data): # pylint: disable=unused-argument 

1078 """Sets whether or not to repeat the signal based on profile events""" 

1079 self.run_widget.repeat_signal_checkbox.setChecked(True) 

1080 

1081 def set_norepeat_from_profile(self, data): # pylint: disable=unused-argument 

1082 """Sets whether or not to repeat the signal based on profile events""" 

1083 self.run_widget.repeat_signal_checkbox.setChecked(False) 

1084 

1085 def set_display_duration(self, value): 

1086 """Updates the display duration in the UI""" 

1087 self.max_plot_samples = int(self.data_acquisition_parameters.sample_rate * value) 

1088 

1089 def create_window(self, event, control_index=None): # pylint: disable=unused-argument 

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

1091 

1092 Parameters 

1093 ---------- 

1094 event : 

1095 

1096 control_index : 

1097 Row index in the specification matrix to display (Default value = None) 

1098 

1099 """ 

1100 if control_index is None: 

1101 control_index = self.run_widget.control_channel_selector.currentIndex() 

1102 self.plot_windows.append( 

1103 PlotTimeWindow( 

1104 None, 

1105 control_index, 

1106 self.environment_parameters.control_signal, 

1107 self.data_acquisition_parameters.sample_rate, 

1108 self.run_widget.control_channel_selector.itemText(control_index), 

1109 ) 

1110 ) 

1111 if self.last_control_data is not None: 

1112 self.plot_windows[-1].update_plot(self.last_control_data) 

1113 

1114 def show_all_channels(self): 

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

1116 for i in range(self.environment_parameters.control_signal.shape[0]): 

1117 self.create_window(None, i) 

1118 self.tile_windows() 

1119 

1120 def tile_windows(self): 

1121 """Tile subwindow equally across the screen""" 

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

1123 # Go through and remove any closed windows 

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

1125 num_windows = len(self.plot_windows) 

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

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

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

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

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

1131 window.resize(window_width, window_height) 

1132 row_ind = index // ncols 

1133 col_ind = index % ncols 

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

1135 

1136 def show_window(self, item): 

1137 """Shows the currently selected control channel in a new subwindow""" 

1138 index = self.run_widget.control_response_error_list.row(item) 

1139 self.create_window(None, index) 

1140 

1141 def close_windows(self): 

1142 """Close all subwindows""" 

1143 for window in self.plot_windows: 

1144 window.close() 

1145 

1146 def update_control_plots(self): 

1147 """Updates plots in all of the existing subwindows""" 

1148 # Go through and remove any closed windows 

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

1150 for window in self.plot_windows: 

1151 window.update_plot(self.last_control_data) 

1152 

1153 def save_control_data(self): 

1154 """Save Time-aligned Control Data from the Controller""" 

1155 filename, _ = QtWidgets.QFileDialog.getSaveFileName( 

1156 self.definition_widget, 

1157 "Select File to Save Spectral Data", 

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

1159 ) 

1160 if filename == "": 

1161 return 

1162 labels = [ 

1163 ["node_number", str], 

1164 ["node_direction", str], 

1165 ["comment", str], 

1166 ["serial_number", str], 

1167 ["triax_dof", str], 

1168 ["sensitivity", str], 

1169 ["unit", str], 

1170 ["make", str], 

1171 ["model", str], 

1172 ["expiration", str], 

1173 ["physical_device", str], 

1174 ["physical_channel", str], 

1175 ["channel_type", str], 

1176 ["minimum_value", str], 

1177 ["maximum_value", str], 

1178 ["coupling", str], 

1179 ["excitation_source", str], 

1180 ["excitation", str], 

1181 ["feedback_device", str], 

1182 ["feedback_channel", str], 

1183 ["warning_level", str], 

1184 ["abort_level", str], 

1185 ] 

1186 global_data_parameters: DataAcquisitionParameters 

1187 global_data_parameters = self.data_acquisition_parameters 

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

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

1190 ) 

1191 # Create dimensions 

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

1193 netcdf_handle.createDimension( 

1194 "output_channels", 

1195 len( 

1196 [ 

1197 channel 

1198 for channel in global_data_parameters.channel_list 

1199 if channel.feedback_device is not None 

1200 ] 

1201 ), 

1202 ) 

1203 netcdf_handle.createDimension("time_samples", None) 

1204 netcdf_handle.createDimension( 

1205 "num_environments", len(global_data_parameters.environment_names) 

1206 ) 

1207 # Create attributes 

1208 netcdf_handle.file_version = "3.0.0" 

1209 netcdf_handle.sample_rate = global_data_parameters.sample_rate 

1210 netcdf_handle.time_per_write = ( 

1211 global_data_parameters.samples_per_write / global_data_parameters.output_sample_rate 

1212 ) 

1213 netcdf_handle.time_per_read = ( 

1214 global_data_parameters.samples_per_read / global_data_parameters.sample_rate 

1215 ) 

1216 netcdf_handle.hardware = global_data_parameters.hardware 

1217 netcdf_handle.hardware_file = ( 

1218 "None" 

1219 if global_data_parameters.hardware_file is None 

1220 else global_data_parameters.hardware_file 

1221 ) 

1222 netcdf_handle.output_oversample = global_data_parameters.output_oversample 

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

1224 setattr(netcdf_handle, key, value) 

1225 # Create Variables 

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

1227 this_environment_index = None 

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

1229 var[i] = name 

1230 if name == self.environment_name: 

1231 this_environment_index = i 

1232 var = netcdf_handle.createVariable( 

1233 "environment_active_channels", 

1234 "i1", 

1235 ("response_channels", "num_environments"), 

1236 ) 

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

1238 global_data_parameters.environment_active_channels[:, this_environment_index], 

1239 :, 

1240 ] 

1241 # Create channel table variables 

1242 

1243 for label, netcdf_datatype in labels: 

1244 var = netcdf_handle.createVariable( 

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

1246 ) 

1247 channel_data = [ 

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

1249 ] 

1250 if netcdf_datatype == "i1": 

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

1252 else: 

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

1254 for i, cd in enumerate(channel_data): 

1255 var[i] = cd 

1256 # Save the environment to the file 

1257 group_handle = netcdf_handle.createGroup(self.environment_name) 

1258 self.environment_parameters.store_to_netcdf(group_handle) 

1259 # Create Variables for Spectral Data 

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

1261 group_handle.createDimension( 

1262 "fft_lines", self.environment_parameters.sysid_frame_size // 2 + 1 

1263 ) 

1264 var = group_handle.createVariable( 

1265 "frf_data_real", 

1266 "f8", 

1267 ("fft_lines", "specification_channels", "drive_channels"), 

1268 ) 

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

1270 var = group_handle.createVariable( 

1271 "frf_data_imag", 

1272 "f8", 

1273 ("fft_lines", "specification_channels", "drive_channels"), 

1274 ) 

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

1276 var = group_handle.createVariable( 

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

1278 ) 

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

1280 var = group_handle.createVariable( 

1281 "response_cpsd_real", 

1282 "f8", 

1283 ("fft_lines", "specification_channels", "specification_channels"), 

1284 ) 

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

1286 var = group_handle.createVariable( 

1287 "response_cpsd_imag", 

1288 "f8", 

1289 ("fft_lines", "specification_channels", "specification_channels"), 

1290 ) 

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

1292 var = group_handle.createVariable( 

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

1294 ) 

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

1296 var = group_handle.createVariable( 

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

1298 ) 

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

1300 var = group_handle.createVariable( 

1301 "response_noise_cpsd_real", 

1302 "f8", 

1303 ("fft_lines", "specification_channels", "specification_channels"), 

1304 ) 

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

1306 var = group_handle.createVariable( 

1307 "response_noise_cpsd_imag", 

1308 "f8", 

1309 ("fft_lines", "specification_channels", "specification_channels"), 

1310 ) 

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

1312 var = group_handle.createVariable( 

1313 "drive_noise_cpsd_real", 

1314 "f8", 

1315 ("fft_lines", "drive_channels", "drive_channels"), 

1316 ) 

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

1318 var = group_handle.createVariable( 

1319 "drive_noise_cpsd_imag", 

1320 "f8", 

1321 ("fft_lines", "drive_channels", "drive_channels"), 

1322 ) 

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

1324 var = group_handle.createVariable( 

1325 "control_response", "f8", ("specification_channels", "signal_samples") 

1326 ) 

1327 var[...] = self.last_control_data 

1328 var = group_handle.createVariable( 

1329 "control_drives", "f8", ("drive_channels", "signal_samples") 

1330 ) 

1331 var[...] = self.last_output_data 

1332 netcdf_handle.close() 

1333 

1334 # %% Misc 

1335 

1336 def retrieve_metadata(self, netcdf_handle=None, environment_name=None): 

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

1338 

1339 # Control channels 

1340 try: 

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

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

1343 item.setCheckState(Qt.Checked) 

1344 except KeyError: 

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

1346 # Other Data 

1347 try: 

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

1349 ... 

1350 ].data 

1351 except KeyError: 

1352 self.response_transformation_matrix = None 

1353 try: 

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

1355 ... 

1356 ].data 

1357 except KeyError: 

1358 self.output_transformation_matrix = None 

1359 self.define_transformation_matrices(None, dialog=False) 

1360 

1361 if ( 

1362 environment_name is None 

1363 ): # environment_name is passed when the saved environment doesn't 

1364 # match the current environment 

1365 self.definition_widget.ramp_selector.setValue(group.test_level_ramp_time) 

1366 self.specification_signal = group.variables["control_signal"][...].data 

1367 self.select_python_module(None, group.control_python_script) 

1368 index = self.definition_widget.control_function_input.findText( 

1369 group.control_python_function 

1370 ) 

1371 if index == -1: 

1372 index = 0 

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

1374 print( 

1375 f'Warning: control function "{group.control_python_function}" ' 

1376 f'not found, defaulting to "{default}"' 

1377 ) 

1378 self.definition_widget.control_function_input.setCurrentIndex(index) 

1379 self.definition_widget.control_parameters_text_input.setText( 

1380 group.control_python_function_parameters 

1381 ) 

1382 self.setup_specification_table() 

1383 self.show_signal() 

1384 

1385 def update_gui(self, queue_data): 

1386 if super().update_gui(queue_data): 

1387 return 

1388 message, data = queue_data 

1389 if message == "time_data": 

1390 response_data, output_data, signal_delay = data 

1391 max_y = -1e15 

1392 min_y = 1e15 

1393 for curve, this_data in zip( 

1394 self.plot_data_items["control_signal_measurement"], response_data 

1395 ): 

1396 x, y = curve.getOriginalDataset() 

1397 if y is not None: 

1398 if np.max(y) > max_y: 

1399 max_y = np.max(y) 

1400 if np.min(y) < min_y: 

1401 min_y = np.min(y) 

1402 if self.max_plot_samples == x.size: 

1403 x += (this_data.size) / self.data_acquisition_parameters.sample_rate 

1404 y = np.roll(y, -this_data.size) 

1405 y[-this_data.size :] = this_data 

1406 else: 

1407 x = np.concatenate( 

1408 ( 

1409 x, 

1410 x[-1] 

1411 + ( 

1412 (1 + np.arange(this_data.size)) 

1413 / self.data_acquisition_parameters.sample_rate 

1414 ), 

1415 ), 

1416 axis=0, 

1417 ) 

1418 y = np.concatenate((y, this_data), axis=0) 

1419 else: 

1420 x = np.arange(this_data.size) / self.data_acquisition_parameters.sample_rate 

1421 y = this_data 

1422 curve.setData(x[-self.max_plot_samples :], y[-self.max_plot_samples :]) 

1423 # Display the data 

1424 for curve, this_output in zip( 

1425 self.plot_data_items["output_signal_measurement"], output_data 

1426 ): 

1427 x, y = curve.getOriginalDataset() 

1428 if y is not None: 

1429 if self.max_plot_samples == x.size: 

1430 x += (this_output.size) / self.data_acquisition_parameters.sample_rate 

1431 y = np.roll(y, -this_output.size) 

1432 y[-this_output.size :] = this_output 

1433 else: 

1434 x = np.concatenate( 

1435 ( 

1436 x, 

1437 x[-1] 

1438 + ( 

1439 (1 + np.arange(this_output.size)) 

1440 / self.data_acquisition_parameters.sample_rate 

1441 ), 

1442 ), 

1443 axis=0, 

1444 ) 

1445 y = np.concatenate((y, this_output), axis=0) 

1446 else: 

1447 x = np.arange(this_output.size) / self.data_acquisition_parameters.sample_rate 

1448 y = this_output 

1449 curve.setData(x[-self.max_plot_samples :], y[-self.max_plot_samples :]) 

1450 if signal_delay is None: 

1451 self.plot_data_items["signal_range"].setData(np.ones(5) * x[-1], np.zeros(5)) 

1452 elif message == "control_data": 

1453 self.last_control_data, self.last_output_data = data 

1454 self.update_control_plots() 

1455 max_y = np.max(self.last_control_data) 

1456 min_y = np.min(self.last_control_data) 

1457 for curve, this_data in zip( 

1458 self.plot_data_items["control_signal_measurement"], 

1459 self.last_control_data, 

1460 ): 

1461 x, y = curve.getOriginalDataset() 

1462 x = np.arange(this_data.size) / self.data_acquisition_parameters.sample_rate 

1463 y = this_data 

1464 curve.setData(x, y) 

1465 # Display the data 

1466 for curve, this_output in zip( 

1467 self.plot_data_items["output_signal_measurement"], self.last_output_data 

1468 ): 

1469 x, y = curve.getOriginalDataset() 

1470 x = np.arange(this_output.size) / self.data_acquisition_parameters.sample_rate 

1471 y = this_output 

1472 curve.setData(x, y) 

1473 sr = self.data_acquisition_parameters.sample_rate 

1474 self.plot_data_items["signal_range"].setData( 

1475 np.array( 

1476 ( 

1477 0, 

1478 0, 

1479 (self.environment_parameters.control_signal.shape[-1] - 1) / sr, 

1480 (self.environment_parameters.control_signal.shape[-1] - 1) / sr, 

1481 0, 

1482 ) 

1483 ), 

1484 1.05 * np.array((min_y, max_y, max_y, min_y, min_y)), 

1485 ) 

1486 elif message == "control_predictions": 

1487 ( 

1488 _, # times, 

1489 self.excitation_prediction, 

1490 self.response_prediction, 

1491 _, # prediction, 

1492 ) = data 

1493 self.plot_predictions() 

1494 elif message == "interactive_control_sysid_update": 

1495 if self.interactive_control_law_widget is not None: 

1496 self.interactive_control_law_widget.update_ui_sysid(*data) 

1497 elif message == "interactive_control_update": 

1498 if self.interactive_control_law_widget is not None: 

1499 self.interactive_control_law_widget.update_ui_control(data) 

1500 elif message == "enable_control": 

1501 self.enable_control(True) 

1502 elif message == "enable": 

1503 widget = None 

1504 for parent in [ 

1505 self.definition_widget, 

1506 self.run_widget, 

1507 self.system_id_widget, 

1508 self.prediction_widget, 

1509 ]: 

1510 try: 

1511 widget = getattr(parent, data) 

1512 break 

1513 except AttributeError: 

1514 continue 

1515 if widget is None: 

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

1517 widget.setEnabled(True) 

1518 elif message == "disable": 

1519 widget = None 

1520 for parent in [ 

1521 self.definition_widget, 

1522 self.run_widget, 

1523 self.system_id_widget, 

1524 self.prediction_widget, 

1525 ]: 

1526 try: 

1527 widget = getattr(parent, data) 

1528 break 

1529 except AttributeError: 

1530 continue 

1531 if widget is None: 

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

1533 widget.setEnabled(False) 

1534 else: 

1535 widget = None 

1536 for parent in [ 

1537 self.definition_widget, 

1538 self.run_widget, 

1539 self.system_id_widget, 

1540 self.prediction_widget, 

1541 ]: 

1542 try: 

1543 widget = getattr(parent, message) 

1544 break 

1545 except AttributeError: 

1546 continue 

1547 if widget is None: 

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

1549 if isinstance(widget, QtWidgets.QDoubleSpinBox): 

1550 widget.setValue(data) 

1551 elif isinstance(widget, QtWidgets.QSpinBox): 

1552 widget.setValue(data) 

1553 elif isinstance(widget, QtWidgets.QLineEdit): 

1554 widget.setText(data) 

1555 elif isinstance(widget, QtWidgets.QListWidget): 

1556 widget.clear() 

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

1558 

1559 def set_parameters_from_template(self, worksheet): 

1560 self.definition_widget.ramp_selector.setValue(float(worksheet.cell(3, 2).value)) 

1561 self.select_python_module(None, worksheet.cell(4, 2).value) 

1562 self.definition_widget.control_function_input.setCurrentIndex( 

1563 self.definition_widget.control_function_input.findText(worksheet.cell(5, 2).value) 

1564 ) 

1565 self.definition_widget.control_parameters_text_input.setText( 

1566 "" if worksheet.cell(6, 2).value is None else str(worksheet.cell(6, 2).value) 

1567 ) 

1568 column_index = 2 

1569 while True: 

1570 value = worksheet.cell(7, column_index).value 

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

1572 break 

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

1574 item.setCheckState(Qt.Checked) 

1575 column_index += 1 

1576 self.system_id_widget.samplesPerFrameSpinBox.setValue(int(worksheet.cell(8, 2).value)) 

1577 self.system_id_widget.averagingTypeComboBox.setCurrentIndex( 

1578 self.system_id_widget.averagingTypeComboBox.findText(worksheet.cell(9, 2).value) 

1579 ) 

1580 self.system_id_widget.noiseAveragesSpinBox.setValue(int(worksheet.cell(10, 2).value)) 

1581 self.system_id_widget.systemIDAveragesSpinBox.setValue(int(worksheet.cell(11, 2).value)) 

1582 self.system_id_widget.averagingCoefficientDoubleSpinBox.setValue( 

1583 float(worksheet.cell(12, 2).value) 

1584 ) 

1585 self.system_id_widget.estimatorComboBox.setCurrentIndex( 

1586 self.system_id_widget.estimatorComboBox.findText(worksheet.cell(13, 2).value) 

1587 ) 

1588 self.system_id_widget.levelDoubleSpinBox.setValue(float(worksheet.cell(14, 2).value)) 

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

1590 low, high = worksheet.cell(14, 3).value, worksheet.cell(14, 4).value 

1591 if low is not None: 

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

1593 if high is not None: 

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

1595 self.system_id_widget.levelRampTimeDoubleSpinBox.setValue( 

1596 float(worksheet.cell(15, 2).value) 

1597 ) 

1598 self.system_id_widget.signalTypeComboBox.setCurrentIndex( 

1599 self.system_id_widget.signalTypeComboBox.findText(worksheet.cell(16, 2).value) 

1600 ) 

1601 self.system_id_widget.windowComboBox.setCurrentIndex( 

1602 self.system_id_widget.windowComboBox.findText(worksheet.cell(17, 2).value) 

1603 ) 

1604 self.system_id_widget.overlapDoubleSpinBox.setValue(float(worksheet.cell(18, 2).value)) 

1605 self.system_id_widget.onFractionDoubleSpinBox.setValue(float(worksheet.cell(19, 2).value)) 

1606 self.system_id_widget.pretriggerDoubleSpinBox.setValue(float(worksheet.cell(20, 2).value)) 

1607 self.system_id_widget.rampFractionDoubleSpinBox.setValue(float(worksheet.cell(21, 2).value)) 

1608 

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

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

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

1612 output_transform_row = 23 

1613 if ( 

1614 isinstance(worksheet.cell(22, 2).value, str) 

1615 and worksheet.cell(22, 2).value.lower() == "none" 

1616 ): 

1617 self.response_transformation_matrix = None 

1618 else: 

1619 while True: 

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

1621 break 

1622 output_transform_row += 1 

1623 response_size = output_transform_row - 22 

1624 response_transformation = [] 

1625 for i in range(response_size): 

1626 response_transformation.append([]) 

1627 for j in range(response_channels): 

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

1629 self.response_transformation_matrix = np.array(response_transformation) 

1630 if ( 

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

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

1633 ): 

1634 self.output_transformation_matrix = None 

1635 else: 

1636 output_transformation = [] 

1637 i = 0 

1638 while True: 

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

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

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

1642 ): 

1643 break 

1644 output_transformation.append([]) 

1645 for j in range(output_channels): 

1646 output_transformation[-1].append( 

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

1648 ) 

1649 i += 1 

1650 self.output_transformation_matrix = np.array(output_transformation) 

1651 self.define_transformation_matrices(None, dialog=False) 

1652 self.load_signal(None, worksheet.cell(2, 2).value) 

1653 

1654 @staticmethod 

1655 def create_environment_template(environment_name, workbook): 

1656 worksheet = workbook.create_sheet(environment_name) 

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

1658 worksheet.cell(1, 2, "Transient") 

1659 worksheet.cell( 

1660 1, 

1661 4, 

1662 "Note: Replace cells with hash marks (#) to provide the requested parameters.", 

1663 ) 

1664 worksheet.cell(2, 1, "Signal File") 

1665 worksheet.cell(2, 2, "# Path to the file that contains the time signal that will be output") 

1666 worksheet.cell(3, 1, "Ramp Time") 

1667 worksheet.cell( 

1668 3, 

1669 2, 

1670 "# Time for the environment to ramp between levels or from start or to stop.", 

1671 ) 

1672 worksheet.cell(4, 1, "Control Python Script:") 

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

1674 worksheet.cell(5, 1, "Control Python Function:") 

1675 worksheet.cell( 

1676 5, 

1677 2, 

1678 "# Function name within the Python Script that will serve as the control law", 

1679 ) 

1680 worksheet.cell(6, 1, "Control Parameters:") 

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

1682 worksheet.cell(7, 1, "Control Channels (1-based):") 

1683 worksheet.cell(7, 2, "# List of channels, one per cell on this row") 

1684 worksheet.cell(8, 1, "System ID Samples per Frame") 

1685 worksheet.cell( 

1686 8, 

1687 2, 

1688 "# Number of Samples per Measurement Frame in the System Identification", 

1689 ) 

1690 worksheet.cell(9, 1, "System ID Averaging:") 

1691 worksheet.cell(9, 2, "# Averaging Type, should be Linear or Exponential") 

1692 worksheet.cell(10, 1, "Noise Averages:") 

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

1694 worksheet.cell(11, 1, "System ID Averages:") 

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

1696 worksheet.cell(12, 1, "Exponential Averaging Coefficient:") 

1697 worksheet.cell(12, 2, "# Averaging Coefficient for Exponential Averaging (if used)") 

1698 worksheet.cell(13, 1, "System ID Estimator:") 

1699 worksheet.cell( 

1700 13, 

1701 2, 

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

1703 ) 

1704 worksheet.cell(14, 1, "System ID Level (V RMS):") 

1705 worksheet.cell( 

1706 14, 

1707 2, 

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

1709 ) 

1710 worksheet.cell(15, 1, "System ID Ramp Time") 

1711 worksheet.cell( 

1712 15, 

1713 2, 

1714 "# Time for the system identification to ramp between levels or from start or to stop.", 

1715 ) 

1716 worksheet.cell(16, 1, "System ID Signal Type:") 

1717 worksheet.cell(16, 2, "# Signal to use for the system identification") 

1718 worksheet.cell(17, 1, "System ID Window:") 

1719 worksheet.cell( 

1720 17, 

1721 2, 

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

1723 ) 

1724 worksheet.cell(18, 1, "System ID Overlap %:") 

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

1726 worksheet.cell(19, 1, "System ID Burst On %:") 

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

1728 worksheet.cell(20, 1, "System ID Burst Pretrigger %:") 

1729 worksheet.cell( 

1730 20, 

1731 2, 

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

1733 ) 

1734 worksheet.cell(21, 1, "System ID Ramp Fraction %:") 

1735 worksheet.cell( 

1736 21, 

1737 2, 

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

1739 ) 

1740 worksheet.cell(22, 1, "Response Transformation Matrix:") 

1741 worksheet.cell( 

1742 22, 

1743 2, 

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

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

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

1747 "the number of physical control channels.", 

1748 ) 

1749 worksheet.cell(23, 1, "Output Transformation Matrix:") 

1750 worksheet.cell( 

1751 23, 

1752 2, 

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

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

1755 "be the number of physical output channels in the environment.", 

1756 ) 

1757 

1758 

1759# %% Environment 

1760 

1761 

1762class TransientEnvironment(AbstractSysIdEnvironment): 

1763 """Class defining calculations for the transient environment""" 

1764 

1765 def __init__( 

1766 self, 

1767 environment_name: str, 

1768 queue_container: TransientQueues, 

1769 acquisition_active: mp.sharedctypes.Synchronized, 

1770 output_active: mp.sharedctypes.Synchronized, 

1771 ): 

1772 super().__init__( 

1773 environment_name, 

1774 queue_container.environment_command_queue, 

1775 queue_container.gui_update_queue, 

1776 queue_container.controller_communication_queue, 

1777 queue_container.log_file_queue, 

1778 queue_container.collector_command_queue, 

1779 queue_container.signal_generation_command_queue, 

1780 queue_container.spectral_command_queue, 

1781 queue_container.data_analysis_command_queue, 

1782 queue_container.data_in_queue, 

1783 queue_container.data_out_queue, 

1784 acquisition_active, 

1785 output_active, 

1786 ) 

1787 self.map_command( 

1788 TransientCommands.PERFORM_CONTROL_PREDICTION, 

1789 self.perform_control_prediction, 

1790 ) 

1791 self.map_command(TransientCommands.START_CONTROL, self.start_control) 

1792 self.map_command(TransientCommands.STOP_CONTROL, self.stop_environment) 

1793 self.map_command( 

1794 GlobalCommands.UPDATE_INTERACTIVE_CONTROL_PARAMETERS, 

1795 self.update_interactive_control_parameters, 

1796 ) 

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

1798 # Persistent data 

1799 self.data_acquisition_parameters = None 

1800 self.environment_parameters = None 

1801 self.queue_container = queue_container 

1802 self.frames = None 

1803 self.frequencies = None 

1804 self.frf = None 

1805 self.sysid_coherence = None 

1806 self.sysid_response_cpsd = None 

1807 self.sysid_reference_cpsd = None 

1808 self.sysid_condition = None 

1809 self.sysid_response_noise = None 

1810 self.sysid_reference_noise = None 

1811 self.control_function_type = None 

1812 self.extra_control_parameters = None 

1813 self.control_function = None 

1814 self.aligned_output = None 

1815 self.aligned_response = None 

1816 self.next_drive = None 

1817 self.predicted_response = None 

1818 self.startup = True 

1819 self.shutdown_flag = False 

1820 self.repeat = False 

1821 self.test_level = 0 

1822 self.control_buffer = None 

1823 self.output_buffer = None 

1824 self.last_signal_found = None 

1825 self.has_sent_interactive_control_transfer_function_results = False 

1826 self.last_interactive_parameters = None 

1827 

1828 def initialize_environment_test_parameters(self, environment_parameters: TransientMetadata): 

1829 if ( 

1830 self.environment_parameters is None 

1831 or self.environment_parameters.control_signal.shape 

1832 != environment_parameters.control_signal.shape 

1833 ): 

1834 self.frames = None 

1835 self.frequencies = None 

1836 self.frf = None 

1837 self.sysid_coherence = None 

1838 self.sysid_response_cpsd = None 

1839 self.sysid_reference_cpsd = None 

1840 self.sysid_condition = None 

1841 self.sysid_response_noise = None 

1842 self.sysid_reference_noise = None 

1843 self.control_function_type = None 

1844 self.extra_control_parameters = None 

1845 self.control_function = None 

1846 self.aligned_output = None 

1847 self.aligned_response = None 

1848 self.next_drive = None 

1849 self.predicted_response = None 

1850 super().initialize_environment_test_parameters(environment_parameters) 

1851 self.environment_parameters: TransientMetadata 

1852 # Load in the control law 

1853 _, file = os.path.split(environment_parameters.control_python_script) 

1854 file, _ = os.path.splitext(file) 

1855 spec = importlib.util.spec_from_file_location( 

1856 file, environment_parameters.control_python_script 

1857 ) 

1858 module = importlib.util.module_from_spec(spec) 

1859 spec.loader.exec_module(module) 

1860 self.control_function_type = environment_parameters.control_python_function_type 

1861 self.extra_control_parameters = environment_parameters.control_python_function_parameters 

1862 if self.control_function_type == 1: # Generator 

1863 # Get the generator function 

1864 generator_function = getattr(module, environment_parameters.control_python_function)() 

1865 # Get us to the first yield statement 

1866 next(generator_function) 

1867 # Define the control function as the generator's send function 

1868 self.control_function = generator_function.send 

1869 elif self.control_function_type == 2: # Class 

1870 self.control_function = getattr(module, environment_parameters.control_python_function)( 

1871 self.data_acquisition_parameters.sample_rate, 

1872 self.environment_parameters.control_signal, 

1873 self.data_acquisition_parameters.output_oversample, 

1874 self.extra_control_parameters, # Required parameters 

1875 self.environment_parameters.sysid_frequency_spacing, # Frequency Spacing 

1876 self.frf, # Transfer Functions 

1877 self.sysid_response_noise, # Noise levels and correlation 

1878 self.sysid_reference_noise, # from the system identification 

1879 self.sysid_response_cpsd, # Response levels and correlation 

1880 self.sysid_reference_cpsd, # from the system identification 

1881 self.sysid_coherence, # Coherence from the system identification 

1882 self.frames, # Number of frames in the CPSD and FRF matrices 

1883 self.environment_parameters.sysid_averages, # Total frames that 

1884 # could be in the CPSD and FRF matrices 

1885 self.aligned_output, # Last excitation signal for drive-based control 

1886 self.aligned_response, 

1887 ) # Last response signal for error-based correction 

1888 elif self.control_function_type == 3: # Interactive Class 

1889 control_class = getattr(module, environment_parameters.control_python_function) 

1890 self.control_function = control_class( 

1891 self.environment_name, 

1892 self.gui_update_queue, 

1893 self.data_acquisition_parameters.sample_rate, 

1894 self.environment_parameters.control_signal, 

1895 self.data_acquisition_parameters.output_oversample, 

1896 self.extra_control_parameters, # Required parameters 

1897 self.environment_parameters.sysid_frequency_spacing, # Frequency Spacing 

1898 self.frf, # Transfer Functions 

1899 self.sysid_response_noise, # Noise levels and correlation 

1900 self.sysid_reference_noise, # from the system identification 

1901 self.sysid_response_cpsd, # Response levels and correlation 

1902 self.sysid_reference_cpsd, # from the system identification 

1903 self.sysid_coherence, # Coherence from the system identification 

1904 self.frames, # Number of frames in the CPSD and FRF matrices 

1905 self.environment_parameters.sysid_averages, # Total frames tha 

1906 # could be in the CPSD and FRF matrices 

1907 self.aligned_output, # Last excitation signal for drive-based control 

1908 self.aligned_response, 

1909 ) # Last response signal for error-based correction 

1910 self.last_interactive_parameters = None 

1911 self.has_sent_interactive_control_transfer_function_results = False 

1912 else: # Function 

1913 self.control_function = getattr(module, environment_parameters.control_python_function) 

1914 

1915 def update_interactive_control_parameters(self, interactive_control_parameters): 

1916 """Updates the interactive control law based on received parameters""" 

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

1918 self.control_function.update_parameters(interactive_control_parameters) 

1919 self.last_interactive_parameters = interactive_control_parameters 

1920 else: 

1921 raise ValueError( 

1922 "Received an UPDATE_INTERACTIVE_CONTROL_PARAMETERS signal without an " 

1923 "interactive control law. How did this happen?" 

1924 ) 

1925 

1926 def send_interactive_command(self, command): 

1927 """General method that can be used by an interactive UI object to pass commands 

1928 and data to its corresponding computation object""" 

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

1930 self.control_function.send_command(command) 

1931 else: 

1932 raise ValueError( 

1933 "Received an SEND_INTERACTIVE_COMMAND signal without an interactive " 

1934 "control law. How did this happen?" 

1935 ) 

1936 

1937 def system_id_complete(self, data): 

1938 """Sends the message that system identification is complete and control calculations 

1939 should be performed""" 

1940 super().system_id_complete(data) 

1941 ( 

1942 self.frames, 

1943 _, # avg, 

1944 self.frequencies, 

1945 self.frf, 

1946 self.sysid_coherence, 

1947 self.sysid_response_cpsd, 

1948 self.sysid_reference_cpsd, 

1949 self.sysid_condition, 

1950 self.sysid_response_noise, 

1951 self.sysid_reference_noise, 

1952 ) = data 

1953 # Perform the control prediction 

1954 self.perform_control_prediction(True) 

1955 

1956 def perform_control_prediction(self, sysid_update): 

1957 """Performs the control prediction based on system identification information""" 

1958 if self.frf is None: 

1959 self.gui_update_queue.put( 

1960 ( 

1961 "error", 

1962 ( 

1963 "Perform System Identification", 

1964 "Perform System ID before performing test predictions", 

1965 ), 

1966 ) 

1967 ) 

1968 return 

1969 if self.control_function_type == 1: # Generator 

1970 output_time_history = self.control_function( 

1971 ( 

1972 self.data_acquisition_parameters.sample_rate, 

1973 self.environment_parameters.control_signal, 

1974 self.environment_parameters.sysid_frequency_spacing, 

1975 self.frf, # Transfer Functions 

1976 self.sysid_response_noise, # Noise levels and correlation 

1977 self.sysid_reference_noise, # from the system identification 

1978 self.sysid_response_cpsd, # Response levels and correlation 

1979 self.sysid_reference_cpsd, # from the system identification 

1980 self.sysid_coherence, # Coherence from the system identification 

1981 self.frames, # Number of frames in the CPSD and FRF matrices 

1982 self.environment_parameters.sysid_averages, # Total frames that could be in 

1983 # the CPSD and FRF matrices 

1984 self.data_acquisition_parameters.output_oversample, 

1985 self.extra_control_parameters, # Required parameters 

1986 self.next_drive, # Last excitation signal for drive-based control 

1987 self.predicted_response, # Last response signal for error correction 

1988 ) 

1989 ) 

1990 elif self.control_function_type in [2, 3]: # Class or Interactive Class 

1991 if ( 

1992 self.environment_parameters.control_python_function == 2 

1993 or not self.has_sent_interactive_control_transfer_function_results 

1994 ): 

1995 if sysid_update: 

1996 self.control_function.system_id_update( 

1997 self.environment_parameters.sysid_frequency_spacing, 

1998 self.frf, # Transfer Functions 

1999 self.sysid_response_noise, # Noise levels and correlation 

2000 self.sysid_reference_noise, # from the system identification 

2001 self.sysid_response_cpsd, # Response levels and correlation 

2002 self.sysid_reference_cpsd, # from the system identification 

2003 self.sysid_coherence, # Coherence from the system identification 

2004 self.frames, # Number of frames in the CPSD and FRF matrices 

2005 self.environment_parameters.sysid_averages, # Total frames that 

2006 # could be in the CPSD and FRF matrices 

2007 ) 

2008 

2009 if self.environment_parameters.control_python_function_type == 3: 

2010 self.gui_update_queue.put( 

2011 ( 

2012 self.environment_name, 

2013 ( 

2014 "interactive_control_sysid_update", 

2015 ( 

2016 self.frf, 

2017 self.sysid_response_noise, 

2018 self.sysid_reference_noise, 

2019 self.sysid_response_cpsd, 

2020 self.sysid_reference_cpsd, 

2021 self.sysid_coherence, 

2022 ), 

2023 ), 

2024 ) 

2025 ) 

2026 self.has_sent_interactive_control_transfer_function_results = True 

2027 if ( 

2028 self.environment_parameters.control_python_function_type == 2 

2029 or self.last_interactive_parameters is not None 

2030 ): 

2031 output_time_history = self.control_function.control( 

2032 self.next_drive, self.predicted_response 

2033 ) 

2034 else: 

2035 self.log("Have not yet received control parameters from interactive control law!") 

2036 output_time_history = None 

2037 return 

2038 else: # Function 

2039 output_time_history = self.control_function( 

2040 self.data_acquisition_parameters.sample_rate, 

2041 self.environment_parameters.control_signal, 

2042 self.environment_parameters.sysid_frequency_spacing, 

2043 self.frf, # Transfer Functions 

2044 self.sysid_response_noise, # Noise levels and correlation 

2045 self.sysid_reference_noise, # from the system identification 

2046 self.sysid_response_cpsd, # Response levels and correlation 

2047 self.sysid_reference_cpsd, # from the system identification 

2048 self.sysid_coherence, # Coherence from the system identification 

2049 self.frames, # Number of frames in the CPSD and FRF matrices 

2050 self.environment_parameters.sysid_averages, # Total frames that could 

2051 # be in the CPSD and FRF matrices 

2052 self.data_acquisition_parameters.output_oversample, 

2053 self.extra_control_parameters, # Required parameters 

2054 self.next_drive, # Last excitation signal for drive-based control 

2055 self.predicted_response, # Last response signal for error correction 

2056 ) 

2057 self.next_drive = output_time_history 

2058 self.show_test_prediction() 

2059 

2060 def show_test_prediction(self): 

2061 """Sends the test predictions to the UI""" 

2062 # print('Drive Signals {:}'.format(self.next_drive.shape)) 

2063 drive_signals = self.next_drive[:, :: self.data_acquisition_parameters.output_oversample] 

2064 impulse_responses = np.moveaxis(np.fft.irfft(self.frf, axis=0), 0, -1) 

2065 

2066 self.predicted_response = np.zeros((impulse_responses.shape[0], drive_signals.shape[-1])) 

2067 

2068 for i, impulse_response_row in enumerate(impulse_responses): 

2069 for _, (impulse, drive) in enumerate(zip(impulse_response_row, drive_signals)): 

2070 # print('Convolving {:},{:}'.format(i,j)) 

2071 self.predicted_response[i, :] += sig.convolve(drive, impulse, "full")[ 

2072 : drive_signals.shape[-1] 

2073 ] 

2074 

2075 # print('Response Prediction {:}'.format(self.predicted_response.shape)) 

2076 # print('Control Signal {:}'.format(self.environment_parameters.control_signal.shape)) 

2077 time_trac = trac(self.predicted_response, self.environment_parameters.control_signal) 

2078 peak_voltages = np.max(np.abs(self.next_drive), axis=-1) 

2079 self.gui_update_queue.put( 

2080 (self.environment_name, ("excitation_voltage_list", peak_voltages)) 

2081 ) 

2082 self.gui_update_queue.put((self.environment_name, ("response_error_list", time_trac))) 

2083 self.gui_update_queue.put( 

2084 ( 

2085 self.environment_name, 

2086 ( 

2087 "control_predictions", 

2088 ( 

2089 np.arange(self.environment_parameters.control_signal.shape[-1]) 

2090 / self.data_acquisition_parameters.sample_rate, 

2091 drive_signals, 

2092 self.predicted_response, 

2093 self.environment_parameters.control_signal, 

2094 ), 

2095 ), 

2096 ) 

2097 ) 

2098 

2099 def get_signal_generation_metadata(self): 

2100 """Collects the metadata required to define the signal generation process""" 

2101 return SignalGenerationMetadata( 

2102 samples_per_write=self.data_acquisition_parameters.samples_per_write, 

2103 level_ramp_samples=self.environment_parameters.test_level_ramp_time 

2104 * self.environment_parameters.sample_rate 

2105 * self.data_acquisition_parameters.output_oversample, 

2106 output_transformation_matrix=self.environment_parameters.reference_transformation_matrix, 

2107 ) 

2108 

2109 def start_control(self, data): 

2110 """Starts up the control to generate the signal""" 

2111 if self.startup: 

2112 self.test_level, self.repeat = data 

2113 self.log("Starting Environment") 

2114 self.siggen_shutdown_achieved = False 

2115 # Set up the signal generation 

2116 self.queue_container.signal_generation_command_queue.put( 

2117 self.environment_name, 

2118 ( 

2119 SignalGenerationCommands.INITIALIZE_PARAMETERS, 

2120 self.get_signal_generation_metadata(), 

2121 ), 

2122 ) 

2123 self.queue_container.signal_generation_command_queue.put( 

2124 self.environment_name, 

2125 ( 

2126 SignalGenerationCommands.INITIALIZE_SIGNAL_GENERATOR, 

2127 TransientSignalGenerator(self.next_drive, self.repeat), 

2128 ), 

2129 ) 

2130 self.queue_container.signal_generation_command_queue.put( 

2131 self.environment_name, 

2132 (SignalGenerationCommands.SET_TEST_LEVEL, self.test_level), 

2133 ) 

2134 # Tell the signal generation to start generating signals 

2135 self.queue_container.signal_generation_command_queue.put( 

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

2137 ) 

2138 # Set up the measurement buffers 

2139 n_control_channels = ( 

2140 len(self.environment_parameters.control_channel_indices) 

2141 if self.environment_parameters.response_transformation_matrix is None 

2142 else self.environment_parameters.response_transformation_matrix.shape[0] 

2143 ) 

2144 n_output_channels = ( 

2145 len(self.environment_parameters.output_channel_indices) 

2146 if self.environment_parameters.reference_transformation_matrix is None 

2147 else self.environment_parameters.reference_transformation_matrix.shape[0] 

2148 ) 

2149 self.control_buffer = FrameBuffer( 

2150 n_control_channels, 

2151 0, 

2152 0, 

2153 False, 

2154 0, 

2155 0, 

2156 0, 

2157 self.environment_parameters.control_signal.shape[-1], 

2158 0, 

2159 False, 

2160 False, 

2161 False, 

2162 0, 

2163 buffer_size_frame_multiplier=1 

2164 + ( 

2165 self.data_acquisition_parameters.samples_per_read 

2166 * BUFFER_SIZE_SAMPLES_PER_READ_MULTIPLIER 

2167 / self.environment_parameters.control_signal.shape[-1] 

2168 ), 

2169 starting_value=0.0, 

2170 ) 

2171 self.output_buffer = FrameBuffer( 

2172 n_output_channels, 

2173 0, 

2174 0, 

2175 False, 

2176 0, 

2177 0, 

2178 0, 

2179 self.environment_parameters.control_signal.shape[-1], 

2180 0, 

2181 False, 

2182 False, 

2183 False, 

2184 0, 

2185 buffer_size_frame_multiplier=1 

2186 + ( 

2187 self.data_acquisition_parameters.samples_per_read 

2188 * BUFFER_SIZE_SAMPLES_PER_READ_MULTIPLIER 

2189 / self.environment_parameters.control_signal.shape[-1] 

2190 ), 

2191 starting_value=0.0, 

2192 ) 

2193 self.startup = False 

2194 # See if any data has come in 

2195 try: 

2196 acquisition_data, last_acquisition = self.queue_container.data_in_queue.get_nowait() 

2197 if self.last_signal_found is not None: 

2198 self.last_signal_found -= self.data_acquisition_parameters.samples_per_read 

2199 if last_acquisition: 

2200 self.log( 

2201 f"Acquired Last Data, Signal Generation " 

2202 f"Shutdown Achieved: {self.siggen_shutdown_achieved}" 

2203 ) 

2204 else: 

2205 self.log("Acquired Data") 

2206 scale_factor = 0.0 if self.test_level < 1e-10 else 1 / self.test_level 

2207 control_data = ( 

2208 acquisition_data[self.environment_parameters.control_channel_indices] * scale_factor 

2209 ) 

2210 if self.environment_parameters.response_transformation_matrix is not None: 

2211 control_data = ( 

2212 self.environment_parameters.response_transformation_matrix @ control_data 

2213 ) 

2214 output_data = ( 

2215 acquisition_data[self.environment_parameters.output_channel_indices] * scale_factor 

2216 ) 

2217 if self.environment_parameters.reference_transformation_matrix is not None: 

2218 output_data = ( 

2219 self.environment_parameters.reference_transformation_matrix @ output_data 

2220 ) 

2221 # Add the data to the buffers 

2222 self.control_buffer.add_data(control_data) 

2223 self.output_buffer.add_data(output_data) 

2224 if last_acquisition: 

2225 # Find alignment with the specification via output 

2226 self.log("Aligning signal with specification") 

2227 ( 

2228 self.aligned_output, 

2229 sample_delay, 

2230 phase_change, 

2231 _, 

2232 ) = align_signals( 

2233 self.output_buffer[:], 

2234 self.next_drive[:, :: self.data_acquisition_parameters.output_oversample], 

2235 correlation_threshold=0.5, 

2236 ) 

2237 else: 

2238 ( 

2239 self.aligned_output, 

2240 sample_delay, 

2241 phase_change, 

2242 _, 

2243 ) = (None, None, None, None) 

2244 self.queue_container.gui_update_queue.put( 

2245 ( 

2246 self.environment_name, 

2247 ("time_data", (control_data, output_data, sample_delay)), 

2248 ) 

2249 ) # Sample_delay will be None if the alignment is not found 

2250 if self.aligned_output is not None: 

2251 self.log(f"Alignment Found at {sample_delay} samples") 

2252 self.aligned_response = shift_signal( 

2253 self.control_buffer[:], 

2254 self.environment_parameters.control_signal.shape[-1], 

2255 sample_delay, 

2256 phase_change, 

2257 ) 

2258 time_trac = trac(self.aligned_response, self.environment_parameters.control_signal) 

2259 self.gui_update_queue.put( 

2260 (self.environment_name, ("control_response_error_list", time_trac)) 

2261 ) 

2262 self.queue_container.gui_update_queue.put( 

2263 ( 

2264 self.environment_name, 

2265 ("control_data", (self.aligned_response, self.aligned_output)), 

2266 ) 

2267 ) 

2268 # Do the next control 

2269 self.log( 

2270 f"Last Signal Found: {self.last_signal_found}, " 

2271 f"Current Signal Found: {sample_delay}" 

2272 ) 

2273 # We don't want to keep a signal if it starts during the last signal. 

2274 # Multiply by 0.8 to give a little wiggle room in case the 

2275 # last signal wasn't found exactly at the right place. 

2276 if ( 

2277 self.last_signal_found is None 

2278 or ( 

2279 self.last_signal_found 

2280 + self.environment_parameters.control_signal.shape[-1] * 0.8 

2281 ) 

2282 < sample_delay 

2283 ): 

2284 self.next_drive = self.aligned_output 

2285 self.predicted_response = self.aligned_response 

2286 self.log("Computing next signal via control law") 

2287 self.perform_control_prediction(False) 

2288 self.last_signal_found = sample_delay 

2289 else: 

2290 self.log("Signal was found previously, not controlling") 

2291 except mp.queues.Empty: 

2292 last_acquisition = False 

2293 # See if we need to keep going 

2294 if self.siggen_shutdown_achieved and last_acquisition: 

2295 self.shutdown() 

2296 else: 

2297 self.queue_container.environment_command_queue.put( 

2298 self.environment_name, (TransientCommands.START_CONTROL, None) 

2299 ) 

2300 

2301 def shutdown(self): 

2302 """Let the UI know that this environment has completely shut down""" 

2303 self.log("Environment Shut Down") 

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

2305 self.startup = True 

2306 

2307 def stop_environment(self, data): 

2308 """Starts the shutdown sequence based on commands from the UI""" 

2309 self.queue_container.signal_generation_command_queue.put( 

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

2311 ) 

2312 

2313 

2314# %% Process 

2315 

2316 

2317def transient_process( 

2318 environment_name: str, 

2319 input_queue: VerboseMessageQueue, 

2320 gui_update_queue: Queue, 

2321 controller_communication_queue: VerboseMessageQueue, 

2322 log_file_queue: Queue, 

2323 data_in_queue: Queue, 

2324 data_out_queue: Queue, 

2325 acquisition_active: mp.sharedctypes.Synchronized, 

2326 output_active: mp.sharedctypes.Synchronized, 

2327): 

2328 """ 

2329 Transient vibration environment process function called by multiprocessing 

2330 

2331 This function defines the Transient Vibration Environment process that 

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

2333 creates a TransientEnvironment object and runs it. 

2334 

2335 Parameters 

2336 ---------- 

2337 environment_name : str : 

2338 Name of the environment 

2339 input_queue : VerboseMessageQueue : 

2340 Queue containing instructions for the environment 

2341 gui_update_queue : Queue : 

2342 Queue where GUI updates are put 

2343 controller_communication_queue : Queue : 

2344 Queue for global communications with the controller 

2345 log_file_queue : Queue : 

2346 Queue for writing log file messages 

2347 data_in_queue : Queue : 

2348 Queue from which data will be read by the environment 

2349 data_out_queue : Queue : 

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

2351 acquisition_active : mp.sharedctypes.Synchronized 

2352 A synchronized value that indicates when the acquisition is active 

2353 output_active : mp.sharedctypes.Synchronized 

2354 A synchronized value that indicates when the output is active 

2355 """ 

2356 try: 

2357 # Create vibration queues 

2358 queue_container = TransientQueues( 

2359 environment_name, 

2360 input_queue, 

2361 gui_update_queue, 

2362 controller_communication_queue, 

2363 data_in_queue, 

2364 data_out_queue, 

2365 log_file_queue, 

2366 ) 

2367 

2368 spectral_proc = mp.Process( 

2369 target=spectral_processing_process, 

2370 args=( 

2371 environment_name, 

2372 queue_container.spectral_command_queue, 

2373 queue_container.data_for_spectral_computation_queue, 

2374 queue_container.updated_spectral_quantities_queue, 

2375 queue_container.environment_command_queue, 

2376 queue_container.gui_update_queue, 

2377 queue_container.log_file_queue, 

2378 ), 

2379 ) 

2380 spectral_proc.start() 

2381 analysis_proc = mp.Process( 

2382 target=sysid_data_analysis_process, 

2383 args=( 

2384 environment_name, 

2385 queue_container.data_analysis_command_queue, 

2386 queue_container.updated_spectral_quantities_queue, 

2387 queue_container.time_history_to_generate_queue, 

2388 queue_container.environment_command_queue, 

2389 queue_container.gui_update_queue, 

2390 queue_container.log_file_queue, 

2391 ), 

2392 ) 

2393 analysis_proc.start() 

2394 siggen_proc = mp.Process( 

2395 target=signal_generation_process, 

2396 args=( 

2397 environment_name, 

2398 queue_container.signal_generation_command_queue, 

2399 queue_container.time_history_to_generate_queue, 

2400 queue_container.data_out_queue, 

2401 queue_container.environment_command_queue, 

2402 queue_container.log_file_queue, 

2403 queue_container.gui_update_queue, 

2404 ), 

2405 ) 

2406 siggen_proc.start() 

2407 collection_proc = mp.Process( 

2408 target=data_collector_process, 

2409 args=( 

2410 environment_name, 

2411 queue_container.collector_command_queue, 

2412 queue_container.data_in_queue, 

2413 [queue_container.data_for_spectral_computation_queue], 

2414 queue_container.environment_command_queue, 

2415 queue_container.log_file_queue, 

2416 queue_container.gui_update_queue, 

2417 ), 

2418 ) 

2419 collection_proc.start() 

2420 

2421 process_class = TransientEnvironment( 

2422 environment_name, queue_container, acquisition_active, output_active 

2423 ) 

2424 process_class.run() 

2425 

2426 # Rejoin all the processes 

2427 process_class.log("Joining Subprocesses") 

2428 process_class.log("Joining Spectral Computation") 

2429 spectral_proc.join() 

2430 process_class.log("Joining Data Analysis") 

2431 analysis_proc.join() 

2432 process_class.log("Joining Signal Generation") 

2433 siggen_proc.join() 

2434 process_class.log("Joining Data Collection") 

2435 collection_proc.join() 

2436 except Exception: # pylint: disable = broad-exception-caught 

2437 print(traceback.format_exc())