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

958 statements  

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

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

2""" 

3Abstract environment that can be used to create new environment control strategies 

4in the controller that use system identification. 

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 multiprocessing as mp 

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

27import time 

28from abc import abstractmethod 

29from copy import deepcopy 

30from enum import Enum 

31from multiprocessing.queues import Queue 

32 

33import netCDF4 as nc4 

34import numpy as np 

35import openpyxl 

36import pyqtgraph as pg 

37from qtpy import QtWidgets, uic 

38from scipy.io import loadmat, savemat 

39 

40from .abstract_environment import AbstractEnvironment, AbstractMetadata, AbstractUI 

41from .data_collector import ( 

42 Acceptance, 

43 AcquisitionType, 

44 CollectorMetadata, 

45 DataCollectorCommands, 

46 TriggerSlope, 

47 Window, 

48) 

49from .environments import system_identification_ui_path 

50from .signal_generation import ( 

51 BurstRandomSignalGenerator, 

52 ChirpSignalGenerator, 

53 PseudorandomSignalGenerator, 

54 RandomSignalGenerator, 

55 SignalGenerator, 

56) 

57from .signal_generation_process import ( 

58 SignalGenerationCommands, 

59 SignalGenerationMetadata, 

60) 

61from .spectral_processing import ( 

62 AveragingTypes, 

63 Estimator, 

64 SpectralProcessingCommands, 

65 SpectralProcessingMetadata, 

66) 

67from .utilities import ( 

68 DataAcquisitionParameters, 

69 GlobalCommands, 

70 VerboseMessageQueue, 

71 error_message_qt, 

72) 

73 

74 

75class SystemIdCommands(Enum): 

76 """Enumeration of commands that could be sent to the system identification environment""" 

77 

78 PREVIEW_NOISE = 0 

79 PREVIEW_TRANSFER_FUNCTION = 1 

80 START_SYSTEM_ID = 2 

81 STOP_SYSTEM_ID = 3 

82 CHECK_FOR_COMPLETE_SHUTDOWN = 4 

83 

84 

85class AbstractSysIdMetadata(AbstractMetadata): 

86 """Abstract class for storing metadata for an environment. 

87 

88 This class is used as a storage container for parameters used by an 

89 environment. It is returned by the environment UI's 

90 ``collect_environment_definition_parameters`` function as well as its 

91 ``initialize_environment`` function. Various parts of the controller and 

92 environment will query the class's data members for parameter values. 

93 

94 Classes inheriting from AbstractMetadata must define: 

95 1. store_to_netcdf - A function defining the way the parameters are 

96 stored to a netCDF file saved during streaming operations. 

97 """ 

98 

99 def __init__(self): 

100 self.sysid_frame_size = None 

101 self.sysid_averaging_type = None 

102 self.sysid_noise_averages = None 

103 self.sysid_averages = None 

104 self.sysid_exponential_averaging_coefficient = None 

105 self.sysid_estimator = None 

106 self.sysid_level = None 

107 self.sysid_level_ramp_time = None 

108 self.sysid_signal_type = None 

109 self.sysid_window = None 

110 self.sysid_overlap = None 

111 self.sysid_burst_on = None 

112 self.sysid_pretrigger = None 

113 self.sysid_burst_ramp_fraction = None 

114 self.sysid_low_frequency_cutoff = None 

115 self.sysid_high_frequency_cutoff = None 

116 

117 @property 

118 @abstractmethod 

119 def number_of_channels(self): 

120 """Number of channels in the environment""" 

121 

122 @property 

123 @abstractmethod 

124 def response_channel_indices(self): 

125 """Indices corresponding to the response or control channels in the environment""" 

126 

127 @property 

128 @abstractmethod 

129 def reference_channel_indices(self): 

130 """Indices corresponding to the excitation channels in the environment""" 

131 

132 @property 

133 def num_response_channels(self): 

134 """Gets the total number of control channels including transformation effects""" 

135 return ( 

136 len(self.response_channel_indices) 

137 if self.response_transformation_matrix is None 

138 else self.response_transformation_matrix.shape[0] 

139 ) 

140 

141 @property 

142 def num_reference_channels(self): 

143 """Gets the total number of excitation channels including transformation effects""" 

144 return ( 

145 len(self.reference_channel_indices) 

146 if self.reference_transformation_matrix is None 

147 else self.reference_transformation_matrix.shape[0] 

148 ) 

149 

150 @property 

151 @abstractmethod 

152 def response_transformation_matrix(self): 

153 """Gets the response transformation matrix""" 

154 

155 @property 

156 @abstractmethod 

157 def reference_transformation_matrix(self): 

158 """Gets the excitation transformation matrix""" 

159 

160 @property 

161 def sysid_frequency_spacing(self): 

162 """Frequency spacing in spectral quantities computed by system identification""" 

163 return self.sample_rate / self.sysid_frame_size 

164 

165 @property 

166 @abstractmethod 

167 def sample_rate(self): 

168 """Sample rate (not oversampled) of the data acquisition system""" 

169 

170 @property 

171 def sysid_fft_lines(self): 

172 """Number of frequency lines in the FFT""" 

173 return self.sysid_frame_size // 2 + 1 

174 

175 @property 

176 def sysid_skip_frames(self): 

177 """Number of frames to skip in the time stream due to ramp time""" 

178 return int( 

179 np.ceil( 

180 self.sysid_level_ramp_time 

181 * self.sample_rate 

182 / (self.sysid_frame_size * (1 - self.sysid_overlap)) 

183 ) 

184 ) 

185 

186 @abstractmethod 

187 def store_to_netcdf( 

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

189 ): 

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

191 

192 This function stores parameters from the environment into the netCDF 

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

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

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

196 attributes, dimensions, or variables. 

197 

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

199 function in the AbstractUI class, which will read parameters from 

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

201 

202 Parameters 

203 ---------- 

204 netcdf_group_handle : nc4._netCDF4.Group 

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

206 environment's metadata is stored. 

207 

208 """ 

209 netcdf_group_handle.sysid_frame_size = self.sysid_frame_size 

210 netcdf_group_handle.sysid_averaging_type = self.sysid_averaging_type 

211 netcdf_group_handle.sysid_noise_averages = self.sysid_noise_averages 

212 netcdf_group_handle.sysid_averages = self.sysid_averages 

213 netcdf_group_handle.sysid_exponential_averaging_coefficient = ( 

214 self.sysid_exponential_averaging_coefficient 

215 ) 

216 netcdf_group_handle.sysid_estimator = self.sysid_estimator 

217 netcdf_group_handle.sysid_level = self.sysid_level 

218 netcdf_group_handle.sysid_level_ramp_time = self.sysid_level_ramp_time 

219 netcdf_group_handle.sysid_signal_type = self.sysid_signal_type 

220 netcdf_group_handle.sysid_window = self.sysid_window 

221 netcdf_group_handle.sysid_overlap = self.sysid_overlap 

222 netcdf_group_handle.sysid_burst_on = self.sysid_burst_on 

223 netcdf_group_handle.sysid_pretrigger = self.sysid_pretrigger 

224 netcdf_group_handle.sysid_burst_ramp_fraction = self.sysid_burst_ramp_fraction 

225 netcdf_group_handle.sysid_low_frequency_cutoff = self.sysid_low_frequency_cutoff 

226 netcdf_group_handle.sysid_high_frequency_cutoff = self.sysid_high_frequency_cutoff 

227 

228 def __eq__(self, other): 

229 try: 

230 return np.all( 

231 [np.all(value == other.__dict__[field]) for field, value in self.__dict__.items()] 

232 ) 

233 except (AttributeError, KeyError): 

234 return False 

235 

236 

237class RotatedAxisItem(pg.AxisItem): # pylint: disable=abstract-method 

238 """Plot axis labels that can be rotated by some value""" 

239 

240 def __init__(self, *args, **kwargs): 

241 super().__init__(*args, **kwargs) 

242 self._original_height = self.height() 

243 self._angle = None 

244 

245 def setAngle(self, angle): # pylint: disable=invalid-name 

246 """Sets the angle and ensures it's between -180 and 180""" 

247 self._angle = angle 

248 self._angle = (self._angle + 180) % 360 - 180 

249 

250 def drawPicture(self, p, axisSpec, tickSpecs, textSpecs): 

251 """UPdated draw picture method that includes the rotation of the text""" 

252 profiler = pg.debug.Profiler() 

253 max_width = 0 

254 

255 # draw long line along axis 

256 pen, p1, p2 = axisSpec 

257 p.setPen(pen) 

258 p.drawLine(p1, p2) 

259 # draw ticks 

260 for pen, p1, p2 in tickSpecs: 

261 p.setPen(pen) 

262 p.drawLine(p1, p2) 

263 profiler("draw ticks") 

264 

265 for rect, flags, text in textSpecs: 

266 p.save() # save the painter state 

267 

268 p.translate(rect.center()) # move coordinate system to center of text rect 

269 p.rotate(self._angle) # rotate text 

270 p.translate(-rect.center()) # revert coordinate system 

271 

272 x_offset = np.ceil(np.fabs(np.sin(np.radians(self._angle)) * rect.width())) 

273 if self._angle < 0: 

274 x_offset = -x_offset 

275 p.translate(x_offset / 2, 0) # Move the coordinate system (relatively) downwards 

276 

277 p.drawText(rect, flags, text) 

278 p.restore() # restore the painter state 

279 offset = np.fabs(x_offset) 

280 max_width = offset if max_width < offset else max_width 

281 

282 profiler("draw text") 

283 # Adjust the height 

284 self.setHeight(self._original_height + max_width) 

285 

286 def boundingRect(self): 

287 """Sets the bounding rectangle of the item to give more space at the bottom""" 

288 rect = super().boundingRect() 

289 rect.adjust(0, 0, 0, 20) # Add 20 pixels to bottom 

290 return rect 

291 

292 

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

294 SysIDDataAnalysisCommands, 

295) 

296 

297 

298class AbstractSysIdUI(AbstractUI): 

299 """Abstract User Interface class defining the interface with the controller 

300 

301 This class is used to define the interface between the User Interface of a 

302 environment in the controller and the main controller.""" 

303 

304 @abstractmethod 

305 def __init__( 

306 self, 

307 environment_name: str, 

308 environment_command_queue: VerboseMessageQueue, 

309 controller_communication_queue: VerboseMessageQueue, 

310 log_file_queue: Queue, 

311 system_id_tabwidget: QtWidgets.QTabWidget, 

312 ): 

313 """ 

314 Stores data required by the controller to interact with the UI 

315 

316 This class stores data required by the controller to interact with the 

317 user interface for a given environment. This includes the environment 

318 name and queues to pass information between the controller and 

319 environment. It additionally initializes the ``command_map`` which is 

320 used by the Test Profile functionality to map profile instructions to 

321 operations on the user interface. 

322 

323 

324 Parameters 

325 ---------- 

326 environment_name : str 

327 The name of the environment 

328 environment_command_queue : VerboseMessageQueue 

329 A queue that will provide instructions to the corresponding 

330 environment 

331 controller_communication_queue : VerboseMessageQueue 

332 The queue that relays global communication messages to the controller 

333 log_file_queue : Queue 

334 The queue that will be used to put messages to the log file. 

335 

336 

337 """ 

338 super().__init__( 

339 environment_name, 

340 environment_command_queue, 

341 controller_communication_queue, 

342 log_file_queue, 

343 ) 

344 # Add the page to the system id tabwidget 

345 self.system_id_widget = QtWidgets.QWidget() 

346 uic.loadUi(system_identification_ui_path, self.system_id_widget) 

347 system_id_tabwidget.addTab(self.system_id_widget, self.environment_name) 

348 self.connect_sysid_callbacks() 

349 

350 self.data_acquisition_parameters = None 

351 self.environment_parameters = None 

352 self.frequencies = None 

353 self.last_time_response = None 

354 self.last_transfer_function = None 

355 self.last_response_noise = None 

356 self.last_reference_noise = None 

357 self.last_response_cpsd = None 

358 self.last_reference_cpsd = None 

359 self.last_coherence = None 

360 self.last_condition = None 

361 self.last_kurtosis = None 

362 

363 self.time_response_plot = self.system_id_widget.time_data_graphicslayout.addPlot( 

364 row=0, column=0 

365 ) 

366 self.time_response_plot.setLabel("left", "Response") 

367 self.time_response_plot.setLabel("bottom", "Time (s)") 

368 self.time_reference_plot = self.system_id_widget.time_data_graphicslayout.addPlot( 

369 row=0, column=1 

370 ) 

371 self.time_reference_plot.setLabel("left", "Reference") 

372 self.time_reference_plot.setLabel("bottom", "Time (s)") 

373 self.level_response_plot = self.system_id_widget.levels_graphicslayout.addPlot( 

374 row=0, column=0 

375 ) 

376 self.level_response_plot.setLabel("left", "Response PSD") 

377 self.level_response_plot.setLabel("bottom", "Frequency (Hz)") 

378 self.level_reference_plot = self.system_id_widget.levels_graphicslayout.addPlot( 

379 row=0, column=1 

380 ) 

381 self.level_reference_plot.setLabel("left", "Reference PSD") 

382 self.level_reference_plot.setLabel("bottom", "Frequency (Hz)") 

383 self.transfer_function_phase_plot = ( 

384 self.system_id_widget.transfer_function_graphics_layout.addPlot(row=0, column=0) 

385 ) 

386 self.transfer_function_phase_plot.setLabel("left", "Phase") 

387 self.transfer_function_phase_plot.setLabel("bottom", "Frequency (Hz)") 

388 self.transfer_function_magnitude_plot = ( 

389 self.system_id_widget.transfer_function_graphics_layout.addPlot(row=0, column=1) 

390 ) 

391 self.transfer_function_magnitude_plot.setLabel("left", "Amplitude") 

392 self.transfer_function_magnitude_plot.setLabel("bottom", "Frequency (Hz)") 

393 self.impulse_response_plot = self.system_id_widget.impulse_graphicslayout.addPlot( 

394 row=0, column=0 

395 ) 

396 self.impulse_response_plot.setLabel("left", "Impulse Response") 

397 self.impulse_response_plot.setLabel("bottom", "Time (s)") 

398 self.coherence_plot = self.system_id_widget.coherence_graphicslayout.addPlot( 

399 row=0, column=0 

400 ) 

401 self.coherence_plot.setLabel("left", "Multiple Coherence") 

402 self.coherence_plot.setLabel("bottom", "Frequency (Hz)") 

403 self.condition_plot = self.system_id_widget.coherence_graphicslayout.addPlot( 

404 row=0, column=1 

405 ) 

406 self.condition_plot.setLabel("left", "Condition Number") 

407 self.condition_plot.setLabel("bottom", "Frequency (Hz)") 

408 self.coherence_plot.vb.setLimits(yMin=0, yMax=1) 

409 self.coherence_plot.vb.disableAutoRange(axis="y") 

410 # Set up kurtosis plots 

411 self.response_nodes = [] 

412 self.reference_nodes = [] 

413 self.all_response_indices = [] 

414 self.all_reference_indices = [] 

415 self.kurtosis_response_plot = self.system_id_widget.kurtosis_graphicslayout.addPlot( 

416 row=0, column=0 

417 ) 

418 self.kurtosis_reference_plot = self.system_id_widget.kurtosis_graphicslayout.addPlot( 

419 row=0, column=1 

420 ) 

421 self.kurtosis_response_plot.setLabel("left", "Response") 

422 self.kurtosis_reference_plot.setLabel("left", "Reference") 

423 response_axis = RotatedAxisItem("bottom") 

424 reference_axis = RotatedAxisItem("bottom") 

425 response_axis.setAngle(-60) 

426 reference_axis.setAngle(-60) 

427 self.kurtosis_response_plot.setAxisItems({"bottom": response_axis}) 

428 self.kurtosis_reference_plot.setAxisItems({"bottom": reference_axis}) 

429 for plot in [ 

430 self.level_response_plot, 

431 self.level_reference_plot, 

432 self.transfer_function_magnitude_plot, 

433 self.condition_plot, 

434 ]: 

435 plot.setLogMode(False, True) 

436 self.show_hide_coherence() 

437 self.show_hide_impulse() 

438 self.show_hide_levels() 

439 self.show_hide_time_data() 

440 self.show_hide_transfer_function() 

441 self.show_hide_kurtosis() 

442 

443 def connect_sysid_callbacks(self): 

444 """Connects the callback functions to the system identification widgets""" 

445 self.system_id_widget.preview_noise_button.clicked.connect(self.preview_noise) 

446 self.system_id_widget.preview_system_id_button.clicked.connect( 

447 self.preview_transfer_function 

448 ) 

449 self.system_id_widget.start_button.clicked.connect(self.acquire_transfer_function) 

450 self.system_id_widget.stop_button.clicked.connect(self.stop_system_id) 

451 self.system_id_widget.select_transfer_function_stream_file_button.clicked.connect( 

452 self.select_transfer_function_stream_file 

453 ) 

454 self.system_id_widget.response_selector.itemSelectionChanged.connect( 

455 self.update_sysid_plots 

456 ) 

457 self.system_id_widget.reference_selector.itemSelectionChanged.connect( 

458 self.update_sysid_plots 

459 ) 

460 self.system_id_widget.coherence_checkbox.stateChanged.connect(self.show_hide_coherence) 

461 self.system_id_widget.levels_checkbox.stateChanged.connect(self.show_hide_levels) 

462 self.system_id_widget.time_data_checkbox.stateChanged.connect(self.show_hide_time_data) 

463 self.system_id_widget.impulse_checkbox.stateChanged.connect(self.show_hide_impulse) 

464 self.system_id_widget.transfer_function_checkbox.stateChanged.connect( 

465 self.show_hide_transfer_function 

466 ) 

467 self.system_id_widget.kurtosis_checkbox.stateChanged.connect(self.show_hide_kurtosis) 

468 self.system_id_widget.signalTypeComboBox.currentIndexChanged.connect( 

469 self.update_signal_type 

470 ) 

471 self.system_id_widget.save_system_id_matrices_button.clicked.connect( 

472 self.save_sysid_matrix_file 

473 ) 

474 self.system_id_widget.load_system_id_matrices_button.clicked.connect( 

475 self.load_sysid_matrix_file 

476 ) 

477 

478 @abstractmethod 

479 def initialize_data_acquisition(self, data_acquisition_parameters: DataAcquisitionParameters): 

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

481 

482 This function is called when the Data Acquisition parameters are 

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

484 accordingly. 

485 

486 Parameters 

487 ---------- 

488 data_acquisition_parameters : DataAcquisitionParameters : 

489 Container containing the data acquisition parameters, including 

490 channel table and sampling information. 

491 

492 """ 

493 self.log("Initializing Data Acquisition") 

494 # Store for later 

495 self.data_acquisition_parameters = data_acquisition_parameters 

496 self.system_id_widget.highFreqCutoffSpinBox.setMaximum( 

497 data_acquisition_parameters.sample_rate // 2 

498 ) 

499 # finish setting up kurtosis plots using node number + direction 

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

501 node = channel.node_number + ( 

502 "" if channel.node_direction is None else channel.node_direction 

503 ) 

504 if channel.feedback_device is None: 

505 self.response_nodes.append(node) 

506 self.all_response_indices.append(i) 

507 else: 

508 self.reference_nodes.append(node) 

509 self.all_reference_indices.append(i) 

510 response_ax = self.kurtosis_response_plot.getAxis("bottom") 

511 reference_ax = self.kurtosis_reference_plot.getAxis("bottom") 

512 response_ax.setTicks([list(enumerate(self.response_nodes))]) 

513 reference_ax.setTicks([list(enumerate(self.reference_nodes))]) 

514 self.system_id_widget.kurtosis_graphicslayout.ci.layout.setColumnStretchFactor( 

515 0, len(self.all_response_indices) * 2 + len(self.all_reference_indices) 

516 ) 

517 self.system_id_widget.kurtosis_graphicslayout.ci.layout.setColumnStretchFactor( 

518 1, len(self.all_reference_indices) * 2 + len(self.all_response_indices) 

519 ) 

520 

521 @abstractmethod 

522 def collect_environment_definition_parameters(self) -> AbstractSysIdMetadata: 

523 """ 

524 Collect the parameters from the user interface defining the environment 

525 

526 Returns 

527 ------- 

528 AbstractSysIdMetadata 

529 A metadata or parameters object containing the parameters defining 

530 the corresponding environment. 

531 

532 """ 

533 

534 def update_sysid_metadata(self, metadata: AbstractSysIdMetadata): 

535 """Updates the provided system identification metadata based on current UI widget values""" 

536 metadata.sysid_frame_size = self.system_id_widget.samplesPerFrameSpinBox.value() 

537 metadata.sysid_averaging_type = self.system_id_widget.averagingTypeComboBox.itemText( 

538 self.system_id_widget.averagingTypeComboBox.currentIndex() 

539 ) 

540 metadata.sysid_noise_averages = self.system_id_widget.noiseAveragesSpinBox.value() 

541 metadata.sysid_averages = self.system_id_widget.systemIDAveragesSpinBox.value() 

542 metadata.sysid_exponential_averaging_coefficient = ( 

543 self.system_id_widget.averagingCoefficientDoubleSpinBox.value() 

544 ) 

545 metadata.sysid_estimator = self.system_id_widget.estimatorComboBox.itemText( 

546 self.system_id_widget.estimatorComboBox.currentIndex() 

547 ) 

548 metadata.sysid_level = self.system_id_widget.levelDoubleSpinBox.value() 

549 metadata.sysid_level_ramp_time = self.system_id_widget.levelRampTimeDoubleSpinBox.value() 

550 metadata.sysid_signal_type = self.system_id_widget.signalTypeComboBox.itemText( 

551 self.system_id_widget.signalTypeComboBox.currentIndex() 

552 ) 

553 metadata.sysid_window = self.system_id_widget.windowComboBox.itemText( 

554 self.system_id_widget.windowComboBox.currentIndex() 

555 ) 

556 metadata.sysid_overlap = ( 

557 self.system_id_widget.overlapDoubleSpinBox.value() / 100 

558 if metadata.sysid_signal_type == "Random" 

559 else 0.0 

560 ) 

561 metadata.sysid_burst_on = self.system_id_widget.onFractionDoubleSpinBox.value() / 100 

562 metadata.sysid_pretrigger = self.system_id_widget.pretriggerDoubleSpinBox.value() / 100 

563 metadata.sysid_burst_ramp_fraction = ( 

564 self.system_id_widget.rampFractionDoubleSpinBox.value() / 100 

565 ) 

566 metadata.sysid_low_frequency_cutoff = self.system_id_widget.lowFreqCutoffSpinBox.value() 

567 metadata.sysid_high_frequency_cutoff = self.system_id_widget.highFreqCutoffSpinBox.value() 

568 # for key in dir(metadata): 

569 # if '__' == key[:2]: 

570 # continue 

571 # print('Key: {:}'.format(key)) 

572 # print('Value: {:}'.format(getattr(metadata,key))) 

573 

574 @property 

575 @abstractmethod 

576 def initialized_control_names(self): 

577 """Names of control channels that have been initialized and will be used in displays""" 

578 

579 @property 

580 @abstractmethod 

581 def initialized_output_names(self): 

582 """Names of output channels that have been initialized and will be used in displays""" 

583 

584 @abstractmethod 

585 def initialize_environment(self) -> AbstractMetadata: 

586 """ 

587 Update the user interface with environment parameters 

588 

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

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

591 return the parameters class of the environment that inherits from 

592 AbstractMetadata. 

593 

594 Returns 

595 ------- 

596 AbstractMetadata 

597 An AbstractMetadata-inheriting object that contains the parameters 

598 defining the environment. 

599 

600 """ 

601 self.environment_parameters = self.collect_environment_definition_parameters() 

602 self.update_sysid_metadata(self.environment_parameters) 

603 self.system_id_widget.reference_selector.blockSignals(True) 

604 self.system_id_widget.response_selector.blockSignals(True) 

605 self.system_id_widget.reference_selector.clear() 

606 self.system_id_widget.response_selector.clear() 

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

608 self.system_id_widget.response_selector.addItem(f"{i + 1}: {control_name}") 

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

610 self.system_id_widget.reference_selector.addItem(f"{i + 1}: {drive_name}") 

611 self.system_id_widget.reference_selector.blockSignals(False) 

612 self.system_id_widget.response_selector.blockSignals(False) 

613 self.system_id_widget.reference_selector.setCurrentRow(0) 

614 self.system_id_widget.response_selector.setCurrentRow(0) 

615 self.update_signal_type() 

616 return self.environment_parameters 

617 

618 def preview_noise(self): 

619 """Starts the noise preview""" 

620 self.log("Starting Noise Preview") 

621 self.update_sysid_metadata(self.environment_parameters) 

622 for widget in [ 

623 self.system_id_widget.preview_noise_button, 

624 self.system_id_widget.preview_system_id_button, 

625 self.system_id_widget.start_button, 

626 self.system_id_widget.samplesPerFrameSpinBox, 

627 self.system_id_widget.averagingTypeComboBox, 

628 self.system_id_widget.noiseAveragesSpinBox, 

629 self.system_id_widget.systemIDAveragesSpinBox, 

630 self.system_id_widget.averagingCoefficientDoubleSpinBox, 

631 self.system_id_widget.estimatorComboBox, 

632 self.system_id_widget.levelDoubleSpinBox, 

633 self.system_id_widget.signalTypeComboBox, 

634 self.system_id_widget.windowComboBox, 

635 self.system_id_widget.overlapDoubleSpinBox, 

636 self.system_id_widget.onFractionDoubleSpinBox, 

637 self.system_id_widget.pretriggerDoubleSpinBox, 

638 self.system_id_widget.rampFractionDoubleSpinBox, 

639 self.system_id_widget.stream_transfer_function_data_checkbox, 

640 self.system_id_widget.select_transfer_function_stream_file_button, 

641 self.system_id_widget.transfer_function_stream_file_display, 

642 self.system_id_widget.levelRampTimeDoubleSpinBox, 

643 self.system_id_widget.save_system_id_matrices_button, 

644 self.system_id_widget.load_system_id_matrices_button, 

645 self.system_id_widget.lowFreqCutoffSpinBox, 

646 self.system_id_widget.highFreqCutoffSpinBox, 

647 ]: 

648 widget.setEnabled(False) 

649 for widget in [self.system_id_widget.stop_button]: 

650 widget.setEnabled(True) 

651 self.environment_command_queue.put( 

652 self.log_name, (SystemIdCommands.PREVIEW_NOISE, self.environment_parameters) 

653 ) 

654 

655 def preview_transfer_function(self): 

656 """Starts previewing the system identification transfer function calculation""" 

657 self.log("Starting System ID Preview") 

658 self.update_sysid_metadata(self.environment_parameters) 

659 for widget in [ 

660 self.system_id_widget.preview_noise_button, 

661 self.system_id_widget.preview_system_id_button, 

662 self.system_id_widget.start_button, 

663 self.system_id_widget.samplesPerFrameSpinBox, 

664 self.system_id_widget.averagingTypeComboBox, 

665 self.system_id_widget.noiseAveragesSpinBox, 

666 self.system_id_widget.systemIDAveragesSpinBox, 

667 self.system_id_widget.averagingCoefficientDoubleSpinBox, 

668 self.system_id_widget.estimatorComboBox, 

669 self.system_id_widget.levelDoubleSpinBox, 

670 self.system_id_widget.signalTypeComboBox, 

671 self.system_id_widget.windowComboBox, 

672 self.system_id_widget.overlapDoubleSpinBox, 

673 self.system_id_widget.onFractionDoubleSpinBox, 

674 self.system_id_widget.pretriggerDoubleSpinBox, 

675 self.system_id_widget.rampFractionDoubleSpinBox, 

676 self.system_id_widget.stream_transfer_function_data_checkbox, 

677 self.system_id_widget.select_transfer_function_stream_file_button, 

678 self.system_id_widget.transfer_function_stream_file_display, 

679 self.system_id_widget.levelRampTimeDoubleSpinBox, 

680 self.system_id_widget.save_system_id_matrices_button, 

681 self.system_id_widget.load_system_id_matrices_button, 

682 self.system_id_widget.lowFreqCutoffSpinBox, 

683 self.system_id_widget.highFreqCutoffSpinBox, 

684 ]: 

685 widget.setEnabled(False) 

686 for widget in [self.system_id_widget.stop_button]: 

687 widget.setEnabled(True) 

688 self.environment_command_queue.put( 

689 self.log_name, 

690 (SystemIdCommands.PREVIEW_TRANSFER_FUNCTION, (self.environment_parameters)), 

691 ) 

692 

693 def acquire_transfer_function(self): 

694 """Starts the acquisition phase of the controller""" 

695 self.log("Starting System ID") 

696 self.update_sysid_metadata(self.environment_parameters) 

697 for widget in [ 

698 self.system_id_widget.preview_noise_button, 

699 self.system_id_widget.preview_system_id_button, 

700 self.system_id_widget.start_button, 

701 self.system_id_widget.samplesPerFrameSpinBox, 

702 self.system_id_widget.averagingTypeComboBox, 

703 self.system_id_widget.noiseAveragesSpinBox, 

704 self.system_id_widget.systemIDAveragesSpinBox, 

705 self.system_id_widget.averagingCoefficientDoubleSpinBox, 

706 self.system_id_widget.estimatorComboBox, 

707 self.system_id_widget.levelDoubleSpinBox, 

708 self.system_id_widget.signalTypeComboBox, 

709 self.system_id_widget.windowComboBox, 

710 self.system_id_widget.overlapDoubleSpinBox, 

711 self.system_id_widget.onFractionDoubleSpinBox, 

712 self.system_id_widget.pretriggerDoubleSpinBox, 

713 self.system_id_widget.rampFractionDoubleSpinBox, 

714 self.system_id_widget.stream_transfer_function_data_checkbox, 

715 self.system_id_widget.select_transfer_function_stream_file_button, 

716 self.system_id_widget.transfer_function_stream_file_display, 

717 self.system_id_widget.levelRampTimeDoubleSpinBox, 

718 self.system_id_widget.save_system_id_matrices_button, 

719 self.system_id_widget.load_system_id_matrices_button, 

720 self.system_id_widget.lowFreqCutoffSpinBox, 

721 self.system_id_widget.highFreqCutoffSpinBox, 

722 ]: 

723 widget.setEnabled(False) 

724 for widget in [self.system_id_widget.stop_button]: 

725 widget.setEnabled(True) 

726 if self.system_id_widget.stream_transfer_function_data_checkbox.isChecked(): 

727 stream_name = self.system_id_widget.transfer_function_stream_file_display.text() 

728 else: 

729 stream_name = None 

730 self.environment_command_queue.put( 

731 self.log_name, 

732 ( 

733 SystemIdCommands.START_SYSTEM_ID, 

734 (self.environment_parameters, stream_name), 

735 ), 

736 ) 

737 

738 def stop_system_id(self): 

739 """Stops the system identification""" 

740 self.log("Stopping System ID") 

741 self.system_id_widget.stop_button.setEnabled(False) 

742 self.environment_command_queue.put( 

743 self.log_name, (SystemIdCommands.STOP_SYSTEM_ID, (True, True)) 

744 ) 

745 

746 def select_transfer_function_stream_file(self): 

747 """Select a file to save transfer function data to""" 

748 filename, _ = QtWidgets.QFileDialog.getSaveFileName( 

749 self.system_id_widget, 

750 "Select NetCDF File to Save Transfer Function Data", 

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

752 ) 

753 if filename == "": 

754 return 

755 self.system_id_widget.transfer_function_stream_file_display.setText(filename) 

756 self.system_id_widget.stream_transfer_function_data_checkbox.setChecked(True) 

757 

758 def update_sysid_plots( 

759 self, 

760 update_time=True, 

761 update_transfer_function=True, 

762 update_noise=True, 

763 update_kurtosis=True, 

764 ): 

765 """Updates the plots on the system identification window 

766 

767 Parameters 

768 ---------- 

769 update_time : bool, optional 

770 If True, updates the time hitory plots, by default True 

771 update_transfer_function : bool, optional 

772 If True, updates the transfer function plots, by default True 

773 update_noise : bool, optional 

774 If True, updates the noise plots, by default True 

775 update_kurtosis : bool, optional 

776 If True, updates the kurtosis bar graph, by default True 

777 """ 

778 # Figure out the selected entries 

779 response_indices = [ 

780 i 

781 for i in range(self.system_id_widget.response_selector.count()) 

782 if self.system_id_widget.response_selector.item(i).isSelected() 

783 ] 

784 reference_indices = [ 

785 i 

786 for i in range(self.system_id_widget.reference_selector.count()) 

787 if self.system_id_widget.reference_selector.item(i).isSelected() 

788 ] 

789 # print(response_indices) 

790 # print(reference_indices) 

791 if update_time: 

792 self.time_response_plot.clear() 

793 self.time_reference_plot.clear() 

794 if self.last_time_response is not None: 

795 response_frame_indices = np.array( 

796 self.environment_parameters.response_channel_indices 

797 )[response_indices] 

798 reference_frame_indices = np.array( 

799 self.environment_parameters.reference_channel_indices 

800 )[reference_indices] 

801 response_time_data = self.last_time_response[response_frame_indices] 

802 reference_time_data = self.last_time_response[reference_frame_indices] 

803 times = ( 

804 np.arange(response_time_data.shape[-1]) 

805 / self.data_acquisition_parameters.sample_rate 

806 ) 

807 for i, time_data in enumerate(response_time_data): 

808 self.time_response_plot.plot(times, time_data, pen=i) 

809 for i, time_data in enumerate(reference_time_data): 

810 self.time_reference_plot.plot(times, time_data, pen=i) 

811 if update_transfer_function: 

812 self.transfer_function_phase_plot.clear() 

813 self.transfer_function_magnitude_plot.clear() 

814 self.condition_plot.clear() 

815 self.coherence_plot.clear() 

816 self.impulse_response_plot.clear() 

817 if ( 

818 self.last_transfer_function is not None 

819 and len(response_indices) > 0 

820 and len(reference_indices) > 0 

821 ): 

822 # print(self.last_transfer_function) 

823 # print(np.array(response_indices)[:,np.newaxis]) 

824 # print(np.array(reference_indices)) 

825 frf_section = np.reshape( 

826 self.last_transfer_function[ 

827 ..., 

828 np.array(response_indices)[:, np.newaxis], 

829 np.array(reference_indices), 

830 ], 

831 (self.frequencies.size, -1), 

832 ).T 

833 impulse_response = np.fft.irfft(frf_section, axis=-1) 

834 for i, (frf, imp) in enumerate(zip(frf_section, impulse_response)): 

835 self.transfer_function_phase_plot.plot( 

836 self.frequencies, np.angle(frf) * 180 / np.pi, pen=i 

837 ) 

838 self.transfer_function_magnitude_plot.plot(self.frequencies, np.abs(frf), pen=i) 

839 self.impulse_response_plot.plot( 

840 np.arange(imp.size) / self.environment_parameters.sample_rate, 

841 imp, 

842 pen=i, 

843 ) 

844 for i, coherence in enumerate(self.last_coherence[..., response_indices].T): 

845 self.coherence_plot.plot(self.frequencies, coherence, pen=i) 

846 if self.last_condition is not None: 

847 self.condition_plot.plot(self.frequencies, self.last_condition, pen=0) 

848 if update_noise: 

849 reference_noise = ( 

850 None 

851 if self.last_reference_noise is None or len(reference_indices) == 0 

852 else self.last_reference_noise[..., reference_indices, reference_indices].real 

853 ) 

854 response_noise = ( 

855 None 

856 if self.last_response_noise is None or len(response_indices) == 0 

857 else self.last_response_noise[..., response_indices, response_indices].real 

858 ) 

859 reference_level = ( 

860 None 

861 if self.last_reference_cpsd is None or len(reference_indices) == 0 

862 else self.last_reference_cpsd[..., reference_indices, reference_indices].real 

863 ) 

864 response_level = ( 

865 None 

866 if self.last_response_cpsd is None or len(response_indices) == 0 

867 else self.last_response_cpsd[..., response_indices, response_indices].real 

868 ) 

869 self.level_reference_plot.clear() 

870 self.level_response_plot.clear() 

871 for i in range(len(reference_indices)): 

872 if reference_noise is not None: 

873 self.level_reference_plot.plot(self.frequencies, reference_noise[:, i], pen=i) 

874 if reference_level is not None: 

875 try: 

876 self.level_reference_plot.plot( 

877 self.frequencies, reference_level[:, i], pen=i 

878 ) 

879 except Exception: 

880 pass 

881 for i in range(len(response_indices)): 

882 if response_noise is not None: 

883 self.level_response_plot.plot(self.frequencies, response_noise[:, i], pen=i) 

884 if response_level is not None: 

885 try: 

886 self.level_response_plot.plot(self.frequencies, response_level[:, i], pen=i) 

887 except Exception: 

888 pass 

889 

890 if update_kurtosis: 

891 self.kurtosis_response_plot.clear() 

892 self.kurtosis_reference_plot.clear() 

893 if self.last_kurtosis is not None: 

894 response_kurtosis = self.last_kurtosis[self.all_response_indices] 

895 reference_kurtosis = self.last_kurtosis[self.all_reference_indices] 

896 response_bar = pg.BarGraphItem( 

897 x=range(len(self.response_nodes)), 

898 height=response_kurtosis, 

899 width=0.5, 

900 pen="r", 

901 brush="r", 

902 ) 

903 reference_bar = pg.BarGraphItem( 

904 x=range(len(self.reference_nodes)), 

905 height=reference_kurtosis, 

906 width=0.5, 

907 pen="r", 

908 brush="r", 

909 ) 

910 self.kurtosis_response_plot.addItem(response_bar) 

911 self.kurtosis_reference_plot.addItem(reference_bar) 

912 

913 def show_hide_coherence(self): 

914 """Sets the visibility of the coherence plots""" 

915 if self.system_id_widget.coherence_checkbox.isChecked(): 

916 self.system_id_widget.coherence_groupbox.show() 

917 else: 

918 self.system_id_widget.coherence_groupbox.hide() 

919 

920 def show_hide_levels(self): 

921 """Sets the visibility of the level plots""" 

922 if self.system_id_widget.levels_checkbox.isChecked(): 

923 self.system_id_widget.levels_groupbox.show() 

924 else: 

925 self.system_id_widget.levels_groupbox.hide() 

926 

927 def show_hide_time_data(self): 

928 """Sets the visibility of the time data plots""" 

929 if self.system_id_widget.time_data_checkbox.isChecked(): 

930 self.system_id_widget.time_data_groupbox.show() 

931 else: 

932 self.system_id_widget.time_data_groupbox.hide() 

933 

934 def show_hide_transfer_function(self): 

935 """Sets the visibility of the transfer function plots""" 

936 if self.system_id_widget.transfer_function_checkbox.isChecked(): 

937 self.system_id_widget.transfer_function_groupbox.show() 

938 else: 

939 self.system_id_widget.transfer_function_groupbox.hide() 

940 

941 def show_hide_impulse(self): 

942 """Sets the visibility of the impulse response plots""" 

943 if self.system_id_widget.impulse_checkbox.isChecked(): 

944 self.system_id_widget.impulse_groupbox.show() 

945 else: 

946 self.system_id_widget.impulse_groupbox.hide() 

947 

948 def show_hide_kurtosis(self): 

949 """Sets the visibility of the kurtosis plots""" 

950 if self.system_id_widget.kurtosis_checkbox.isChecked(): 

951 self.system_id_widget.kurtosis_groupbox.show() 

952 else: 

953 self.system_id_widget.kurtosis_groupbox.hide() 

954 

955 def update_signal_type(self): 

956 """Updates the UI widgets based on the type of signal that has been selected""" 

957 if self.system_id_widget.signalTypeComboBox.currentIndex() == 0: # Random 

958 self.system_id_widget.windowComboBox.setCurrentIndex(0) 

959 self.system_id_widget.overlapDoubleSpinBox.show() 

960 self.system_id_widget.overlapLabel.show() 

961 self.system_id_widget.onFractionLabel.hide() 

962 self.system_id_widget.onFractionDoubleSpinBox.hide() 

963 self.system_id_widget.pretriggerLabel.hide() 

964 self.system_id_widget.pretriggerDoubleSpinBox.hide() 

965 self.system_id_widget.rampFractionLabel.hide() 

966 self.system_id_widget.rampFractionDoubleSpinBox.hide() 

967 self.system_id_widget.bandwidthLabel.show() 

968 self.system_id_widget.lowFreqCutoffSpinBox.show() 

969 self.system_id_widget.highFreqCutoffSpinBox.show() 

970 elif self.system_id_widget.signalTypeComboBox.currentIndex() == 1: # Pseudorandom 

971 self.system_id_widget.windowComboBox.setCurrentIndex(1) 

972 self.system_id_widget.overlapDoubleSpinBox.hide() 

973 self.system_id_widget.overlapLabel.hide() 

974 self.system_id_widget.onFractionLabel.hide() 

975 self.system_id_widget.onFractionDoubleSpinBox.hide() 

976 self.system_id_widget.pretriggerLabel.hide() 

977 self.system_id_widget.pretriggerDoubleSpinBox.hide() 

978 self.system_id_widget.rampFractionLabel.hide() 

979 self.system_id_widget.rampFractionDoubleSpinBox.hide() 

980 self.system_id_widget.bandwidthLabel.show() 

981 self.system_id_widget.lowFreqCutoffSpinBox.show() 

982 self.system_id_widget.highFreqCutoffSpinBox.show() 

983 elif self.system_id_widget.signalTypeComboBox.currentIndex() == 2: # Burst 

984 self.system_id_widget.windowComboBox.setCurrentIndex(1) 

985 self.system_id_widget.overlapDoubleSpinBox.hide() 

986 self.system_id_widget.overlapLabel.hide() 

987 self.system_id_widget.onFractionLabel.show() 

988 self.system_id_widget.onFractionDoubleSpinBox.show() 

989 self.system_id_widget.pretriggerLabel.show() 

990 self.system_id_widget.pretriggerDoubleSpinBox.show() 

991 self.system_id_widget.rampFractionLabel.show() 

992 self.system_id_widget.rampFractionDoubleSpinBox.show() 

993 self.system_id_widget.bandwidthLabel.show() 

994 self.system_id_widget.lowFreqCutoffSpinBox.show() 

995 self.system_id_widget.highFreqCutoffSpinBox.show() 

996 elif self.system_id_widget.signalTypeComboBox.currentIndex() == 3: # Chirp 

997 self.system_id_widget.windowComboBox.setCurrentIndex(1) 

998 self.system_id_widget.overlapDoubleSpinBox.hide() 

999 self.system_id_widget.overlapLabel.hide() 

1000 self.system_id_widget.onFractionLabel.hide() 

1001 self.system_id_widget.onFractionDoubleSpinBox.hide() 

1002 self.system_id_widget.pretriggerLabel.hide() 

1003 self.system_id_widget.pretriggerDoubleSpinBox.hide() 

1004 self.system_id_widget.rampFractionLabel.hide() 

1005 self.system_id_widget.rampFractionDoubleSpinBox.hide() 

1006 self.system_id_widget.bandwidthLabel.hide() 

1007 self.system_id_widget.lowFreqCutoffSpinBox.hide() 

1008 self.system_id_widget.highFreqCutoffSpinBox.hide() 

1009 

1010 @abstractmethod 

1011 def retrieve_metadata( 

1012 self, 

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

1014 environment_name: str = None, 

1015 ) -> nc4._netCDF4.Group: # pylint: disable=c-extension-no-member 

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

1017 

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

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

1020 in the user interface with the proper information. 

1021 

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

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

1024 the netCDF file to document the metadata. 

1025 

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

1027 should collect parameters pertaining to the environment from a Group 

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

1029 

1030 Parameters 

1031 ---------- 

1032 netcdf_handle : nc4._netCDF4.Dataset : 

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

1034 a group name with the enviroment's name. 

1035 

1036 environment_name : str : (optional) 

1037 The netCDF group name from which the data will be read. This will override 

1038 the current environment's name if given. 

1039 

1040 Returns 

1041 ------- 

1042 group : nc4._netCDF4.Group 

1043 The netCDF group that was used to set the system ID parameters 

1044 """ 

1045 # Get the group 

1046 group = netcdf_handle.groups[ 

1047 self.environment_name if environment_name is None else environment_name 

1048 ] 

1049 self.system_id_widget.samplesPerFrameSpinBox.setValue(group.sysid_frame_size) 

1050 self.system_id_widget.averagingTypeComboBox.setCurrentIndex( 

1051 self.system_id_widget.averagingTypeComboBox.findText(group.sysid_averaging_type) 

1052 ) 

1053 self.system_id_widget.noiseAveragesSpinBox.setValue(group.sysid_noise_averages) 

1054 self.system_id_widget.systemIDAveragesSpinBox.setValue(group.sysid_averages) 

1055 self.system_id_widget.averagingCoefficientDoubleSpinBox.setValue( 

1056 group.sysid_exponential_averaging_coefficient 

1057 ) 

1058 self.system_id_widget.estimatorComboBox.setCurrentIndex( 

1059 self.system_id_widget.estimatorComboBox.findText(group.sysid_estimator) 

1060 ) 

1061 self.system_id_widget.levelDoubleSpinBox.setValue(group.sysid_level) 

1062 self.system_id_widget.levelRampTimeDoubleSpinBox.setValue(group.sysid_level_ramp_time) 

1063 self.system_id_widget.signalTypeComboBox.setCurrentIndex( 

1064 self.system_id_widget.signalTypeComboBox.findText(group.sysid_signal_type) 

1065 ) 

1066 self.system_id_widget.windowComboBox.setCurrentIndex( 

1067 self.system_id_widget.windowComboBox.findText(group.sysid_window) 

1068 ) 

1069 self.system_id_widget.overlapDoubleSpinBox.setValue(group.sysid_overlap * 100) 

1070 self.system_id_widget.onFractionDoubleSpinBox.setValue(group.sysid_burst_on * 100) 

1071 self.system_id_widget.pretriggerDoubleSpinBox.setValue(group.sysid_pretrigger * 100) 

1072 self.system_id_widget.rampFractionDoubleSpinBox.setValue( 

1073 group.sysid_burst_ramp_fraction * 100 

1074 ) 

1075 if hasattr(group, "sysid_low_frequency_cutoff"): 

1076 self.system_id_widget.lowFreqCutoffSpinBox.setValue(group.sysid_low_frequency_cutoff) 

1077 if hasattr(group, "sysid_high_frequency_cutoff"): 

1078 self.system_id_widget.highFreqCutoffSpinBox.setValue(group.sysid_high_frequency_cutoff) 

1079 return group 

1080 

1081 @abstractmethod 

1082 def update_gui(self, queue_data: tuple): 

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

1084 

1085 This function will receive data from the gui_update_queue that 

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

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

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

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

1090 

1091 Parameters 

1092 ---------- 

1093 queue_data : tuple 

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

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

1096 the data used to perform the operation. 

1097 """ 

1098 message, data = queue_data 

1099 self.log(f"Got GUI Message {message}") 

1100 # print('Update GUI Got {:}'.format(message)) 

1101 if message == "time_frame": 

1102 self.last_time_response, accept = data 

1103 self.update_sysid_plots( 

1104 update_time=True, 

1105 update_transfer_function=False, 

1106 update_noise=False, 

1107 update_kurtosis=False, 

1108 ) 

1109 elif message == "kurtosis": 

1110 self.last_kurtosis = data 

1111 self.update_sysid_plots( 

1112 update_time=False, 

1113 update_transfer_function=False, 

1114 update_noise=False, 

1115 update_kurtosis=True, 

1116 ) 

1117 elif message == "noise_update": 

1118 ( 

1119 frames, 

1120 total_frames, 

1121 self.frequencies, 

1122 self.last_response_noise, 

1123 self.last_reference_noise, 

1124 ) = data 

1125 self.update_sysid_plots( 

1126 update_time=False, 

1127 update_transfer_function=False, 

1128 update_noise=True, 

1129 update_kurtosis=False, 

1130 ) 

1131 self.system_id_widget.current_frames_spinbox.setValue(frames) 

1132 self.system_id_widget.total_frames_spinbox.setValue(total_frames) 

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

1134 elif message == "sysid_update": 

1135 ( 

1136 frames, 

1137 total_frames, 

1138 self.frequencies, 

1139 self.last_transfer_function, 

1140 self.last_coherence, 

1141 self.last_response_cpsd, 

1142 self.last_reference_cpsd, 

1143 self.last_condition, 

1144 ) = data 

1145 # print(self.last_transfer_function.shape) 

1146 # print(self.last_coherence.shape) 

1147 # print(self.last_response_cpsd.shape) 

1148 # print(self.last_reference_cpsd.shape) 

1149 self.update_sysid_plots( 

1150 update_time=False, 

1151 update_transfer_function=True, 

1152 update_noise=True, 

1153 update_kurtosis=False, 

1154 ) 

1155 self.system_id_widget.current_frames_spinbox.setValue(frames) 

1156 self.system_id_widget.total_frames_spinbox.setValue(total_frames) 

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

1158 elif message == "enable_system_id": 

1159 for widget in [ 

1160 self.system_id_widget.preview_noise_button, 

1161 self.system_id_widget.preview_system_id_button, 

1162 self.system_id_widget.start_button, 

1163 self.system_id_widget.samplesPerFrameSpinBox, 

1164 self.system_id_widget.averagingTypeComboBox, 

1165 self.system_id_widget.noiseAveragesSpinBox, 

1166 self.system_id_widget.systemIDAveragesSpinBox, 

1167 self.system_id_widget.averagingCoefficientDoubleSpinBox, 

1168 self.system_id_widget.estimatorComboBox, 

1169 self.system_id_widget.levelDoubleSpinBox, 

1170 self.system_id_widget.signalTypeComboBox, 

1171 self.system_id_widget.windowComboBox, 

1172 self.system_id_widget.overlapDoubleSpinBox, 

1173 self.system_id_widget.onFractionDoubleSpinBox, 

1174 self.system_id_widget.pretriggerDoubleSpinBox, 

1175 self.system_id_widget.rampFractionDoubleSpinBox, 

1176 self.system_id_widget.stream_transfer_function_data_checkbox, 

1177 self.system_id_widget.select_transfer_function_stream_file_button, 

1178 self.system_id_widget.transfer_function_stream_file_display, 

1179 self.system_id_widget.levelRampTimeDoubleSpinBox, 

1180 self.system_id_widget.save_system_id_matrices_button, 

1181 self.system_id_widget.load_system_id_matrices_button, 

1182 self.system_id_widget.lowFreqCutoffSpinBox, 

1183 self.system_id_widget.highFreqCutoffSpinBox, 

1184 ]: 

1185 widget.setEnabled(True) 

1186 for widget in [self.system_id_widget.stop_button]: 

1187 widget.setEnabled(False) 

1188 elif message == "disable_system_id": 

1189 for widget in [ 

1190 self.system_id_widget.preview_noise_button, 

1191 self.system_id_widget.preview_system_id_button, 

1192 self.system_id_widget.start_button, 

1193 self.system_id_widget.samplesPerFrameSpinBox, 

1194 self.system_id_widget.averagingTypeComboBox, 

1195 self.system_id_widget.noiseAveragesSpinBox, 

1196 self.system_id_widget.systemIDAveragesSpinBox, 

1197 self.system_id_widget.averagingCoefficientDoubleSpinBox, 

1198 self.system_id_widget.estimatorComboBox, 

1199 self.system_id_widget.levelDoubleSpinBox, 

1200 self.system_id_widget.signalTypeComboBox, 

1201 self.system_id_widget.windowComboBox, 

1202 self.system_id_widget.overlapDoubleSpinBox, 

1203 self.system_id_widget.onFractionDoubleSpinBox, 

1204 self.system_id_widget.pretriggerDoubleSpinBox, 

1205 self.system_id_widget.rampFractionDoubleSpinBox, 

1206 self.system_id_widget.stream_transfer_function_data_checkbox, 

1207 self.system_id_widget.select_transfer_function_stream_file_button, 

1208 self.system_id_widget.transfer_function_stream_file_display, 

1209 self.system_id_widget.levelRampTimeDoubleSpinBox, 

1210 self.system_id_widget.save_system_id_matrices_button, 

1211 self.system_id_widget.load_system_id_matrices_button, 

1212 self.system_id_widget.lowFreqCutoffSpinBox, 

1213 self.system_id_widget.highFreqCutoffSpinBox, 

1214 ]: 

1215 widget.setEnabled(False) 

1216 for widget in [self.system_id_widget.stop_button]: 

1217 widget.setEnabled(True) 

1218 else: 

1219 return False 

1220 return True 

1221 

1222 @staticmethod 

1223 @abstractmethod 

1224 def create_environment_template( 

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

1226 ): 

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

1228 environment. 

1229 

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

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

1232 environment. 

1233 

1234 This function is the "write" counterpart to the 

1235 ``set_parameters_from_template`` function in the ``AbstractUI`` class, 

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

1237 interface. 

1238 

1239 Parameters 

1240 ---------- 

1241 environment_name : str : 

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

1243 workbook : openpyxl.workbook.workbook.Workbook : 

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

1245 

1246 """ 

1247 

1248 @abstractmethod 

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

1250 """ 

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

1252 

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

1254 environment. Cells on this worksheet contain parameters needed to 

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

1256 update the UI widgets with those parameters. 

1257 

1258 This function is the "read" counterpart to the 

1259 ``create_environment_template`` function in the ``AbstractUI`` class, 

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

1261 

1262 

1263 Parameters 

1264 ---------- 

1265 worksheet : openpyxl.worksheet.worksheet.Worksheet 

1266 An openpyxl worksheet that contains the environment template. 

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

1268 user interface. 

1269 

1270 """ 

1271 

1272 def save_sysid_matrix_file(self): 

1273 """Saves out system identification data to a file""" 

1274 if self.last_transfer_function is None or self.last_response_noise is None: 

1275 error_message_qt( 

1276 "Run System Identification First!", 

1277 "System Identification Matrices not yet created.\n\n" 

1278 "Run System Identification First!", 

1279 ) 

1280 return 

1281 filename, file_filter = QtWidgets.QFileDialog.getSaveFileName( 

1282 self.system_id_widget, 

1283 "Select File to Save Transfer Function Matrices", 

1284 filter="NetCDF File (*.nc4);;MatLab File (*.mat);;Numpy File (*.npz)", 

1285 ) 

1286 labels = [ 

1287 ["node_number", str], 

1288 ["node_direction", str], 

1289 ["comment", str], 

1290 ["serial_number", str], 

1291 ["triax_dof", str], 

1292 ["sensitivity", str], 

1293 ["unit", str], 

1294 ["make", str], 

1295 ["model", str], 

1296 ["expiration", str], 

1297 ["physical_device", str], 

1298 ["physical_channel", str], 

1299 ["channel_type", str], 

1300 ["minimum_value", str], 

1301 ["maximum_value", str], 

1302 ["coupling", str], 

1303 ["excitation_source", str], 

1304 ["excitation", str], 

1305 ["feedback_device", str], 

1306 ["feedback_channel", str], 

1307 ["warning_level", str], 

1308 ["abort_level", str], 

1309 ] 

1310 if file_filter == "NetCDF File (*.nc4)": 

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

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

1313 ) 

1314 # Create dimensions 

1315 netcdf_handle.createDimension( 

1316 "response_channels", len(self.data_acquisition_parameters.channel_list) 

1317 ) 

1318 

1319 netcdf_handle.createDimension( 

1320 "num_environments", 

1321 len(self.data_acquisition_parameters.environment_names), 

1322 ) 

1323 # Create attributes 

1324 netcdf_handle.file_version = "3.0.0" 

1325 netcdf_handle.sample_rate = self.data_acquisition_parameters.sample_rate 

1326 netcdf_handle.time_per_write = ( 

1327 self.data_acquisition_parameters.samples_per_write 

1328 / self.data_acquisition_parameters.output_sample_rate 

1329 ) 

1330 netcdf_handle.time_per_read = ( 

1331 self.data_acquisition_parameters.samples_per_read 

1332 / self.data_acquisition_parameters.sample_rate 

1333 ) 

1334 netcdf_handle.hardware = self.data_acquisition_parameters.hardware 

1335 netcdf_handle.hardware_file = ( 

1336 "None" 

1337 if self.data_acquisition_parameters.hardware_file is None 

1338 else self.data_acquisition_parameters.hardware_file 

1339 ) 

1340 netcdf_handle.output_oversample = self.data_acquisition_parameters.output_oversample 

1341 for ( 

1342 name, 

1343 value, 

1344 ) in self.data_acquisition_parameters.extra_parameters.items(): 

1345 setattr(netcdf_handle, name, value) 

1346 # Create Variables 

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

1348 this_environment_index = None 

1349 for i, name in enumerate(self.data_acquisition_parameters.environment_names): 

1350 var[i] = name 

1351 if name == self.environment_name: 

1352 this_environment_index = i 

1353 var = netcdf_handle.createVariable( 

1354 "environment_active_channels", 

1355 "i1", 

1356 ("response_channels", "num_environments"), 

1357 ) 

1358 var[...] = self.data_acquisition_parameters.environment_active_channels.astype("int8")[ 

1359 self.data_acquisition_parameters.environment_active_channels[ 

1360 :, this_environment_index 

1361 ], 

1362 :, 

1363 ] 

1364 # Create channel table variables 

1365 for label, netcdf_datatype in labels: 

1366 var = netcdf_handle.createVariable( 

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

1368 ) 

1369 channel_data = [ 

1370 getattr(channel, label) 

1371 for channel in self.data_acquisition_parameters.channel_list 

1372 ] 

1373 if netcdf_datatype == "i1": 

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

1375 else: 

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

1377 for i, cd in enumerate(channel_data): 

1378 var[i] = cd 

1379 group_handle = netcdf_handle.createGroup(self.environment_name) 

1380 self.environment_parameters.store_to_netcdf(group_handle) 

1381 try: 

1382 group_handle.createDimension( 

1383 "sysid_control_channels", self.last_transfer_function.shape[1] 

1384 ) 

1385 except RuntimeError: 

1386 pass 

1387 try: 

1388 group_handle.createDimension( 

1389 "sysid_output_channels", self.last_transfer_function.shape[2] 

1390 ) 

1391 except RuntimeError: 

1392 pass 

1393 try: 

1394 group_handle.createDimension( 

1395 "sysid_fft_lines", self.last_transfer_function.shape[0] 

1396 ) 

1397 except RuntimeError: 

1398 pass 

1399 var = group_handle.createVariable( 

1400 "frf_data_real", 

1401 "f8", 

1402 ("sysid_fft_lines", "sysid_control_channels", "sysid_output_channels"), 

1403 ) 

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

1405 var = group_handle.createVariable( 

1406 "frf_data_imag", 

1407 "f8", 

1408 ("sysid_fft_lines", "sysid_control_channels", "sysid_output_channels"), 

1409 ) 

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

1411 var = group_handle.createVariable( 

1412 "frf_coherence", "f8", ("sysid_fft_lines", "sysid_control_channels") 

1413 ) 

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

1415 var = group_handle.createVariable( 

1416 "response_cpsd_real", 

1417 "f8", 

1418 ("sysid_fft_lines", "sysid_control_channels", "sysid_control_channels"), 

1419 ) 

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

1421 var = group_handle.createVariable( 

1422 "response_cpsd_imag", 

1423 "f8", 

1424 ("sysid_fft_lines", "sysid_control_channels", "sysid_control_channels"), 

1425 ) 

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

1427 var = group_handle.createVariable( 

1428 "reference_cpsd_real", 

1429 "f8", 

1430 ("sysid_fft_lines", "sysid_output_channels", "sysid_output_channels"), 

1431 ) 

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

1433 var = group_handle.createVariable( 

1434 "reference_cpsd_imag", 

1435 "f8", 

1436 ("sysid_fft_lines", "sysid_output_channels", "sysid_output_channels"), 

1437 ) 

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

1439 var = group_handle.createVariable( 

1440 "response_noise_cpsd_real", 

1441 "f8", 

1442 ("sysid_fft_lines", "sysid_control_channels", "sysid_control_channels"), 

1443 ) 

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

1445 var = group_handle.createVariable( 

1446 "response_noise_cpsd_imag", 

1447 "f8", 

1448 ("sysid_fft_lines", "sysid_control_channels", "sysid_control_channels"), 

1449 ) 

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

1451 var = group_handle.createVariable( 

1452 "reference_noise_cpsd_real", 

1453 "f8", 

1454 ("sysid_fft_lines", "sysid_output_channels", "sysid_output_channels"), 

1455 ) 

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

1457 var = group_handle.createVariable( 

1458 "reference_noise_cpsd_imag", 

1459 "f8", 

1460 ("sysid_fft_lines", "sysid_output_channels", "sysid_output_channels"), 

1461 ) 

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

1463 else: 

1464 field_dict = {} 

1465 field_dict["version"] = "3.0.0" 

1466 field_dict["sample_rate"] = self.data_acquisition_parameters.sample_rate 

1467 field_dict["time_per_write"] = ( 

1468 self.data_acquisition_parameters.samples_per_write 

1469 / self.data_acquisition_parameters.output_sample_rate 

1470 ) 

1471 field_dict["time_per_read"] = ( 

1472 self.data_acquisition_parameters.samples_per_read 

1473 / self.data_acquisition_parameters.sample_rate 

1474 ) 

1475 field_dict["hardware"] = self.data_acquisition_parameters.hardware 

1476 field_dict["hardware_file"] = ( 

1477 "None" 

1478 if self.data_acquisition_parameters.hardware_file is None 

1479 else self.data_acquisition_parameters.hardware_file 

1480 ) 

1481 field_dict["output_oversample"] = self.data_acquisition_parameters.output_oversample 

1482 field_dict["frf_data"] = self.last_transfer_function 

1483 field_dict["response_cpsd"] = self.last_response_cpsd 

1484 field_dict["reference_cpsd"] = self.last_reference_cpsd 

1485 field_dict["coherence"] = self.last_coherence 

1486 field_dict["response_noise_cpsd"] = self.last_response_noise 

1487 field_dict["reference_noise_cpsd"] = self.last_reference_noise 

1488 field_dict["response_indices"] = self.environment_parameters.response_channel_indices 

1489 field_dict["reference_indices"] = self.environment_parameters.reference_channel_indices 

1490 field_dict["response_transformation_matrix"] = ( 

1491 np.nan 

1492 if self.environment_parameters.response_transformation_matrix is None 

1493 else self.environment_parameters.response_transformation_matrix 

1494 ) 

1495 field_dict["reference_transformation_matrix"] = ( 

1496 np.nan 

1497 if self.environment_parameters.reference_transformation_matrix is None 

1498 else self.environment_parameters.reference_transformation_matrix 

1499 ) 

1500 field_dict["sysid_frequency_spacing"] = ( 

1501 self.environment_parameters.sysid_frequency_spacing 

1502 ) 

1503 field_dict.update(self.data_acquisition_parameters.extra_parameters) 

1504 for key, value in self.environment_parameters.__dict__.items(): 

1505 try: 

1506 if "sysid_" in key: 

1507 field_dict[key] = np.array(value) 

1508 except TypeError: 

1509 continue 

1510 for label, _ in labels: 

1511 field_dict["channel_" + label] = np.array( 

1512 [ 

1513 ("" if getattr(channel, label) is None else getattr(channel, label)) 

1514 for channel in self.data_acquisition_parameters.channel_list 

1515 ] 

1516 ) 

1517 # print(field_dict) 

1518 if file_filter == "MatLab File (*.mat)": 

1519 for field in [ 

1520 "frf_data", 

1521 "response_cpsd", 

1522 "reference_cpsd", 

1523 "coherence", 

1524 "response_noise_cpsd", 

1525 "reference_noise_cpsd", 

1526 ]: 

1527 field_dict[field] = np.moveaxis(field_dict[field], 0, -1) 

1528 savemat(filename, field_dict) 

1529 elif file_filter == "Numpy File (*.npz)": 

1530 np.savez(filename, **field_dict) 

1531 

1532 def load_sysid_matrix_file(self, filename, popup=True): 

1533 """Loads a system identification dataset from previous analysis or testing 

1534 

1535 Parameters 

1536 ---------- 

1537 filename : str 

1538 The filename of the system identification file to load 

1539 popup : bool, optional 

1540 If True, bring up a file selection dialog box instead of using filename, by default True 

1541 

1542 Raises 

1543 ------ 

1544 ValueError 

1545 If the wrong type of file is loaded 

1546 """ 

1547 if popup: 

1548 filename, file_filter = QtWidgets.QFileDialog.getOpenFileName( 

1549 self.system_id_widget, 

1550 "Select File to Load Transfer Function Matrices", 

1551 filter="NetCDF File (*.nc4);;MatLab File (*.mat);;Numpy File (*.npz);;" 

1552 "SDynPy FRF (*.npz);;Forcefinder SPR (*.npz)", 

1553 ) 

1554 else: 

1555 file_filter = None 

1556 if filename is None or filename == "": 

1557 return 

1558 elif file_filter == "NetCDF File (*.nc4)" or ( 

1559 file_filter is None and filename.endswith(".nc4") 

1560 ): 

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

1562 filename, "r", format="NETCDF4" 

1563 ) 

1564 # TODO: error checking to make sure relevant info matches current controller state 

1565 group_handle = netcdf_handle[self.environment_name] 

1566 sample_rate = netcdf_handle.sample_rate 

1567 frame_size = group_handle.sysid_frame_size 

1568 fft_lines = group_handle.dimensions["fft_lines"].size 

1569 variables = group_handle.variables 

1570 combine = np.vectorize(complex) 

1571 try: 

1572 self.last_transfer_function = np.array( 

1573 combine(variables["frf_data_real"][:], variables["frf_data_imag"][:]) 

1574 ) 

1575 self.last_coherence = np.array(variables["frf_coherence"][:]) 

1576 self.last_response_cpsd = np.array( 

1577 combine( 

1578 variables["response_cpsd_real"][:], 

1579 variables["response_cpsd_imag"][:], 

1580 ) 

1581 ) 

1582 self.last_reference_cpsd = np.array( 

1583 combine( 

1584 variables["reference_cpsd_real"][:], 

1585 variables["reference_cpsd_imag"][:], 

1586 ) 

1587 ) 

1588 self.last_response_noise = np.array( 

1589 combine( 

1590 variables["response_noise_cpsd_real"][:], 

1591 variables["response_noise_cpsd_imag"][:], 

1592 ) 

1593 ) 

1594 self.last_reference_noise = np.array( 

1595 combine( 

1596 variables["reference_noise_cpsd_real"][:], 

1597 variables["reference_noise_cpsd_imag"][:], 

1598 ) 

1599 ) 

1600 self.last_condition = np.linalg.cond(self.last_transfer_function) 

1601 self.frequencies = np.arange(fft_lines) * sample_rate / frame_size 

1602 except KeyError: 

1603 # TODO: in the case that a time history file was chosen, should FRF be 

1604 # auto-computed? could work on environment run or sysid (environment run just 

1605 # may have poor FRF) 

1606 # could we use the data analysis process to do the computation? so we don't 

1607 # lock up the UI 

1608 # could we also pass the FRF to any virtual hardware? 

1609 return 

1610 elif file_filter == "SDynPy FRF (*.npz)": 

1611 sdynpy_dict = np.load(filename) 

1612 if sdynpy_dict["function_type"].item() != 4: 

1613 raise ValueError("File must contain a Sdynpy FrequencyResponseFunctionArray") 

1614 self.last_transfer_function = np.moveaxis( 

1615 np.array(sdynpy_dict["data"]["ordinate"]), -1, 0 

1616 ) 

1617 self.last_condition = np.linalg.cond(self.last_transfer_function) 

1618 self.frequencies = np.array(sdynpy_dict["data"]["abscissa"][0][0]) 

1619 self.last_coherence = np.zeros((0, self.last_transfer_function.shape[1])) 

1620 # TODO: pull coordinate out to verify matching info 

1621 elif file_filter == "Forcefinder SPR (*.npz)": 

1622 forcefinder_dict = np.load(filename) 

1623 self.last_transfer_function = np.array( 

1624 forcefinder_dict["training_frf"] 

1625 ) # training frf will generally be the one used for testing 

1626 self.last_condition = np.linalg.cond(self.last_transfer_function) 

1627 self.frequencies = np.array(forcefinder_dict["abscissa"]) 

1628 self.last_coherence = np.zeros((0, self.last_transfer_function.shape[1])) 

1629 if "buzz_cpsd" in forcefinder_dict: 

1630 self.last_response_cpsd = np.array(forcefinder_dict["buzz_cpsd"]) 

1631 else: 

1632 if file_filter == "MatLab File (*.mat)": 

1633 field_dict = loadmat(filename) 

1634 for field in [ 

1635 "frf_data", 

1636 "response_cpsd", 

1637 "reference_cpsd", 

1638 "coherence", 

1639 "response_noise_cpsd", 

1640 "reference_noise_cpsd", 

1641 ]: 

1642 field_dict[field] = np.moveaxis(field_dict[field], -1, 0) 

1643 elif file_filter == "Numpy File (*.npz)": 

1644 field_dict = np.load(filename) 

1645 self.last_transfer_function = np.array(field_dict["frf_data"]) 

1646 self.last_response_cpsd = np.array(field_dict["response_cpsd"]) 

1647 self.last_reference_cpsd = np.array(field_dict["reference_cpsd"]) 

1648 self.last_coherence = np.array(field_dict["coherence"]) 

1649 self.last_response_noise = np.array(field_dict["response_noise_cpsd"]) 

1650 self.last_reference_noise = np.array(field_dict["reference_noise_cpsd"]) 

1651 self.last_condition = np.linalg.cond(self.last_transfer_function) 

1652 self.frequencies = ( 

1653 np.arange(self.last_transfer_function.shape[0]) 

1654 * field_dict["sysid_frequency_spacing"].squeeze() 

1655 ) 

1656 # Send values to data analysis process (through the 

1657 # environment queue, environment then passes to data analysis) 

1658 self.environment_command_queue.put( 

1659 self.log_name, 

1660 ( 

1661 SysIDDataAnalysisCommands.LOAD_NOISE, 

1662 ( 

1663 0, 

1664 self.frequencies, 

1665 None, 

1666 None, 

1667 self.last_response_noise, 

1668 self.last_reference_noise, 

1669 None, 

1670 ), 

1671 ), 

1672 ) 

1673 self.environment_command_queue.put( 

1674 self.log_name, 

1675 ( 

1676 SysIDDataAnalysisCommands.LOAD_TRANSFER_FUNCTION, 

1677 ( 

1678 0, 

1679 self.frequencies, 

1680 self.last_transfer_function, 

1681 self.last_coherence, 

1682 self.last_response_cpsd, 

1683 self.last_reference_cpsd, 

1684 self.last_condition, 

1685 ), 

1686 ), 

1687 ) 

1688 self.update_sysid_plots( 

1689 update_time=False, 

1690 update_transfer_function=True, 

1691 update_noise=True, 

1692 update_kurtosis=False, 

1693 ) 

1694 self.system_id_widget.current_frames_spinbox.setValue(0) 

1695 self.system_id_widget.total_frames_spinbox.setValue(0) 

1696 self.system_id_widget.progressBar.setValue(100) 

1697 

1698 def disable_system_id_daq_armed(self): 

1699 """Disables widget on the UI due to the data acquisition being in use""" 

1700 for widget in [ 

1701 self.system_id_widget.preview_noise_button, 

1702 self.system_id_widget.preview_system_id_button, 

1703 self.system_id_widget.start_button, 

1704 self.system_id_widget.samplesPerFrameSpinBox, 

1705 self.system_id_widget.averagingTypeComboBox, 

1706 self.system_id_widget.noiseAveragesSpinBox, 

1707 self.system_id_widget.systemIDAveragesSpinBox, 

1708 self.system_id_widget.averagingCoefficientDoubleSpinBox, 

1709 self.system_id_widget.estimatorComboBox, 

1710 self.system_id_widget.levelDoubleSpinBox, 

1711 self.system_id_widget.signalTypeComboBox, 

1712 self.system_id_widget.windowComboBox, 

1713 self.system_id_widget.overlapDoubleSpinBox, 

1714 self.system_id_widget.onFractionDoubleSpinBox, 

1715 self.system_id_widget.pretriggerDoubleSpinBox, 

1716 self.system_id_widget.rampFractionDoubleSpinBox, 

1717 self.system_id_widget.stream_transfer_function_data_checkbox, 

1718 self.system_id_widget.select_transfer_function_stream_file_button, 

1719 self.system_id_widget.transfer_function_stream_file_display, 

1720 self.system_id_widget.levelRampTimeDoubleSpinBox, 

1721 self.system_id_widget.save_system_id_matrices_button, 

1722 self.system_id_widget.load_system_id_matrices_button, 

1723 self.system_id_widget.lowFreqCutoffSpinBox, 

1724 self.system_id_widget.highFreqCutoffSpinBox, 

1725 ]: 

1726 widget.setEnabled(False) 

1727 for widget in [self.system_id_widget.stop_button]: 

1728 widget.setEnabled(False) 

1729 

1730 def enable_system_id_daq_disarmed(self): 

1731 """Enables widgets on the UI due to the data acquisition being no longer in use""" 

1732 for widget in [ 

1733 self.system_id_widget.preview_noise_button, 

1734 self.system_id_widget.preview_system_id_button, 

1735 self.system_id_widget.start_button, 

1736 self.system_id_widget.samplesPerFrameSpinBox, 

1737 self.system_id_widget.averagingTypeComboBox, 

1738 self.system_id_widget.noiseAveragesSpinBox, 

1739 self.system_id_widget.systemIDAveragesSpinBox, 

1740 self.system_id_widget.averagingCoefficientDoubleSpinBox, 

1741 self.system_id_widget.estimatorComboBox, 

1742 self.system_id_widget.levelDoubleSpinBox, 

1743 self.system_id_widget.signalTypeComboBox, 

1744 self.system_id_widget.windowComboBox, 

1745 self.system_id_widget.overlapDoubleSpinBox, 

1746 self.system_id_widget.onFractionDoubleSpinBox, 

1747 self.system_id_widget.pretriggerDoubleSpinBox, 

1748 self.system_id_widget.rampFractionDoubleSpinBox, 

1749 self.system_id_widget.stream_transfer_function_data_checkbox, 

1750 self.system_id_widget.select_transfer_function_stream_file_button, 

1751 self.system_id_widget.transfer_function_stream_file_display, 

1752 self.system_id_widget.levelRampTimeDoubleSpinBox, 

1753 self.system_id_widget.save_system_id_matrices_button, 

1754 self.system_id_widget.load_system_id_matrices_button, 

1755 self.system_id_widget.lowFreqCutoffSpinBox, 

1756 self.system_id_widget.highFreqCutoffSpinBox, 

1757 ]: 

1758 widget.setEnabled(True) 

1759 for widget in [self.system_id_widget.stop_button]: 

1760 widget.setEnabled(False) 

1761 

1762 

1763class AbstractSysIdEnvironment(AbstractEnvironment): 

1764 """Abstract Environment class defining the interface with the controller 

1765 

1766 This class is used to define the operation of an environment within the 

1767 controller, which must be completed by subclasses inheriting from this 

1768 class. Children of this class will sit in a While loop in the 

1769 ``AbstractEnvironment.run()`` function. While in this loop, the 

1770 Environment will pull instructions and data from the 

1771 ``command_queue`` and then use the ``command_map`` to map those instructions 

1772 to functions in the class. 

1773 

1774 All child classes inheriting from AbstractEnvironment will require functions 

1775 to be defined for global operations of the controller, which are already 

1776 mapped in the ``command_map``. Any additional operations must be defined 

1777 by functions and then added to the command_map when initilizing the child 

1778 class. 

1779 

1780 All functions called via the ``command_map`` must accept one input argument 

1781 which is the data passed along with the command. For functions that do not 

1782 require additional data, this argument can be ignored, but it must still be 

1783 present in the function's calling signature. 

1784 

1785 The run function will continue until one of the functions called by 

1786 ``command_map`` returns a truthy value, which signifies the controller to 

1787 quit. Therefore, any functions mapped to ``command_map`` that should not 

1788 instruct the program to quit should not return any value that could be 

1789 interpreted as true.""" 

1790 

1791 def __init__( 

1792 self, 

1793 environment_name: str, 

1794 command_queue: VerboseMessageQueue, 

1795 gui_update_queue: Queue, 

1796 controller_communication_queue: VerboseMessageQueue, 

1797 log_file_queue: Queue, 

1798 collector_command_queue: VerboseMessageQueue, 

1799 signal_generator_command_queue: VerboseMessageQueue, 

1800 spectral_processing_command_queue: VerboseMessageQueue, 

1801 data_analysis_command_queue: VerboseMessageQueue, 

1802 data_in_queue: Queue, 

1803 data_out_queue: Queue, 

1804 acquisition_active: mp.sharedctypes.Synchronized, 

1805 output_active: mp.sharedctypes.Synchronized, 

1806 ): 

1807 super().__init__( 

1808 environment_name, 

1809 command_queue, 

1810 gui_update_queue, 

1811 controller_communication_queue, 

1812 log_file_queue, 

1813 data_in_queue, 

1814 data_out_queue, 

1815 acquisition_active, 

1816 output_active, 

1817 ) 

1818 self.map_command(SystemIdCommands.PREVIEW_NOISE, self.preview_noise) 

1819 self.map_command(SystemIdCommands.PREVIEW_TRANSFER_FUNCTION, self.preview_transfer_function) 

1820 self.map_command(SystemIdCommands.START_SYSTEM_ID, self.start_noise) 

1821 self.map_command(SystemIdCommands.STOP_SYSTEM_ID, self.stop_system_id) 

1822 self.map_command( 

1823 SignalGenerationCommands.SHUTDOWN_ACHIEVED, self.siggen_shutdown_achieved_fn 

1824 ) 

1825 self.map_command( 

1826 DataCollectorCommands.SHUTDOWN_ACHIEVED, self.collector_shutdown_achieved_fn 

1827 ) 

1828 self.map_command( 

1829 SpectralProcessingCommands.SHUTDOWN_ACHIEVED, 

1830 self.spectral_shutdown_achieved_fn, 

1831 ) 

1832 self.map_command( 

1833 SysIDDataAnalysisCommands.SHUTDOWN_ACHIEVED, 

1834 self.analysis_shutdown_achieved_fn, 

1835 ) 

1836 self.map_command(SysIDDataAnalysisCommands.START_SHUTDOWN, self.stop_system_id) 

1837 self.map_command( 

1838 SysIDDataAnalysisCommands.START_SHUTDOWN_AND_RUN_SYSID, 

1839 self.start_shutdown_and_run_sysid, 

1840 ) 

1841 self.map_command(SysIDDataAnalysisCommands.SYSTEM_ID_COMPLETE, self.system_id_complete) 

1842 self.map_command(SysIDDataAnalysisCommands.LOAD_NOISE, self.load_noise) 

1843 self.map_command( 

1844 SysIDDataAnalysisCommands.LOAD_TRANSFER_FUNCTION, 

1845 self.load_transfer_function, 

1846 ) 

1847 self.map_command( 

1848 SystemIdCommands.CHECK_FOR_COMPLETE_SHUTDOWN, self.check_for_sysid_shutdown 

1849 ) 

1850 self._waiting_to_start_transfer_function = False 

1851 self.collector_command_queue = collector_command_queue 

1852 self.signal_generator_command_queue = signal_generator_command_queue 

1853 self.spectral_processing_command_queue = spectral_processing_command_queue 

1854 self.data_analysis_command_queue = data_analysis_command_queue 

1855 self.data_acquisition_parameters = None 

1856 self.environment_parameters = None 

1857 self.collector_shutdown_achieved = True 

1858 self.spectral_shutdown_achieved = True 

1859 self.siggen_shutdown_achieved = True 

1860 self.analysis_shutdown_achieved = True 

1861 self._sysid_stream_name = None 

1862 

1863 def initialize_data_acquisition_parameters( 

1864 self, data_acquisition_parameters: DataAcquisitionParameters 

1865 ): 

1866 """Initialize the data acquisition parameters in the environment. 

1867 

1868 The environment will receive the global data acquisition parameters from 

1869 the controller, and must set itself up accordingly. 

1870 

1871 Parameters 

1872 ---------- 

1873 data_acquisition_parameters : DataAcquisitionParameters : 

1874 A container containing data acquisition parameters, including 

1875 channels active in the environment as well as sampling parameters. 

1876 """ 

1877 self.data_acquisition_parameters = data_acquisition_parameters 

1878 

1879 def initialize_environment_test_parameters(self, environment_parameters: AbstractSysIdMetadata): 

1880 """ 

1881 Initialize the environment parameters specific to this environment 

1882 

1883 The environment will recieve parameters defining itself from the 

1884 user interface and must set itself up accordingly. 

1885 

1886 Parameters 

1887 ---------- 

1888 environment_parameters : AbstractMetadata 

1889 A container containing the parameters defining the environment 

1890 

1891 """ 

1892 self.environment_parameters = environment_parameters 

1893 

1894 def get_sysid_data_collector_metadata(self) -> CollectorMetadata: 

1895 """Collects metadata to send to the data collector""" 

1896 num_channels = self.environment_parameters.number_of_channels 

1897 response_channel_indices = self.environment_parameters.response_channel_indices 

1898 reference_channel_indices = self.environment_parameters.reference_channel_indices 

1899 if self.environment_parameters.sysid_signal_type in [ 

1900 "Random", 

1901 "Pseudorandom", 

1902 "Chirp", 

1903 ]: 

1904 acquisition_type = AcquisitionType.FREE_RUN 

1905 else: 

1906 acquisition_type = AcquisitionType.TRIGGER_FIRST_FRAME 

1907 acceptance = Acceptance.AUTOMATIC 

1908 acceptance_function = None 

1909 if self.environment_parameters.sysid_signal_type == "Random": 

1910 overlap_fraction = self.environment_parameters.sysid_overlap 

1911 else: 

1912 overlap_fraction = 0 

1913 if self.environment_parameters.sysid_signal_type == "Burst Random": 

1914 trigger_channel_index = reference_channel_indices[0] 

1915 else: 

1916 trigger_channel_index = 0 

1917 trigger_slope = TriggerSlope.POSITIVE 

1918 trigger_level = self.environment_parameters.sysid_level / 100 

1919 trigger_hysteresis = self.environment_parameters.sysid_level / 200 

1920 trigger_hysteresis_samples = ( 

1921 (1 - self.environment_parameters.sysid_burst_on) 

1922 * self.environment_parameters.sysid_frame_size 

1923 ) // 2 

1924 pretrigger_fraction = self.environment_parameters.sysid_pretrigger 

1925 frame_size = self.environment_parameters.sysid_frame_size 

1926 window = ( 

1927 Window.HANN if self.environment_parameters.sysid_window == "Hann" else Window.RECTANGLE 

1928 ) 

1929 kurtosis_buffer_length = self.environment_parameters.sysid_averages 

1930 

1931 return CollectorMetadata( 

1932 num_channels, 

1933 response_channel_indices, 

1934 reference_channel_indices, 

1935 acquisition_type, 

1936 acceptance, 

1937 acceptance_function, 

1938 overlap_fraction, 

1939 trigger_channel_index, 

1940 trigger_slope, 

1941 trigger_level, 

1942 trigger_hysteresis, 

1943 trigger_hysteresis_samples, 

1944 pretrigger_fraction, 

1945 frame_size, 

1946 window, 

1947 kurtosis_buffer_length=kurtosis_buffer_length, 

1948 response_transformation_matrix=self.environment_parameters.response_transformation_matrix, 

1949 reference_transformation_matrix=self.environment_parameters.reference_transformation_matrix, 

1950 ) 

1951 

1952 def get_sysid_spectral_processing_metadata(self, is_noise=False) -> SpectralProcessingMetadata: 

1953 """Collects metadata to send to the spectral processing process""" 

1954 averaging_type = ( 

1955 AveragingTypes.LINEAR 

1956 if self.environment_parameters.sysid_averaging_type == "Linear" 

1957 else AveragingTypes.EXPONENTIAL 

1958 ) 

1959 averages = ( 

1960 self.environment_parameters.sysid_noise_averages 

1961 if is_noise 

1962 else self.environment_parameters.sysid_averages 

1963 ) 

1964 exponential_averaging_coefficient = ( 

1965 self.environment_parameters.sysid_exponential_averaging_coefficient 

1966 ) 

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

1968 frf_estimator = Estimator.H1 

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

1970 frf_estimator = Estimator.H2 

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

1972 frf_estimator = Estimator.H3 

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

1974 frf_estimator = Estimator.HV 

1975 else: 

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

1977 num_response_channels = self.environment_parameters.num_response_channels 

1978 num_reference_channels = self.environment_parameters.num_reference_channels 

1979 frequency_spacing = self.environment_parameters.sysid_frequency_spacing 

1980 sample_rate = self.environment_parameters.sample_rate 

1981 num_frequency_lines = self.environment_parameters.sysid_fft_lines 

1982 return SpectralProcessingMetadata( 

1983 averaging_type, 

1984 averages, 

1985 exponential_averaging_coefficient, 

1986 frf_estimator, 

1987 num_response_channels, 

1988 num_reference_channels, 

1989 frequency_spacing, 

1990 sample_rate, 

1991 num_frequency_lines, 

1992 ) 

1993 

1994 def get_sysid_signal_generation_metadata(self) -> SignalGenerationMetadata: 

1995 """Collects metadata to send to the signal generation process""" 

1996 return SignalGenerationMetadata( 

1997 samples_per_write=self.data_acquisition_parameters.samples_per_write, 

1998 level_ramp_samples=self.environment_parameters.sysid_level_ramp_time 

1999 * self.environment_parameters.sample_rate 

2000 * self.data_acquisition_parameters.output_oversample, 

2001 output_transformation_matrix=self.environment_parameters.reference_transformation_matrix, 

2002 ) 

2003 

2004 def get_sysid_signal_generator(self) -> SignalGenerator: 

2005 """Creates a signal generator object that will generate the signals""" 

2006 if self.environment_parameters.sysid_signal_type == "Random": 

2007 return RandomSignalGenerator( 

2008 rms=self.environment_parameters.sysid_level, 

2009 sample_rate=self.environment_parameters.sample_rate, 

2010 num_samples_per_frame=self.environment_parameters.sysid_frame_size, 

2011 num_signals=self.environment_parameters.num_reference_channels, 

2012 low_frequency_cutoff=self.environment_parameters.sysid_low_frequency_cutoff, 

2013 high_frequency_cutoff=self.environment_parameters.sysid_high_frequency_cutoff, 

2014 cola_overlap=0.5, 

2015 cola_window="hann", 

2016 cola_exponent=0.5, 

2017 output_oversample=self.data_acquisition_parameters.output_oversample, 

2018 ) 

2019 elif self.environment_parameters.sysid_signal_type == "Pseudorandom": 

2020 return PseudorandomSignalGenerator( 

2021 rms=self.environment_parameters.sysid_level, 

2022 sample_rate=self.environment_parameters.sample_rate, 

2023 num_samples_per_frame=self.environment_parameters.sysid_frame_size, 

2024 num_signals=self.environment_parameters.num_reference_channels, 

2025 low_frequency_cutoff=self.environment_parameters.sysid_low_frequency_cutoff, 

2026 high_frequency_cutoff=self.environment_parameters.sysid_high_frequency_cutoff, 

2027 output_oversample=self.data_acquisition_parameters.output_oversample, 

2028 ) 

2029 elif self.environment_parameters.sysid_signal_type == "Burst Random": 

2030 return BurstRandomSignalGenerator( 

2031 rms=self.environment_parameters.sysid_level, 

2032 sample_rate=self.environment_parameters.sample_rate, 

2033 num_samples_per_frame=self.environment_parameters.sysid_frame_size, 

2034 num_signals=self.environment_parameters.num_reference_channels, 

2035 low_frequency_cutoff=self.environment_parameters.sysid_low_frequency_cutoff, 

2036 high_frequency_cutoff=self.environment_parameters.sysid_high_frequency_cutoff, 

2037 on_fraction=self.environment_parameters.sysid_burst_on, 

2038 ramp_fraction=self.environment_parameters.sysid_burst_ramp_fraction, 

2039 output_oversample=self.data_acquisition_parameters.output_oversample, 

2040 ) 

2041 elif self.environment_parameters.sysid_signal_type == "Chirp": 

2042 return ChirpSignalGenerator( 

2043 level=self.environment_parameters.sysid_level, 

2044 sample_rate=self.environment_parameters.sample_rate, 

2045 num_samples_per_frame=self.environment_parameters.sysid_frame_size, 

2046 num_signals=self.environment_parameters.num_reference_channels, 

2047 low_frequency_cutoff=np.max( 

2048 [ 

2049 self.environment_parameters.sysid_frequency_spacing, 

2050 self.environment_parameters.sysid_low_frequency_cutoff, 

2051 ] 

2052 ), 

2053 high_frequency_cutoff=np.min( 

2054 [ 

2055 self.environment_parameters.sample_rate / 2, 

2056 self.environment_parameters.sysid_high_frequency_cutoff, 

2057 ] 

2058 ), 

2059 output_oversample=self.data_acquisition_parameters.output_oversample, 

2060 ) 

2061 

2062 def load_noise(self, data): 

2063 """Sends noise data to the data analysis process""" 

2064 self.data_analysis_command_queue.put( 

2065 self.environment_name, (SysIDDataAnalysisCommands.LOAD_NOISE, data) 

2066 ) 

2067 

2068 def load_transfer_function(self, data): 

2069 """Sends transfer function data to the data analysis process""" 

2070 self.data_analysis_command_queue.put( 

2071 self.environment_name, 

2072 (SysIDDataAnalysisCommands.LOAD_TRANSFER_FUNCTION, data), 

2073 ) 

2074 

2075 def preview_noise(self, data): 

2076 """Starts up the noise preview with the defined metadata""" 

2077 self.log("Starting Noise Preview") 

2078 self.siggen_shutdown_achieved = False 

2079 self.collector_shutdown_achieved = False 

2080 self.spectral_shutdown_achieved = False 

2081 self.analysis_shutdown_achieved = False 

2082 self.environment_parameters = data 

2083 # Start up controller 

2084 self.controller_communication_queue.put( 

2085 self.environment_name, (GlobalCommands.RUN_HARDWARE, None) 

2086 ) 

2087 self.controller_communication_queue.put( 

2088 self.environment_name, 

2089 (GlobalCommands.START_ENVIRONMENT, self.environment_name), 

2090 ) 

2091 

2092 # Set up the collector 

2093 collector_metadata = deepcopy(self.get_sysid_data_collector_metadata()) 

2094 collector_metadata.acquisition_type = AcquisitionType.FREE_RUN 

2095 self.collector_command_queue.put( 

2096 self.environment_name, 

2097 (DataCollectorCommands.FORCE_INITIALIZE_COLLECTOR, collector_metadata), 

2098 ) 

2099 

2100 self.collector_command_queue.put( 

2101 self.environment_name, 

2102 ( 

2103 DataCollectorCommands.SET_TEST_LEVEL, 

2104 (self.environment_parameters.sysid_skip_frames, 1), 

2105 ), 

2106 ) 

2107 time.sleep(0.01) 

2108 

2109 # Set up the signal generation 

2110 self.signal_generator_command_queue.put( 

2111 self.environment_name, 

2112 ( 

2113 SignalGenerationCommands.INITIALIZE_PARAMETERS, 

2114 self.get_sysid_signal_generation_metadata(), 

2115 ), 

2116 ) 

2117 

2118 self.signal_generator_command_queue.put( 

2119 self.environment_name, 

2120 ( 

2121 SignalGenerationCommands.INITIALIZE_SIGNAL_GENERATOR, 

2122 self.get_sysid_signal_generator(), 

2123 ), 

2124 ) 

2125 

2126 self.signal_generator_command_queue.put( 

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

2128 ) 

2129 

2130 # Tell the collector to start acquiring data 

2131 self.collector_command_queue.put( 

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

2133 ) 

2134 

2135 # Tell the signal generation to start generating signals 

2136 self.signal_generator_command_queue.put( 

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

2138 ) 

2139 

2140 # Set up the data analysis 

2141 self.data_analysis_command_queue.put( 

2142 self.environment_name, 

2143 ( 

2144 SysIDDataAnalysisCommands.INITIALIZE_PARAMETERS, 

2145 self.environment_parameters, 

2146 ), 

2147 ) 

2148 

2149 # Start the data analysis running 

2150 self.data_analysis_command_queue.put( 

2151 self.environment_name, (SysIDDataAnalysisCommands.RUN_NOISE, False) 

2152 ) 

2153 

2154 # Set up the spectral processing 

2155 self.spectral_processing_command_queue.put( 

2156 self.environment_name, 

2157 ( 

2158 SpectralProcessingCommands.INITIALIZE_PARAMETERS, 

2159 self.get_sysid_spectral_processing_metadata(is_noise=True), 

2160 ), 

2161 ) 

2162 

2163 # Tell the spectral analysis to clear and start acquiring 

2164 self.spectral_processing_command_queue.put( 

2165 self.environment_name, 

2166 (SpectralProcessingCommands.CLEAR_SPECTRAL_PROCESSING, None), 

2167 ) 

2168 

2169 self.spectral_processing_command_queue.put( 

2170 self.environment_name, 

2171 (SpectralProcessingCommands.RUN_SPECTRAL_PROCESSING, None), 

2172 ) 

2173 

2174 # Tell data collector to clear the kurtosis buffer 

2175 self.collector_command_queue.put( 

2176 self.environment_name, (DataCollectorCommands.CLEAR_KURTOSIS_BUFFER, None) 

2177 ) 

2178 

2179 def preview_transfer_function(self, data): 

2180 """Starts up a transfer function preview with the provided environment metadata""" 

2181 self.log("Starting System ID Preview") 

2182 self.siggen_shutdown_achieved = False 

2183 self.collector_shutdown_achieved = False 

2184 self.spectral_shutdown_achieved = False 

2185 self.analysis_shutdown_achieved = False 

2186 self.environment_parameters = data 

2187 # Start up controller 

2188 self.controller_communication_queue.put( 

2189 self.environment_name, (GlobalCommands.RUN_HARDWARE, None) 

2190 ) 

2191 # Wait for the environment to start up 

2192 while not (self.acquisition_active and self.output_active): 

2193 # print('Waiting for Acquisition and Output to Start up') 

2194 time.sleep(0.1) 

2195 self.controller_communication_queue.put( 

2196 self.environment_name, 

2197 (GlobalCommands.START_ENVIRONMENT, self.environment_name), 

2198 ) 

2199 

2200 # Set up the collector 

2201 self.collector_command_queue.put( 

2202 self.environment_name, 

2203 ( 

2204 DataCollectorCommands.FORCE_INITIALIZE_COLLECTOR, 

2205 self.get_sysid_data_collector_metadata(), 

2206 ), 

2207 ) 

2208 

2209 self.collector_command_queue.put( 

2210 self.environment_name, 

2211 ( 

2212 DataCollectorCommands.SET_TEST_LEVEL, 

2213 (self.environment_parameters.sysid_skip_frames, 1), 

2214 ), 

2215 ) 

2216 time.sleep(0.01) 

2217 

2218 # Set up the signal generation 

2219 self.signal_generator_command_queue.put( 

2220 self.environment_name, 

2221 ( 

2222 SignalGenerationCommands.INITIALIZE_PARAMETERS, 

2223 self.get_sysid_signal_generation_metadata(), 

2224 ), 

2225 ) 

2226 

2227 self.signal_generator_command_queue.put( 

2228 self.environment_name, 

2229 ( 

2230 SignalGenerationCommands.INITIALIZE_SIGNAL_GENERATOR, 

2231 self.get_sysid_signal_generator(), 

2232 ), 

2233 ) 

2234 

2235 self.signal_generator_command_queue.put( 

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

2237 ) 

2238 

2239 self.signal_generator_command_queue.put( 

2240 self.environment_name, (SignalGenerationCommands.ADJUST_TEST_LEVEL, 1.0) 

2241 ) 

2242 

2243 # Tell the collector to start acquiring data 

2244 self.collector_command_queue.put( 

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

2246 ) 

2247 

2248 # Tell the signal generation to start generating signals 

2249 self.signal_generator_command_queue.put( 

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

2251 ) 

2252 

2253 # Set up the data analysis 

2254 self.data_analysis_command_queue.put( 

2255 self.environment_name, 

2256 ( 

2257 SysIDDataAnalysisCommands.INITIALIZE_PARAMETERS, 

2258 self.environment_parameters, 

2259 ), 

2260 ) 

2261 

2262 # Start the data analysis running 

2263 self.data_analysis_command_queue.put( 

2264 self.environment_name, 

2265 (SysIDDataAnalysisCommands.RUN_TRANSFER_FUNCTION, False), 

2266 ) 

2267 

2268 # Set up the spectral processing 

2269 self.spectral_processing_command_queue.put( 

2270 self.environment_name, 

2271 ( 

2272 SpectralProcessingCommands.INITIALIZE_PARAMETERS, 

2273 self.get_sysid_spectral_processing_metadata(is_noise=False), 

2274 ), 

2275 ) 

2276 

2277 # Tell the spectral analysis to clear and start acquiring 

2278 self.spectral_processing_command_queue.put( 

2279 self.environment_name, 

2280 (SpectralProcessingCommands.CLEAR_SPECTRAL_PROCESSING, None), 

2281 ) 

2282 

2283 self.spectral_processing_command_queue.put( 

2284 self.environment_name, 

2285 (SpectralProcessingCommands.RUN_SPECTRAL_PROCESSING, None), 

2286 ) 

2287 

2288 # Tell data collector to clear the kurtosis buffer 

2289 self.collector_command_queue.put( 

2290 self.environment_name, (DataCollectorCommands.CLEAR_KURTOSIS_BUFFER, None) 

2291 ) 

2292 

2293 def start_noise(self, data): 

2294 """Starts the noise measurement with the provided metadata""" 

2295 self.log("Starting Noise Measurement for System ID") 

2296 self.siggen_shutdown_achieved = False 

2297 self.collector_shutdown_achieved = False 

2298 self.spectral_shutdown_achieved = False 

2299 self.analysis_shutdown_achieved = False 

2300 self.environment_parameters, self._sysid_stream_name = data 

2301 self.controller_communication_queue.put( 

2302 self.environment_name, 

2303 ( 

2304 GlobalCommands.UPDATE_METADATA, 

2305 (self.environment_name, self.environment_parameters), 

2306 ), 

2307 ) 

2308 # Start up controller 

2309 if self._sysid_stream_name is not None: 

2310 self.controller_communication_queue.put( 

2311 self.environment_name, 

2312 (GlobalCommands.INITIALIZE_STREAMING, self._sysid_stream_name), 

2313 ) 

2314 self.controller_communication_queue.put( 

2315 self.environment_name, (GlobalCommands.START_STREAMING, None) 

2316 ) 

2317 self.controller_communication_queue.put( 

2318 self.environment_name, (GlobalCommands.RUN_HARDWARE, None) 

2319 ) 

2320 self.controller_communication_queue.put( 

2321 self.environment_name, 

2322 (GlobalCommands.START_ENVIRONMENT, self.environment_name), 

2323 ) 

2324 

2325 # Set up the collector 

2326 collector_metadata = deepcopy(self.get_sysid_data_collector_metadata()) 

2327 collector_metadata.acquisition_type = AcquisitionType.FREE_RUN 

2328 self.collector_command_queue.put( 

2329 self.environment_name, 

2330 (DataCollectorCommands.FORCE_INITIALIZE_COLLECTOR, collector_metadata), 

2331 ) 

2332 

2333 self.collector_command_queue.put( 

2334 self.environment_name, 

2335 ( 

2336 DataCollectorCommands.SET_TEST_LEVEL, 

2337 (self.environment_parameters.sysid_skip_frames, 1), 

2338 ), 

2339 ) 

2340 time.sleep(0.01) 

2341 

2342 # Set up the signal generation 

2343 self.signal_generator_command_queue.put( 

2344 self.environment_name, 

2345 ( 

2346 SignalGenerationCommands.INITIALIZE_PARAMETERS, 

2347 self.get_sysid_signal_generation_metadata(), 

2348 ), 

2349 ) 

2350 

2351 self.signal_generator_command_queue.put( 

2352 self.environment_name, 

2353 ( 

2354 SignalGenerationCommands.INITIALIZE_SIGNAL_GENERATOR, 

2355 self.get_sysid_signal_generator(), 

2356 ), 

2357 ) 

2358 

2359 self.signal_generator_command_queue.put( 

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

2361 ) 

2362 

2363 # Tell the collector to start acquiring data 

2364 self.collector_command_queue.put( 

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

2366 ) 

2367 

2368 # Tell the signal generation to start generating signals 

2369 self.signal_generator_command_queue.put( 

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

2371 ) 

2372 

2373 # Set up the data analysis 

2374 self.data_analysis_command_queue.put( 

2375 self.environment_name, 

2376 ( 

2377 SysIDDataAnalysisCommands.INITIALIZE_PARAMETERS, 

2378 self.environment_parameters, 

2379 ), 

2380 ) 

2381 

2382 # Start the data analysis running 

2383 self.data_analysis_command_queue.put( 

2384 self.environment_name, (SysIDDataAnalysisCommands.RUN_NOISE, True) 

2385 ) 

2386 

2387 # Set up the spectral processing 

2388 self.spectral_processing_command_queue.put( 

2389 self.environment_name, 

2390 ( 

2391 SpectralProcessingCommands.INITIALIZE_PARAMETERS, 

2392 self.get_sysid_spectral_processing_metadata(is_noise=True), 

2393 ), 

2394 ) 

2395 

2396 # Tell the spectral analysis to clear and start acquiring 

2397 self.spectral_processing_command_queue.put( 

2398 self.environment_name, 

2399 (SpectralProcessingCommands.CLEAR_SPECTRAL_PROCESSING, None), 

2400 ) 

2401 

2402 self.spectral_processing_command_queue.put( 

2403 self.environment_name, 

2404 (SpectralProcessingCommands.RUN_SPECTRAL_PROCESSING, None), 

2405 ) 

2406 

2407 # Tell data collector to clear the kurtosis buffer 

2408 self.collector_command_queue.put( 

2409 self.environment_name, (DataCollectorCommands.CLEAR_KURTOSIS_BUFFER, None) 

2410 ) 

2411 

2412 def start_transfer_function(self, data): 

2413 """Starts the transfer function measurement with the provided metadata""" 

2414 self.log("Starting Transfer Function for System ID") 

2415 self.siggen_shutdown_achieved = False 

2416 self.collector_shutdown_achieved = False 

2417 self.spectral_shutdown_achieved = False 

2418 self.analysis_shutdown_achieved = False 

2419 self.environment_parameters = data 

2420 # Start up controller 

2421 if self._sysid_stream_name is not None: 

2422 self.controller_communication_queue.put( 

2423 self.environment_name, (GlobalCommands.START_STREAMING, None) 

2424 ) 

2425 

2426 self.controller_communication_queue.put( 

2427 self.environment_name, 

2428 (GlobalCommands.START_ENVIRONMENT, self.environment_name), 

2429 ) 

2430 

2431 # Set up the collector 

2432 self.collector_command_queue.put( 

2433 self.environment_name, 

2434 ( 

2435 DataCollectorCommands.FORCE_INITIALIZE_COLLECTOR, 

2436 self.get_sysid_data_collector_metadata(), 

2437 ), 

2438 ) 

2439 

2440 self.collector_command_queue.put( 

2441 self.environment_name, 

2442 ( 

2443 DataCollectorCommands.SET_TEST_LEVEL, 

2444 (self.environment_parameters.sysid_skip_frames, 1), 

2445 ), 

2446 ) 

2447 time.sleep(0.01) 

2448 

2449 # Set up the signal generation 

2450 self.signal_generator_command_queue.put( 

2451 self.environment_name, 

2452 ( 

2453 SignalGenerationCommands.INITIALIZE_PARAMETERS, 

2454 self.get_sysid_signal_generation_metadata(), 

2455 ), 

2456 ) 

2457 

2458 self.signal_generator_command_queue.put( 

2459 self.environment_name, 

2460 ( 

2461 SignalGenerationCommands.INITIALIZE_SIGNAL_GENERATOR, 

2462 self.get_sysid_signal_generator(), 

2463 ), 

2464 ) 

2465 

2466 self.signal_generator_command_queue.put( 

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

2468 ) 

2469 

2470 self.signal_generator_command_queue.put( 

2471 self.environment_name, (SignalGenerationCommands.ADJUST_TEST_LEVEL, 1.0) 

2472 ) 

2473 

2474 # Tell the collector to start acquiring data 

2475 self.collector_command_queue.put( 

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

2477 ) 

2478 

2479 # Tell the signal generation to start generating signals 

2480 self.signal_generator_command_queue.put( 

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

2482 ) 

2483 

2484 # Set up the data analysis 

2485 self.data_analysis_command_queue.put( 

2486 self.environment_name, 

2487 ( 

2488 SysIDDataAnalysisCommands.INITIALIZE_PARAMETERS, 

2489 self.environment_parameters, 

2490 ), 

2491 ) 

2492 

2493 # Start the data analysis running 

2494 self.data_analysis_command_queue.put( 

2495 self.environment_name, 

2496 (SysIDDataAnalysisCommands.RUN_TRANSFER_FUNCTION, True), 

2497 ) 

2498 

2499 # Set up the spectral processing 

2500 self.spectral_processing_command_queue.put( 

2501 self.environment_name, 

2502 ( 

2503 SpectralProcessingCommands.INITIALIZE_PARAMETERS, 

2504 self.get_sysid_spectral_processing_metadata(is_noise=False), 

2505 ), 

2506 ) 

2507 

2508 # Tell the spectral analysis to clear and start acquiring 

2509 self.spectral_processing_command_queue.put( 

2510 self.environment_name, 

2511 (SpectralProcessingCommands.CLEAR_SPECTRAL_PROCESSING, None), 

2512 ) 

2513 

2514 self.spectral_processing_command_queue.put( 

2515 self.environment_name, 

2516 (SpectralProcessingCommands.RUN_SPECTRAL_PROCESSING, None), 

2517 ) 

2518 

2519 # Tell data collector to clear the kurtosis buffer 

2520 self.collector_command_queue.put( 

2521 self.environment_name, (DataCollectorCommands.CLEAR_KURTOSIS_BUFFER, None) 

2522 ) 

2523 

2524 def stop_system_id(self, stop_tasks): 

2525 """Starts the shutdown process for the system identification""" 

2526 stop_data_analysis, stop_hardware = stop_tasks 

2527 self.log("Stop Transfer Function") 

2528 if stop_hardware: 

2529 self.controller_communication_queue.put( 

2530 self.environment_name, (GlobalCommands.STOP_HARDWARE, None) 

2531 ) 

2532 elif self._sysid_stream_name is not None: 

2533 self.controller_communication_queue.put( 

2534 self.environment_name, (GlobalCommands.STOP_STREAMING, None) 

2535 ) 

2536 self.collector_command_queue.put( 

2537 self.environment_name, 

2538 ( 

2539 DataCollectorCommands.SET_TEST_LEVEL, 

2540 (self.environment_parameters.sysid_skip_frames * 10, 1), 

2541 ), 

2542 ) 

2543 self.signal_generator_command_queue.put( 

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

2545 ) 

2546 self.spectral_processing_command_queue.put( 

2547 self.environment_name, 

2548 (SpectralProcessingCommands.STOP_SPECTRAL_PROCESSING, None), 

2549 ) 

2550 if stop_data_analysis: 

2551 self.data_analysis_command_queue.put( 

2552 self.environment_name, (SysIDDataAnalysisCommands.STOP_SYSTEM_ID, None) 

2553 ) 

2554 self.environment_command_queue.put( 

2555 self.environment_name, (SystemIdCommands.CHECK_FOR_COMPLETE_SHUTDOWN, None) 

2556 ) 

2557 

2558 def siggen_shutdown_achieved_fn(self, data): # pylint: disable=unused-argument 

2559 """Sets the sshutdown flag to denote the signal generation has shut down successfully""" 

2560 self.siggen_shutdown_achieved = True 

2561 

2562 def collector_shutdown_achieved_fn(self, data): # pylint: disable=unused-argument 

2563 """Sets the shutdown flag to denote the data collector has shut down successfully""" 

2564 self.collector_shutdown_achieved = True 

2565 

2566 def spectral_shutdown_achieved_fn(self, data): # pylint: disable=unused-argument 

2567 """Sets the shutdown flag to denote the spectral computation has shut down successfully""" 

2568 self.spectral_shutdown_achieved = True 

2569 

2570 def analysis_shutdown_achieved_fn(self, data): # pylint: disable=unused-argument 

2571 """Sets the shutdown flag to denote the data analysis has shut down successfully""" 

2572 self.analysis_shutdown_achieved = True 

2573 

2574 def check_for_sysid_shutdown(self, data): # pylint: disable=unused-argument 

2575 """Checks that all of the relevant system identification processes have shut down""" 

2576 if ( 

2577 self.siggen_shutdown_achieved 

2578 and self.collector_shutdown_achieved 

2579 and self.spectral_shutdown_achieved 

2580 and self.analysis_shutdown_achieved 

2581 and ((not self.acquisition_active) or self._waiting_to_start_transfer_function) 

2582 and ((not self.output_active) or self._waiting_to_start_transfer_function) 

2583 ): 

2584 self.log("Shutdown Achieved") 

2585 if self._waiting_to_start_transfer_function: 

2586 self.start_transfer_function(self.environment_parameters) 

2587 else: 

2588 self.gui_update_queue.put((self.environment_name, ("enable_system_id", None))) 

2589 self._sysid_stream_name = None 

2590 self._waiting_to_start_transfer_function = False 

2591 else: 

2592 # Recheck some time later 

2593 time.sleep(1) 

2594 waiting_for = [] 

2595 if not self.siggen_shutdown_achieved: 

2596 waiting_for.append("Signal Generation") 

2597 if not self.collector_shutdown_achieved: 

2598 waiting_for.append("Collector") 

2599 if not self.spectral_shutdown_achieved: 

2600 waiting_for.append("Spectral Processing") 

2601 if not self.analysis_shutdown_achieved: 

2602 waiting_for.append("Data Analysis") 

2603 if self.output_active and (not self._waiting_to_start_transfer_function): 

2604 waiting_for.append("Output Shutdown") 

2605 if self.acquisition_active and (not self._waiting_to_start_transfer_function): 

2606 waiting_for.append("Acquisition Shutdown") 

2607 self.log(f"Waiting for {' and '.join(waiting_for)}") 

2608 self.environment_command_queue.put( 

2609 self.environment_name, 

2610 (SystemIdCommands.CHECK_FOR_COMPLETE_SHUTDOWN, None), 

2611 ) 

2612 

2613 def start_shutdown_and_run_sysid(self, data): # pylint: disable=unused-argument 

2614 """After successful noise run, shut down and start up system identification""" 

2615 self.log("Shutting down and then Running System ID Afterwards") 

2616 self._waiting_to_start_transfer_function = True 

2617 self.stop_system_id((False, False)) 

2618 

2619 def system_id_complete(self, data): 

2620 """Sends a message to the controller that this environment has completed system id""" 

2621 self.log("Finished System Identification") 

2622 self.controller_communication_queue.put( 

2623 self.environment_name, 

2624 (GlobalCommands.COMPLETED_SYSTEM_ID, (self.environment_name, data)), 

2625 ) 

2626 

2627 @abstractmethod 

2628 def stop_environment(self, data): 

2629 """Stop the environment gracefully 

2630 

2631 This function defines the operations to shut down the environment 

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

2633 or parts. 

2634 

2635 Parameters 

2636 ---------- 

2637 data : Ignored 

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

2639 due to the calling signature of functions called through the 

2640 ``command_map`` 

2641 

2642 """ 

2643 

2644 def quit(self, data): 

2645 """Closes down the environment when quitting the software""" 

2646 for queue in [ 

2647 self.collector_command_queue, 

2648 self.signal_generator_command_queue, 

2649 self.spectral_processing_command_queue, 

2650 self.data_analysis_command_queue, 

2651 ]: 

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

2653 # Return true to stop the task 

2654 return True