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

1147 statements  

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

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

2""" 

3This file defines a Modal Testing Environment where users can perform 

4hammer or shaker modal tests and export FRFs and other relevant data. 

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 inspect 

26import multiprocessing as mp 

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

28import os 

29import time 

30from enum import Enum 

31from glob import glob 

32from multiprocessing.queues import Queue 

33 

34import netCDF4 as nc4 

35import numpy as np 

36import openpyxl 

37import scipy.signal as sig 

38from qtpy import QtWidgets, uic 

39from qtpy.QtCore import Qt 

40 

41from .abstract_environment import AbstractEnvironment, AbstractMetadata, AbstractUI 

42from .environments import ( 

43 ControlTypes, 

44 environment_definition_ui_paths, 

45 environment_run_ui_paths, 

46) 

47from .signal_generation import ( 

48 BurstRandomSignalGenerator, 

49 ChirpSignalGenerator, 

50 PseudorandomSignalGenerator, 

51 RandomSignalGenerator, 

52 SineSignalGenerator, 

53 SquareSignalGenerator, 

54) 

55from .ui_utilities import ModalMDISubWindow, multiline_plotter 

56from .utilities import ( 

57 DataAcquisitionParameters, 

58 GlobalCommands, 

59 VerboseMessageQueue, 

60 error_message_qt, 

61 flush_queue, 

62 load_python_module, 

63) 

64 

65CONTROL_TYPE = ControlTypes.MODAL 

66MAXIMUM_NAME_LENGTH = 50 

67 

68WAIT_TIME = 0.02 

69 

70 

71class ModalCommands(Enum): 

72 """Valid commands for the modal environment""" 

73 

74 START_CONTROL = 0 

75 STOP_CONTROL = 1 

76 ACCEPT_FRAME = 2 

77 RUN_CONTROL = 3 

78 CHECK_FOR_COMPLETE_SHUTDOWN = 4 

79 

80 

81class ModalQueues: 

82 """A set of queues used by the modal environment""" 

83 

84 def __init__( 

85 self, 

86 environment_name: str, 

87 environment_command_queue: VerboseMessageQueue, 

88 gui_update_queue: mp.queues.Queue, 

89 controller_communication_queue: VerboseMessageQueue, 

90 data_in_queue: mp.queues.Queue, 

91 data_out_queue: mp.queues.Queue, 

92 log_file_queue: VerboseMessageQueue, 

93 ): 

94 """ 

95 Creates a namespace to store all the queues used by the Modal Environment 

96 

97 Parameters 

98 ---------- 

99 environment_command_queue : VerboseMessageQueue 

100 Queue from which the environment will receive instructions. 

101 gui_update_queue : mp.queues.Queue 

102 Queue to which the environment will put GUI updates. 

103 controller_communication_queue : VerboseMessageQueue 

104 Queue to which the environment will put global contorller instructions. 

105 data_in_queue : mp.queues.Queue 

106 Queue from which the environment will receive data from acquisition. 

107 data_out_queue : mp.queues.Queue 

108 Queue to which the environment will write data for output. 

109 log_file_queue : VerboseMessageQueue 

110 Queue to which the environment will write log file messages. 

111 """ 

112 self.environment_command_queue = environment_command_queue 

113 self.gui_update_queue = gui_update_queue 

114 self.controller_communication_queue = controller_communication_queue 

115 self.data_in_queue = data_in_queue 

116 self.data_out_queue = data_out_queue 

117 self.log_file_queue = log_file_queue 

118 self.data_for_spectral_computation_queue = mp.Queue() 

119 self.updated_spectral_quantities_queue = mp.Queue() 

120 self.signal_generation_update_queue = mp.Queue() 

121 self.spectral_command_queue = VerboseMessageQueue( 

122 log_file_queue, environment_name + " Spectral Computation Command Queue" 

123 ) 

124 self.collector_command_queue = VerboseMessageQueue( 

125 log_file_queue, environment_name + " Data Collector Command Queue" 

126 ) 

127 self.signal_generation_command_queue = VerboseMessageQueue( 

128 log_file_queue, environment_name + " Signal Generation Command Queue" 

129 ) 

130 

131 

132class ModalMetadata(AbstractMetadata): 

133 """Class for storing metadata for an environment. 

134 

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

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

137 ``collect_environment_definition_parameters`` function as well as its 

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

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

140 """ 

141 

142 def __init__( 

143 self, 

144 sample_rate: float, 

145 samples_per_frame: int, 

146 averaging_type: str, 

147 num_averages: int, 

148 averaging_coefficient: float, 

149 frf_technique: str, 

150 frf_window: str, 

151 overlap_percent: float, 

152 trigger_type: str, 

153 accept_type: str, 

154 wait_for_steady_state: float, 

155 trigger_channel: int, 

156 pretrigger_percent: float, 

157 trigger_slope_positive: bool, 

158 trigger_level_percent: float, 

159 hysteresis_level_percent: float, 

160 hysteresis_frame_percent: float, 

161 signal_generator_type: str, 

162 signal_generator_level: float, 

163 signal_generator_min_frequency: float, 

164 signal_generator_max_frequency: float, 

165 signal_generator_on_percent: float, 

166 acceptance_function, 

167 reference_channel_indices, 

168 response_channel_indices, 

169 output_channel_indices, 

170 data_acquisition_parameters: DataAcquisitionParameters, 

171 exponential_window_value_at_frame_end: float, 

172 ): 

173 self.sample_rate = sample_rate 

174 self.samples_per_frame = samples_per_frame 

175 self.averaging_type = averaging_type 

176 self.num_averages = num_averages 

177 self.averaging_coefficient = averaging_coefficient 

178 self.frf_technique = frf_technique 

179 self.frf_window = frf_window 

180 self.overlap = overlap_percent / 100 

181 self.trigger_type = trigger_type 

182 self.accept_type = accept_type 

183 self.wait_for_steady_state = wait_for_steady_state 

184 self.trigger_channel = trigger_channel 

185 self.pretrigger = pretrigger_percent / 100 

186 self.trigger_slope_positive = trigger_slope_positive 

187 self.trigger_level = trigger_level_percent / 100 

188 self.hysteresis_level = hysteresis_level_percent / 100 

189 self.hysteresis_length = hysteresis_frame_percent / 100 

190 self.signal_generator_type = signal_generator_type 

191 self.signal_generator_level = signal_generator_level 

192 self.signal_generator_min_frequency = signal_generator_min_frequency 

193 self.signal_generator_max_frequency = signal_generator_max_frequency 

194 self.signal_generator_on_fraction = signal_generator_on_percent / 100 

195 self.acceptance_function = acceptance_function 

196 self.reference_channel_indices = reference_channel_indices 

197 self.response_channel_indices = response_channel_indices 

198 self.output_channel_indices = output_channel_indices 

199 self.exponential_window_value_at_frame_end = exponential_window_value_at_frame_end 

200 # Set up signal generator 

201 self.output_oversample = data_acquisition_parameters.output_oversample 

202 self.signal_generator = self.get_signal_generator() 

203 

204 def get_signal_generator(self): 

205 """Gets a signal generator object that the modal environment will use to generate signals""" 

206 if self.signal_generator_type == "none": 

207 signal_generator = PseudorandomSignalGenerator( 

208 rms=0.0, 

209 sample_rate=self.sample_rate, 

210 num_samples_per_frame=self.samples_per_frame, 

211 num_signals=len(self.output_channel_indices), 

212 low_frequency_cutoff=self.signal_generator_min_frequency, 

213 high_frequency_cutoff=self.signal_generator_max_frequency, 

214 output_oversample=self.output_oversample, 

215 ) 

216 elif self.signal_generator_type == "random": 

217 signal_generator = RandomSignalGenerator( 

218 rms=self.signal_generator_level, 

219 sample_rate=self.sample_rate, 

220 num_samples_per_frame=self.samples_per_frame, 

221 num_signals=len(self.output_channel_indices), 

222 low_frequency_cutoff=self.signal_generator_min_frequency, 

223 high_frequency_cutoff=self.signal_generator_max_frequency, 

224 cola_overlap=0.5, 

225 cola_window="hann", 

226 cola_exponent=0.5, 

227 output_oversample=self.output_oversample, 

228 ) 

229 elif self.signal_generator_type == "pseudorandom": 

230 signal_generator = PseudorandomSignalGenerator( 

231 rms=self.signal_generator_level, 

232 sample_rate=self.sample_rate, 

233 num_samples_per_frame=self.samples_per_frame, 

234 num_signals=len(self.output_channel_indices), 

235 low_frequency_cutoff=self.signal_generator_min_frequency, 

236 high_frequency_cutoff=self.signal_generator_max_frequency, 

237 output_oversample=self.output_oversample, 

238 ) 

239 elif self.signal_generator_type == "burst": 

240 signal_generator = BurstRandomSignalGenerator( 

241 rms=self.signal_generator_level, 

242 sample_rate=self.sample_rate, 

243 num_samples_per_frame=self.samples_per_frame, 

244 num_signals=len(self.output_channel_indices), 

245 low_frequency_cutoff=self.signal_generator_min_frequency, 

246 high_frequency_cutoff=self.signal_generator_max_frequency, 

247 on_fraction=self.signal_generator_on_fraction, 

248 ramp_fraction=0.05, 

249 output_oversample=self.output_oversample, 

250 ) 

251 elif self.signal_generator_type == "chirp": 

252 signal_generator = ChirpSignalGenerator( 

253 level=self.signal_generator_level, 

254 sample_rate=self.sample_rate, 

255 num_samples_per_frame=self.samples_per_frame, 

256 num_signals=len(self.output_channel_indices), 

257 low_frequency_cutoff=self.signal_generator_min_frequency, 

258 high_frequency_cutoff=self.signal_generator_max_frequency, 

259 output_oversample=self.output_oversample, 

260 ) 

261 elif self.signal_generator_type == "square": 

262 signal_generator = SquareSignalGenerator( 

263 level=self.signal_generator_level, 

264 sample_rate=self.sample_rate, 

265 num_samples_per_frame=self.samples_per_frame, 

266 num_signals=len(self.output_channel_indices), 

267 frequency=self.signal_generator_min_frequency, 

268 phase=0, 

269 on_fraction=self.signal_generator_on_fraction, 

270 output_oversample=self.output_oversample, 

271 ) 

272 elif self.signal_generator_type == "sine": 

273 signal_generator = SineSignalGenerator( 

274 level=self.signal_generator_level, 

275 sample_rate=self.sample_rate, 

276 num_samples_per_frame=self.samples_per_frame, 

277 num_signals=len(self.output_channel_indices), 

278 frequency=self.signal_generator_min_frequency, 

279 phase=0, 

280 output_oversample=self.output_oversample, 

281 ) 

282 else: 

283 raise ValueError(f"Invalid Signal Type {self.signal_generator_type}") 

284 return signal_generator 

285 

286 @property 

287 def samples_per_acquire(self): 

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

289 return int(self.samples_per_frame * (1 - self.overlap)) 

290 

291 @property 

292 def frame_time(self): 

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

294 return self.samples_per_frame / self.sample_rate 

295 

296 @property 

297 def nyquist_frequency(self): 

298 """Property returning half the sample rate""" 

299 return self.sample_rate / 2 

300 

301 @property 

302 def fft_lines(self): 

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

304 return self.samples_per_frame // 2 + 1 

305 

306 @property 

307 def skip_frames(self): 

308 """Property returning the number of frames to skip while waiting for steady state""" 

309 return int( 

310 np.ceil( 

311 self.wait_for_steady_state 

312 * self.sample_rate 

313 / (self.samples_per_frame * (1 - self.overlap)) 

314 ) 

315 ) 

316 

317 @property 

318 def frequency_spacing(self): 

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

320 return self.sample_rate / self.samples_per_frame 

321 

322 def get_trigger_levels(self, channels): 

323 """Gets the trigger levels for a channel based on the channel table information 

324 

325 Parameters 

326 ---------- 

327 channels : list of Channel 

328 A list of channels in the environment 

329 

330 Returns 

331 ------- 

332 trigger_level_v 

333 The trigger level in volts 

334 trigger_level_eu 

335 The trigger level in engineering units defined in the channel table 

336 hysterisis_level_v 

337 The level that the signal must return below before another trigger can be accepted, 

338 in volts 

339 hysterisis_level_eu 

340 The level that the signal must return below before another trigger can be accepted, 

341 in engineering units defined in the channel table 

342 """ 

343 channel = channels[self.trigger_channel] 

344 try: 

345 volt_range = float(channel.maximum_value) 

346 if volt_range == 0.0: 

347 volt_range = 10.0 

348 except (ValueError, TypeError): 

349 volt_range = 10.0 

350 try: 

351 mv_per_eu = float(channel.sensitivity) 

352 if mv_per_eu == 0.0: 

353 mv_per_eu = 1000.0 

354 except (ValueError, TypeError): 

355 mv_per_eu = 1000.0 

356 v_per_eu = mv_per_eu / 1000.0 

357 trigger_level_v = self.trigger_level * volt_range 

358 trigger_level_eu = trigger_level_v / v_per_eu 

359 hysterisis_level_v = self.hysteresis_level * volt_range 

360 hysterisis_level_eu = hysterisis_level_v / v_per_eu 

361 return ( 

362 trigger_level_v, 

363 trigger_level_eu, 

364 hysterisis_level_v, 

365 hysterisis_level_eu, 

366 ) 

367 

368 @property 

369 def disabled_signals(self): 

370 """Returns a list of indices corresponding to output signals that have been disabled""" 

371 return [ 

372 i 

373 for i, index in enumerate(self.output_channel_indices) 

374 if not ( 

375 index in self.response_channel_indices or index in self.reference_channel_indices 

376 ) 

377 ] 

378 

379 @property 

380 def hysteresis_samples(self): 

381 """Property returning the number of samples that a signal must be below the hysterisis 

382 level""" 

383 return int(self.hysteresis_length * self.samples_per_frame) 

384 

385 def store_to_netcdf( 

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

387 ): 

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

389 

390 This function stores parameters from the environment into the netCDF 

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

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

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

394 attributes, dimensions, or variables. 

395 

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

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

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

399 

400 Parameters 

401 ---------- 

402 netcdf_group_handle : nc4._netCDF4.Group 

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

404 environment's metadata is stored. 

405 

406 """ 

407 netcdf_group_handle.samples_per_frame = self.samples_per_frame 

408 netcdf_group_handle.averaging_type = self.averaging_type 

409 netcdf_group_handle.num_averages = self.num_averages 

410 netcdf_group_handle.averaging_coefficient = self.averaging_coefficient 

411 netcdf_group_handle.frf_technique = self.frf_technique 

412 netcdf_group_handle.frf_window = self.frf_window 

413 netcdf_group_handle.overlap = self.overlap 

414 netcdf_group_handle.trigger_type = self.trigger_type 

415 netcdf_group_handle.accept_type = self.accept_type 

416 netcdf_group_handle.wait_for_steady_state = self.wait_for_steady_state 

417 netcdf_group_handle.trigger_channel = self.trigger_channel 

418 netcdf_group_handle.pretrigger = self.pretrigger 

419 netcdf_group_handle.trigger_slope_positive = 1 if self.trigger_slope_positive else 0 

420 netcdf_group_handle.trigger_level = self.trigger_level 

421 netcdf_group_handle.hysteresis_level = self.hysteresis_level 

422 netcdf_group_handle.hysteresis_length = self.hysteresis_length 

423 netcdf_group_handle.signal_generator_type = self.signal_generator_type 

424 netcdf_group_handle.signal_generator_level = self.signal_generator_level 

425 netcdf_group_handle.signal_generator_min_frequency = self.signal_generator_min_frequency 

426 netcdf_group_handle.signal_generator_max_frequency = self.signal_generator_max_frequency 

427 netcdf_group_handle.signal_generator_on_fraction = self.signal_generator_on_fraction 

428 netcdf_group_handle.exponential_window_value_at_frame_end = ( 

429 self.exponential_window_value_at_frame_end 

430 ) 

431 netcdf_group_handle.acceptance_function = ( 

432 self.acceptance_function[0] + ":" + self.acceptance_function[1] 

433 if self.acceptance_function is not None 

434 else "None" 

435 ) 

436 # Reference channels 

437 netcdf_group_handle.createDimension( 

438 "reference_channels", len(self.reference_channel_indices) 

439 ) 

440 var = netcdf_group_handle.createVariable( 

441 "reference_channel_indices", "i4", ("reference_channels") 

442 ) 

443 var[...] = self.reference_channel_indices 

444 # Response channels 

445 netcdf_group_handle.createDimension("response_channels", len(self.response_channel_indices)) 

446 var = netcdf_group_handle.createVariable( 

447 "response_channel_indices", "i4", ("response_channels") 

448 ) 

449 var[...] = self.response_channel_indices 

450 

451 @classmethod 

452 def from_ui(cls, ui): 

453 """ 

454 Creates a ModalMetadata object from the user interface 

455 

456 Parameters 

457 ---------- 

458 ui : ModalUI 

459 A Modal User Interface. 

460 

461 Returns 

462 ------- 

463 test_parameters : ModalMetadata 

464 Parameters corresponding to the data in the user interface 

465 

466 """ 

467 signal_generator_level = 0 

468 signal_generator_min_frequency = 0 

469 signal_generator_max_frequency = 0 

470 signal_generator_on_percent = 0 

471 if ui.definition_widget.signal_generator_selector.currentIndex() == 0: # None 

472 signal_generator_type = "none" 

473 elif ui.definition_widget.signal_generator_selector.currentIndex() == 1: # Random 

474 signal_generator_type = "random" 

475 signal_generator_level = ui.definition_widget.random_rms_selector.value() 

476 signal_generator_min_frequency = ( 

477 ui.definition_widget.random_min_frequency_selector.value() 

478 ) 

479 signal_generator_max_frequency = ( 

480 ui.definition_widget.random_max_frequency_selector.value() 

481 ) 

482 elif ui.definition_widget.signal_generator_selector.currentIndex() == 2: # Burst Random 

483 signal_generator_type = "burst" 

484 signal_generator_level = ui.definition_widget.burst_rms_selector.value() 

485 signal_generator_min_frequency = ( 

486 ui.definition_widget.burst_min_frequency_selector.value() 

487 ) 

488 signal_generator_max_frequency = ( 

489 ui.definition_widget.burst_max_frequency_selector.value() 

490 ) 

491 signal_generator_on_percent = ui.definition_widget.burst_on_percentage_selector.value() 

492 elif ui.definition_widget.signal_generator_selector.currentIndex() == 3: # Pseudorandom 

493 signal_generator_type = "pseudorandom" 

494 signal_generator_level = ui.definition_widget.pseudorandom_rms_selector.value() 

495 signal_generator_min_frequency = ( 

496 ui.definition_widget.pseudorandom_min_frequency_selector.value() 

497 ) 

498 signal_generator_max_frequency = ( 

499 ui.definition_widget.pseudorandom_max_frequency_selector.value() 

500 ) 

501 elif ui.definition_widget.signal_generator_selector.currentIndex() == 4: # Chirp 

502 signal_generator_type = "chirp" 

503 signal_generator_level = ui.definition_widget.chirp_level_selector.value() 

504 signal_generator_min_frequency = ( 

505 ui.definition_widget.chirp_min_frequency_selector.value() 

506 ) 

507 signal_generator_max_frequency = ( 

508 ui.definition_widget.chirp_max_frequency_selector.value() 

509 ) 

510 elif ui.definition_widget.signal_generator_selector.currentIndex() == 5: # Square 

511 signal_generator_type = "square" 

512 signal_generator_level = ui.definition_widget.square_level_selector.value() 

513 signal_generator_min_frequency = ui.definition_widget.square_frequency_selector.value() 

514 signal_generator_on_percent = ui.definition_widget.square_percent_on_selector.value() 

515 elif ui.definition_widget.signal_generator_selector.currentIndex() == 6: # Sine 

516 signal_generator_type = "sine" 

517 signal_generator_level = ui.definition_widget.sine_level_selector.value() 

518 signal_generator_min_frequency = ui.definition_widget.sine_frequency_selector.value() 

519 else: 

520 index = ui.definition_widget.signal_generator_selector.currentIndex() 

521 raise ValueError(f"Invalid Signal Generator {index} (How did you get here?)") 

522 return cls( 

523 ui.definition_widget.sample_rate_display.value(), 

524 ui.definition_widget.samples_per_frame_selector.value(), 

525 ui.definition_widget.system_id_averaging_scheme_selector.itemText( 

526 ui.definition_widget.system_id_averaging_scheme_selector.currentIndex() 

527 ), 

528 ui.definition_widget.system_id_frames_to_average_selector.value(), 

529 ui.definition_widget.system_id_averaging_coefficient_selector.value(), 

530 ui.definition_widget.system_id_frf_technique_selector.itemText( 

531 ui.definition_widget.system_id_frf_technique_selector.currentIndex() 

532 ), 

533 ui.definition_widget.system_id_transfer_function_computation_window_selector.itemText( 

534 ui.definition_widget.system_id_transfer_function_computation_window_selector.currentIndex() 

535 ).lower(), 

536 ui.definition_widget.system_id_overlap_percentage_selector.value(), 

537 ui.definition_widget.triggering_type_selector.itemText( 

538 ui.definition_widget.triggering_type_selector.currentIndex() 

539 ), 

540 ui.definition_widget.acceptance_selector.itemText( 

541 ui.definition_widget.acceptance_selector.currentIndex() 

542 ), 

543 ui.definition_widget.wait_for_steady_selector.value(), 

544 ui.definition_widget.trigger_channel_selector.currentIndex(), 

545 ui.definition_widget.pretrigger_selector.value(), 

546 ui.definition_widget.trigger_slope_selector.currentIndex() == 0, 

547 ui.definition_widget.trigger_level_selector.value(), 

548 ui.definition_widget.hysteresis_selector.value(), 

549 ui.definition_widget.hysteresis_length_selector.value(), 

550 signal_generator_type, 

551 signal_generator_level, 

552 signal_generator_min_frequency, 

553 signal_generator_max_frequency, 

554 signal_generator_on_percent, 

555 ui.acceptance_function, 

556 ui.reference_indices, 

557 ui.response_indices, 

558 ui.all_output_channel_indices, 

559 ui.data_acquisition_parameters, 

560 ui.definition_widget.window_value_selector.value() / 100, 

561 ) 

562 

563 def generate_signal(self): 

564 """Generates a single frame of data""" 

565 if self.signal_generator is None: 

566 return np.zeros( 

567 ( 

568 len(self.output_channel_indices), 

569 self.samples_per_frame * self.output_oversample, 

570 ) 

571 ) 

572 else: 

573 return self.signal_generator.generate_frame()[0] 

574 

575 

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

577 Acceptance, 

578 AcquisitionType, 

579 CollectorMetadata, 

580 DataCollectorCommands, 

581 TriggerSlope, 

582 Window, 

583 data_collector_process, 

584) 

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

586 SignalGenerationCommands, 

587 SignalGenerationMetadata, 

588 signal_generation_process, 

589) 

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

591 AveragingTypes, 

592 Estimator, 

593 SpectralProcessingCommands, 

594 SpectralProcessingMetadata, 

595 spectral_processing_process, 

596) 

597 

598 

599class ModalUI(AbstractUI): 

600 """Modal User Interface class defining the interface with the controller 

601 

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

603 Modal environment in the controller and the main controller.""" 

604 

605 def __init__( 

606 self, 

607 environment_name: str, 

608 definition_tabwidget: QtWidgets.QTabWidget, 

609 system_id_tabwidget: QtWidgets.QTabWidget, # pylint: disable=unused-argument 

610 test_predictions_tabwidget: QtWidgets.QTabWidget, # pylint: disable=unused-argument 

611 run_tabwidget: QtWidgets.QTabWidget, 

612 environment_command_queue: VerboseMessageQueue, 

613 controller_communication_queue: VerboseMessageQueue, 

614 log_file_queue: Queue, 

615 ): 

616 """ 

617 Constructs a Modal User Interface 

618 

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

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

621 the Modal Environment 

622 

623 Parameters 

624 ---------- 

625 definition_tabwidget : QtWidgets.QTabWidget 

626 QTabWidget containing the environment subtabs on the Control 

627 Definition main tab 

628 system_id_tabwidget : QtWidgets.QTabWidget 

629 QTabWidget containing the environment subtabs on the System 

630 Identification main tab 

631 test_predictions_tabwidget : QtWidgets.QTabWidget 

632 QTabWidget containing the environment subtabs on the Test Predictions 

633 main tab 

634 run_tabwidget : QtWidgets.QTabWidget 

635 QTabWidget containing the environment subtabs on the Run 

636 main tab. 

637 environment_command_queue : VerboseMessageQueue 

638 Queue for sending commands to the Modal Environment 

639 controller_communication_queue : VerboseMessageQueue 

640 Queue for sending global commands to the controller 

641 log_file_queue : Queue 

642 Queue where log file messages can be written. 

643 

644 """ 

645 super().__init__( 

646 environment_name, 

647 environment_command_queue, 

648 controller_communication_queue, 

649 log_file_queue, 

650 ) 

651 # Add the page to the control definition tabwidget 

652 self.definition_widget = QtWidgets.QWidget() 

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

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

655 # Add the page to the run tabwidget 

656 self.run_widget = QtWidgets.QWidget() 

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

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

659 

660 self.trigger_widgets = [ 

661 self.definition_widget.trigger_channel_selector, 

662 self.definition_widget.pretrigger_selector, 

663 self.definition_widget.trigger_slope_selector, 

664 self.definition_widget.trigger_level_selector, 

665 self.definition_widget.trigger_level_voltage_display, 

666 self.definition_widget.trigger_level_eu_display, 

667 self.definition_widget.hysteresis_selector, 

668 self.definition_widget.hysteresis_voltage_display, 

669 self.definition_widget.hysteresis_eu_display, 

670 self.definition_widget.hysteresis_length_selector, 

671 self.definition_widget.hysteresis_samples_display, 

672 self.definition_widget.hysteresis_time_display, 

673 ] 

674 

675 self.signal_generator_widgets = [ 

676 self.definition_widget.random_rms_selector, 

677 self.definition_widget.random_min_frequency_selector, 

678 self.definition_widget.random_max_frequency_selector, 

679 self.definition_widget.burst_rms_selector, 

680 self.definition_widget.burst_min_frequency_selector, 

681 self.definition_widget.burst_max_frequency_selector, 

682 self.definition_widget.burst_on_percentage_selector, 

683 self.definition_widget.pseudorandom_rms_selector, 

684 self.definition_widget.pseudorandom_min_frequency_selector, 

685 self.definition_widget.pseudorandom_max_frequency_selector, 

686 self.definition_widget.chirp_level_selector, 

687 self.definition_widget.chirp_min_frequency_selector, 

688 self.definition_widget.chirp_max_frequency_selector, 

689 self.definition_widget.square_level_selector, 

690 self.definition_widget.square_frequency_selector, 

691 self.definition_widget.square_percent_on_selector, 

692 self.definition_widget.sine_level_selector, 

693 self.definition_widget.sine_frequency_selector, 

694 ] 

695 

696 self.window_parameter_widgets = [ 

697 self.definition_widget.window_value_label, 

698 self.definition_widget.window_value_selector, 

699 ] 

700 

701 self.definition_widget.reference_channels_selector.setColumnCount(3) 

702 self.definition_widget.reference_channels_selector.setVerticalHeaderLabels( 

703 ["Enabled", "Reference", "Channel"] 

704 ) 

705 

706 self.data_acquisition_parameters = None 

707 self.environment_parameters = None 

708 self.channel_names = None 

709 self.acceptance_function = None 

710 self.plot_data_items = {} 

711 self.reference_channel_indices = None 

712 self.all_output_channel_indices = None 

713 self.response_channel_indices = None 

714 self.last_frame = None 

715 self.last_frf = None 

716 self.last_coherence = None 

717 self.last_response_cpsd = None 

718 self.last_reference_cpsd = None 

719 self.last_condition = None 

720 self.acquiring = False 

721 self.netcdf_handle = None 

722 self.override_table = {} 

723 self.reciprocal_responses = [] 

724 

725 # Store some information into the channel display so the plots have 

726 # access to it 

727 self.run_widget.channel_display_area.time_abscissa = None 

728 self.run_widget.channel_display_area.frequency_abscissa = None 

729 self.run_widget.channel_display_area.window_function = None 

730 self.run_widget.channel_display_area.last_frame = None 

731 self.run_widget.channel_display_area.last_spectrum = None 

732 self.run_widget.channel_display_area.last_autospectrum = None 

733 self.run_widget.channel_display_area.last_frf = None 

734 self.run_widget.channel_display_area.last_coh = None 

735 self.run_widget.channel_display_area.channel_names = None 

736 self.run_widget.channel_display_area.reference_channel_indices = None 

737 self.run_widget.channel_display_area.response_channel_indices = None 

738 

739 self.complete_ui() 

740 self.connect_callbacks() 

741 

742 @property 

743 def reference_indices(self): 

744 """Returns indices corresponding to the reference channels""" 

745 return [ 

746 i 

747 for i in range(self.definition_widget.reference_channels_selector.rowCount()) 

748 if self.definition_widget.reference_channels_selector.cellWidget(i, 0).isChecked() 

749 and self.definition_widget.reference_channels_selector.cellWidget(i, 1).isChecked() 

750 ] 

751 

752 @property 

753 def response_indices(self): 

754 """Returns indices corresponding to the response channels in a test""" 

755 return [ 

756 i 

757 for i in range(self.definition_widget.reference_channels_selector.rowCount()) 

758 if self.definition_widget.reference_channels_selector.cellWidget(i, 0).isChecked() 

759 and not self.definition_widget.reference_channels_selector.cellWidget(i, 1).isChecked() 

760 ] 

761 

762 @property 

763 def output_channel_indices(self): 

764 """Returns indices corresponding to the output channels in a test""" 

765 return [ 

766 i 

767 for i in self.all_output_channel_indices 

768 if self.definition_widget.reference_channels_selector.cellWidget(i, 0).isChecked() 

769 ] 

770 

771 @property 

772 def initialized_response_names(self): 

773 """Returns channel names corresponding to the initialized response channels""" 

774 return [ 

775 self.channel_names[i] 

776 for i in range(len(self.channel_names)) 

777 if i not in self.environment_parameters.response_channel_indices 

778 ] 

779 

780 @property 

781 def initialized_reference_names(self): 

782 """Returns channel names corresponding to the initialized reference channels""" 

783 return [ 

784 self.channel_names[i] for i in self.environment_parameters.reference_channel_indices 

785 ] 

786 

787 def complete_ui(self): 

788 """Applies some finishing touches to the UI""" 

789 self.definition_widget.system_id_averaging_coefficient_selector.setEnabled(False) 

790 for widget in self.trigger_widgets: 

791 widget.setEnabled(False) 

792 

793 # Set common look and feel for plots 

794 plot_widgets = [self.definition_widget.output_signal_plot] 

795 for plot_widget in plot_widgets: 

796 plot_item = plot_widget.getPlotItem() 

797 plot_item.showGrid(True, True, 0.25) 

798 plot_item.enableAutoRange() 

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

800 

801 # Disable the currently inactive portions of the definition layout 

802 self.definition_widget.system_id_averaging_coefficient_selector.setEnabled(False) 

803 for widget in self.window_parameter_widgets: 

804 widget.hide() 

805 

806 def connect_callbacks(self): 

807 """Connects callback functions to the user interface widgets""" 

808 # Definition Callbacks 

809 self.definition_widget.samples_per_frame_selector.valueChanged.connect( 

810 self.update_parameters 

811 ) 

812 self.definition_widget.system_id_overlap_percentage_selector.valueChanged.connect( 

813 self.update_parameters 

814 ) 

815 self.definition_widget.triggering_type_selector.currentIndexChanged.connect( 

816 self.activate_trigger_options 

817 ) 

818 self.definition_widget.acceptance_selector.currentIndexChanged.connect( 

819 self.select_acceptance 

820 ) 

821 self.definition_widget.trigger_channel_selector.currentIndexChanged.connect( 

822 self.update_trigger_levels 

823 ) 

824 self.definition_widget.trigger_level_selector.valueChanged.connect( 

825 self.update_trigger_levels 

826 ) 

827 self.definition_widget.hysteresis_selector.valueChanged.connect(self.update_trigger_levels) 

828 self.definition_widget.regenerate_signal_button.clicked.connect(self.generate_signal) 

829 self.definition_widget.signal_generator_selector.currentChanged.connect(self.update_signal) 

830 for widget in self.signal_generator_widgets: 

831 widget.valueChanged.connect(self.update_signal) 

832 self.definition_widget.check_selected_button.clicked.connect( 

833 self.check_selected_reference_channels 

834 ) 

835 self.definition_widget.uncheck_selected_button.clicked.connect( 

836 self.uncheck_selected_reference_channels 

837 ) 

838 self.definition_widget.enable_selected_button.clicked.connect(self.enable_selected_channels) 

839 self.definition_widget.disable_selected_button.clicked.connect( 

840 self.disable_selected_channels 

841 ) 

842 self.definition_widget.hysteresis_length_selector.valueChanged.connect( 

843 self.update_hysteresis_length 

844 ) 

845 self.definition_widget.system_id_averaging_scheme_selector.currentIndexChanged.connect( 

846 self.update_averaging_type 

847 ) 

848 self.definition_widget.system_id_transfer_function_computation_window_selector.currentIndexChanged.connect( 

849 self.update_window 

850 ) 

851 # Run Callbacks 

852 self.run_widget.preview_test_button.clicked.connect(self.preview_acquisition) 

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

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

855 self.run_widget.select_file_button.clicked.connect(self.select_file) 

856 self.run_widget.accept_average_button.clicked.connect(self.accept_frame) 

857 self.run_widget.reject_average_button.clicked.connect(self.reject_frame) 

858 self.run_widget.new_window_button.clicked.connect(self.new_window) 

859 self.run_widget.new_from_template_combobox.currentIndexChanged.connect( 

860 self.new_window_from_template 

861 ) 

862 self.run_widget.tile_layout_button.clicked.connect( 

863 self.run_widget.channel_display_area.tileSubWindows 

864 ) 

865 self.run_widget.close_all_button.clicked.connect(self.close_windows) 

866 self.run_widget.decrement_channels_button.clicked.connect(self.decrement_channels) 

867 self.run_widget.increment_channels_button.clicked.connect(self.increment_channels) 

868 self.run_widget.dof_override_table.itemChanged.connect(self.update_override_table) 

869 self.run_widget.add_override_button.clicked.connect(self.add_override_channel) 

870 self.run_widget.remove_override_button.clicked.connect(self.remove_override_channel) 

871 

872 # Definition Callbacks 

873 def update_parameters(self): 

874 """Updates widget values when fundamental signal processing parameters change""" 

875 if self.definition_widget.samples_per_frame_selector.value() % 2 == 1: 

876 self.definition_widget.samples_per_frame_selector.blockSignals(True) 

877 self.definition_widget.samples_per_frame_selector.setValue( 

878 self.definition_widget.samples_per_frame_selector.value() + 1 

879 ) 

880 self.definition_widget.samples_per_frame_selector.blockSignals(False) 

881 data = self.collect_environment_definition_parameters() 

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

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

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

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

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

887 if self.definition_widget.regenerate_signal_auto_checkbox.isChecked(): 

888 self.generate_signal() 

889 

890 def update_reference_channels(self): 

891 """Updates widgets based on changes in the selected reference channels""" 

892 self.definition_widget.response_channels_display.setValue(len(self.response_indices)) 

893 self.definition_widget.reference_channels_display.setValue(len(self.reference_indices)) 

894 self.definition_widget.output_channels_display.setValue(len(self.output_channel_indices)) 

895 if self.definition_widget.regenerate_signal_auto_checkbox.isChecked(): 

896 self.generate_signal() 

897 

898 def check_selected_reference_channels(self): 

899 """Checks reference channels that are selected in the list widget""" 

900 select = self.definition_widget.reference_channels_selector.selectionModel() 

901 rows = select.selectedRows() 

902 for row in rows: 

903 index = row.row() 

904 self.definition_widget.reference_channels_selector.cellWidget(index, 1).setChecked(True) 

905 

906 def uncheck_selected_reference_channels(self): 

907 """Unchecks reference channels that are selected in the list widget""" 

908 select = self.definition_widget.reference_channels_selector.selectionModel() 

909 rows = select.selectedRows() 

910 for row in rows: 

911 index = row.row() 

912 self.definition_widget.reference_channels_selector.cellWidget(index, 1).setChecked( 

913 False 

914 ) 

915 

916 def enable_selected_channels(self): 

917 """Enables channels that are selected in the list widget""" 

918 select = self.definition_widget.reference_channels_selector.selectionModel() 

919 rows = select.selectedRows() 

920 for row in rows: 

921 index = row.row() 

922 self.definition_widget.reference_channels_selector.cellWidget(index, 0).setChecked(True) 

923 

924 def disable_selected_channels(self): 

925 """Disables channels that are selected in the list widget""" 

926 select = self.definition_widget.reference_channels_selector.selectionModel() 

927 rows = select.selectedRows() 

928 for row in rows: 

929 index = row.row() 

930 self.definition_widget.reference_channels_selector.cellWidget(index, 0).setChecked( 

931 False 

932 ) 

933 

934 def activate_trigger_options(self): 

935 """Enables widgets corresponding to the trigger selection""" 

936 if self.definition_widget.triggering_type_selector.currentIndex() == 0: 

937 for widget in self.trigger_widgets: 

938 widget.setEnabled(False) 

939 else: 

940 for widget in self.trigger_widgets: 

941 widget.setEnabled(True) 

942 

943 def select_acceptance(self): 

944 """Selects the acceptance type and opens up a file dialog if necessary""" 

945 if self.definition_widget.acceptance_selector.currentIndex() == 2: 

946 # Open up a file dialog 

947 filename, _ = QtWidgets.QFileDialog.getOpenFileName( 

948 self.definition_widget, 

949 "Select Python Module", 

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

951 ) 

952 if filename == "": 

953 self.definition_widget.acceptance_selector.setCurrentIndex(0) 

954 return 

955 module = load_python_module(filename) 

956 functions = [ 

957 function 

958 for function in inspect.getmembers(module) 

959 if inspect.isfunction(function[1]) 

960 ] 

961 item, ok_pressed = QtWidgets.QInputDialog.getItem( 

962 self.definition_widget, 

963 "Select Acceptance Function", 

964 "Function Name:", 

965 [function[0] for function in functions], 

966 0, 

967 False, 

968 ) 

969 if ok_pressed: 

970 self.acceptance_function = [filename, item] 

971 else: 

972 self.definition_widget.acceptance_selector.setCurrentIndex(0) 

973 return 

974 else: 

975 self.acceptance_function = None 

976 

977 def update_trigger_levels(self): 

978 """Updates trigger levels based on selected widget values""" 

979 data = self.collect_environment_definition_parameters() 

980 t_v, t_eu, h_v, h_eu = data.get_trigger_levels( 

981 self.data_acquisition_parameters.channel_list 

982 ) 

983 self.definition_widget.trigger_level_voltage_display.setValue(t_v) 

984 self.definition_widget.trigger_level_eu_display.setValue(t_eu) 

985 self.definition_widget.hysteresis_voltage_display.setValue(h_v) 

986 self.definition_widget.hysteresis_eu_display.setValue(h_eu) 

987 eu_suffix = self.data_acquisition_parameters.channel_list[data.trigger_channel].unit 

988 self.definition_widget.hysteresis_eu_display.setSuffix( 

989 (" " + eu_suffix) if not (eu_suffix == "" or eu_suffix is None) else "" 

990 ) 

991 self.definition_widget.trigger_level_eu_display.setSuffix( 

992 (" " + eu_suffix) if not (eu_suffix == "" or eu_suffix is None) else "" 

993 ) 

994 

995 def update_hysteresis_length(self): 

996 """Updates hysterisis length based on the selected trigger parameters""" 

997 data = self.collect_environment_definition_parameters() 

998 self.definition_widget.hysteresis_samples_display.setValue(data.hysteresis_samples) 

999 self.definition_widget.hysteresis_time_display.setValue( 

1000 data.hysteresis_samples / data.sample_rate 

1001 ) 

1002 

1003 def update_signal(self): 

1004 """Updates the generated signal based on widget value changes""" 

1005 if self.definition_widget.regenerate_signal_auto_checkbox.isChecked(): 

1006 self.generate_signal() 

1007 

1008 def generate_signal(self): 

1009 """Generates an example signal to show in the definition widget""" 

1010 if self.data_acquisition_parameters is None: 

1011 return 

1012 output_oversample = self.data_acquisition_parameters.output_oversample 

1013 output_rate = self.data_acquisition_parameters.output_sample_rate 

1014 data = self.collect_environment_definition_parameters() 

1015 frame_output_samples = int(data.samples_per_frame * output_oversample) 

1016 signal = data.generate_signal() 

1017 # Reduce down to just one frame 

1018 while signal.shape[-1] < frame_output_samples: 

1019 signal = np.concatenate((signal, data.generate_signal()), axis=-1) 

1020 signal = signal[..., :frame_output_samples] 

1021 signal[data.disabled_signals] = 0 

1022 times = np.arange(frame_output_samples) / output_rate 

1023 for s, plot in zip(signal, self.plot_data_items["signal_representation"]): 

1024 plot.setData(times, s) 

1025 

1026 def update_averaging_type(self): 

1027 """Enables exponential averaging coefficient widgets if exponential averaging is chosen""" 

1028 if self.definition_widget.system_id_averaging_scheme_selector.currentIndex() == 0: 

1029 self.definition_widget.system_id_averaging_coefficient_selector.setEnabled(False) 

1030 else: 

1031 self.definition_widget.system_id_averaging_coefficient_selector.setEnabled(True) 

1032 

1033 def update_window(self): 

1034 """Shows additional window function options based on the selected window""" 

1035 if ( 

1036 self.definition_widget.system_id_transfer_function_computation_window_selector.currentIndex() 

1037 == 2 

1038 ): 

1039 for widget in self.window_parameter_widgets: 

1040 widget.show() 

1041 else: 

1042 for widget in self.window_parameter_widgets: 

1043 widget.hide() 

1044 

1045 # Run Callbacks 

1046 def preview_acquisition(self): 

1047 """Tells the environment process to start in preview mode""" 

1048 self.run_widget.stop_test_button.setEnabled(True) 

1049 self.run_widget.preview_test_button.setEnabled(False) 

1050 self.run_widget.start_test_button.setEnabled(False) 

1051 self.run_widget.select_file_button.setEnabled(False) 

1052 self.controller_communication_queue.put( 

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

1054 ) 

1055 self.environment_command_queue.put(self.log_name, (ModalCommands.START_CONTROL, None)) 

1056 self.run_widget.dof_override_table.setEnabled(False) 

1057 self.run_widget.add_override_button.setEnabled(False) 

1058 self.run_widget.remove_override_button.setEnabled(False) 

1059 

1060 def start_control(self): 

1061 """Tells the environment process to start in acquisition mode""" 

1062 self.acquiring = True 

1063 # Create the output file 

1064 filename = self.run_widget.data_file_selector.text() 

1065 if filename == "": 

1066 error_message_qt("Invalid File", "Please select a file in which to store modal data") 

1067 return 

1068 if self.run_widget.autoincrement_checkbox.isChecked(): 

1069 # Add the file increment 

1070 path, ext = os.path.splitext(filename) 

1071 index = len(glob(path + "*" + ext)) 

1072 filename = path + f"_{index:04d}" + ext 

1073 self.create_netcdf_file(filename) 

1074 self.preview_acquisition() 

1075 

1076 def stop_control(self): 

1077 """Tells the environment process to stop the current measurement""" 

1078 self.environment_command_queue.put(self.log_name, (ModalCommands.STOP_CONTROL, None)) 

1079 

1080 def select_file(self): 

1081 """Brings up a file dialog box to select the save file location""" 

1082 filename, _ = QtWidgets.QFileDialog.getSaveFileName( 

1083 self.run_widget, 

1084 "Select NetCDF File to Save Modal Data", 

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

1086 ) 

1087 if filename == "": 

1088 return 

1089 self.run_widget.data_file_selector.setText(filename) 

1090 

1091 def accept_frame(self): 

1092 """Sends a signal to the environment process to accept the current measurement frame""" 

1093 self.environment_command_queue.put(self.log_name, (ModalCommands.ACCEPT_FRAME, True)) 

1094 self.run_widget.accept_average_button.setEnabled(False) 

1095 self.run_widget.reject_average_button.setEnabled(False) 

1096 

1097 def reject_frame(self): 

1098 """Sends a signal to the environment process to reject the current measurement frame""" 

1099 self.environment_command_queue.put(self.log_name, (ModalCommands.ACCEPT_FRAME, False)) 

1100 self.run_widget.accept_average_button.setEnabled(False) 

1101 self.run_widget.reject_average_button.setEnabled(False) 

1102 

1103 def new_window(self): 

1104 """Creates a new window to display modal data""" 

1105 widget = ModalMDISubWindow(self.run_widget.channel_display_area) 

1106 self.run_widget.channel_display_area.addSubWindow(widget) 

1107 widget.show() 

1108 return widget 

1109 # print('Windows: {:}'.format(self.run_widget.channel_display_area.subWindowList())) 

1110 

1111 def new_window_from_template(self): 

1112 """Creates a new window from the template functions""" 

1113 if self.run_widget.new_from_template_combobox.currentIndex() == 0: 

1114 return 

1115 elif self.run_widget.new_from_template_combobox.currentIndex() == 6: 

1116 # 3x3 channel grid 

1117 for i in range(9): 

1118 widget = self.new_window() 

1119 widget.signal_selector.setCurrentIndex(0) 

1120 widget.response_coordinate_selector.setCurrentIndex(i) 

1121 widget.lock_response_checkbox.setChecked(False) 

1122 elif self.run_widget.new_from_template_combobox.currentIndex() == 5: 

1123 # Reference autospectra 

1124 for index in self.run_widget.channel_display_area.reference_channel_indices: 

1125 widget = self.new_window() 

1126 widget.signal_selector.setCurrentIndex(3) 

1127 widget.response_coordinate_selector.setCurrentIndex(index) 

1128 widget.lock_response_checkbox.setChecked(True) 

1129 else: 

1130 corresponding_drive_responses = ( 

1131 self.run_widget.channel_display_area.reciprocal_responses 

1132 ) 

1133 if self.run_widget.new_from_template_combobox.currentIndex() == 1: 

1134 # Create drive point FRFs in magnitude 

1135 for i, index in enumerate(corresponding_drive_responses): 

1136 widget = self.new_window() 

1137 widget.signal_selector.setCurrentIndex(4) 

1138 widget.response_coordinate_selector.setCurrentIndex(index) 

1139 widget.reference_coordinate_selector.setCurrentIndex(i) 

1140 widget.data_type_selector.setCurrentIndex(0) 

1141 widget.lock_response_checkbox.setChecked(True) 

1142 elif self.run_widget.new_from_template_combobox.currentIndex() == 2: 

1143 # Create drive point FRFs in imaginary 

1144 for i, index in enumerate(corresponding_drive_responses): 

1145 widget = self.new_window() 

1146 widget.signal_selector.setCurrentIndex(4) 

1147 widget.response_coordinate_selector.setCurrentIndex(index) 

1148 widget.reference_coordinate_selector.setCurrentIndex(i) 

1149 widget.data_type_selector.setCurrentIndex(3) 

1150 widget.lock_response_checkbox.setChecked(True) 

1151 elif self.run_widget.new_from_template_combobox.currentIndex() == 3: 

1152 # Create drive point Coherence 

1153 for i, index in enumerate(corresponding_drive_responses): 

1154 widget = self.new_window() 

1155 widget.signal_selector.setCurrentIndex(6) 

1156 widget.response_coordinate_selector.setCurrentIndex(index) 

1157 widget.reference_coordinate_selector.setCurrentIndex(i) 

1158 widget.lock_response_checkbox.setChecked(True) 

1159 elif self.run_widget.new_from_template_combobox.currentIndex() == 4: 

1160 # Create drive point Coherence 

1161 for i, index in enumerate(corresponding_drive_responses): 

1162 for j, index in enumerate(corresponding_drive_responses): 

1163 if i <= j: 

1164 continue 

1165 widget = self.new_window() 

1166 widget.signal_selector.setCurrentIndex(7) 

1167 widget.response_coordinate_selector.setCurrentIndex(i) 

1168 widget.reference_coordinate_selector.setCurrentIndex(j) 

1169 widget.lock_response_checkbox.setChecked(True) 

1170 self.run_widget.new_from_template_combobox.setCurrentIndex(0) 

1171 

1172 def close_windows(self): 

1173 """Closes all existing windows""" 

1174 for window in self.run_widget.channel_display_area.subWindowList(): 

1175 window.close() 

1176 

1177 def decrement_channels(self): 

1178 """Decrements the unlocked window response channels by the specified number of channels""" 

1179 number = -self.run_widget.increment_channels_number.value() 

1180 for window in self.run_widget.channel_display_area.subWindowList(): 

1181 window.widget().increment_channel(number) 

1182 

1183 def increment_channels(self): 

1184 """Increments the unlocked window response channels by the specified number of channels""" 

1185 number = self.run_widget.increment_channels_number.value() 

1186 for window in self.run_widget.channel_display_area.subWindowList(): 

1187 window.widget().increment_channel(number) 

1188 

1189 def add_override_channel(self): 

1190 """Adds a row to the channel override table""" 

1191 selected_row = self.run_widget.dof_override_table.blockSignals(True) 

1192 selected_row = self.run_widget.dof_override_table.rowCount() 

1193 self.run_widget.dof_override_table.insertRow(selected_row) 

1194 channel_combobox = QtWidgets.QComboBox() 

1195 for channel_name in self.channel_names: 

1196 channel_combobox.addItem(channel_name) 

1197 channel_combobox.currentIndexChanged.connect(self.update_override_table) 

1198 self.run_widget.dof_override_table.setCellWidget(selected_row, 0, channel_combobox) 

1199 data_item = QtWidgets.QTableWidgetItem() 

1200 data_item.setText("1") 

1201 self.run_widget.dof_override_table.setItem(selected_row, 1, data_item) 

1202 data_item = QtWidgets.QTableWidgetItem() 

1203 data_item.setText("X+") 

1204 self.run_widget.dof_override_table.setItem(selected_row, 2, data_item) 

1205 selected_row = self.run_widget.dof_override_table.blockSignals(False) 

1206 self.update_override_table() 

1207 

1208 def remove_override_channel(self): 

1209 """Removes a row from the channel override table""" 

1210 selected_row = self.run_widget.dof_override_table.currentRow() 

1211 if selected_row >= 0: 

1212 self.run_widget.dof_override_table.removeRow(selected_row) 

1213 self.update_override_table() 

1214 

1215 def update_override_table(self): 

1216 """Updates channel information in the test based on the override table values""" 

1217 self.override_table = {} 

1218 for row in range(self.run_widget.dof_override_table.rowCount()): 

1219 index = self.run_widget.dof_override_table.cellWidget(row, 0).currentIndex() 

1220 new_node = self.run_widget.dof_override_table.item(row, 1).text() 

1221 new_direction = self.run_widget.dof_override_table.item(row, 2).text() 

1222 self.override_table[index] = [new_node, new_direction] 

1223 self.update_channel_names() 

1224 self.run_widget.channel_display_area.reciprocal_responses = ( 

1225 self.get_reciprocal_measurements() 

1226 ) 

1227 # Go through and update all the existing windows in the MDI display 

1228 for window in self.run_widget.channel_display_area.subWindowList(): 

1229 widget = window.widget() 

1230 current_response = widget.response_coordinate_selector.currentIndex() 

1231 current_reference = widget.reference_coordinate_selector.currentIndex() 

1232 current_data_type = widget.data_type_selector.currentIndex() 

1233 widget.channel_names = self.channel_names 

1234 widget.reference_names = [ 

1235 self.channel_names[i] 

1236 for i in self.run_widget.channel_display_area.reference_channel_indices 

1237 ] 

1238 widget.response_names = [ 

1239 self.channel_names[i] 

1240 for i in self.run_widget.channel_display_area.response_channel_indices 

1241 ] 

1242 widget.reciprocal_responses = self.run_widget.channel_display_area.reciprocal_responses 

1243 widget.update_ui() 

1244 widget.response_coordinate_selector.setCurrentIndex(current_response) 

1245 widget.reference_coordinate_selector.setCurrentIndex(current_reference) 

1246 widget.data_type_selector.setCurrentIndex(current_data_type) 

1247 

1248 def get_reciprocal_measurements(self): 

1249 """Finds all reciprocal measurements in the test""" 

1250 node_numbers = np.array( 

1251 [ 

1252 (channel.node_number if i not in self.override_table else self.override_table[i][0]) 

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

1254 ] 

1255 ) 

1256 node_directions = np.array( 

1257 [ 

1258 ( 

1259 "" 

1260 if channel.node_direction is None 

1261 else "".join( 

1262 [ 

1263 char 

1264 for char in ( 

1265 channel.node_direction 

1266 if i not in self.override_table 

1267 else self.override_table[i][1] 

1268 ) 

1269 if char not in "+-" 

1270 ] 

1271 ) 

1272 ) 

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

1274 ] 

1275 ) 

1276 reference_node_numbers = node_numbers[self.environment_parameters.reference_channel_indices] 

1277 reference_node_directions = node_directions[ 

1278 self.environment_parameters.reference_channel_indices 

1279 ] 

1280 response_node_numbers = node_numbers[self.environment_parameters.response_channel_indices] 

1281 response_node_directions = node_directions[ 

1282 self.environment_parameters.response_channel_indices 

1283 ] 

1284 corresponding_drive_responses = [] 

1285 for node, direction in zip(reference_node_numbers, reference_node_directions): 

1286 # print('Node: {:} Direction: {:}'.format(node,direction)) 

1287 # print('Response Node Numbers:') 

1288 # print(response_node_numbers) 

1289 # print('Response Node Directions:') 

1290 # print(response_node_directions) 

1291 # print('Node Match:') 

1292 # print(response_node_numbers == node) 

1293 # print('Direction Match:') 

1294 # print(response_node_directions == direction) 

1295 index = np.where( 

1296 (response_node_numbers == node) & (response_node_directions == direction) 

1297 )[0] 

1298 # print('Index:') 

1299 # print(index) 

1300 if len(index) == 0: 

1301 corresponding_drive_responses.append(None) 

1302 print(f"Warning: No Drive Point Found for Reference {node}{direction}") 

1303 elif len(index) > 1: 

1304 corresponding_drive_responses.append(None) 

1305 print(f"Warning: Multiple Drive Points Found for Reference {node}{direction}") 

1306 else: 

1307 corresponding_drive_responses.append(index[0]) 

1308 # print(corresponding_drive_responses) 

1309 return corresponding_drive_responses 

1310 

1311 def create_netcdf_file(self, filename): 

1312 """Creates an output NetCDF4 file to save modal data to 

1313 

1314 Parameters 

1315 ---------- 

1316 filename : str 

1317 The file name to which the netCDF4 file will be stored 

1318 """ 

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

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

1321 ) 

1322 # Create dimensions 

1323 self.netcdf_handle.createDimension( 

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

1325 ) 

1326 self.netcdf_handle.createDimension( 

1327 "output_channels", 

1328 len( 

1329 [ 

1330 channel 

1331 for channel in self.data_acquisition_parameters.channel_list 

1332 if channel.feedback_device is not None 

1333 ] 

1334 ), 

1335 ) 

1336 self.netcdf_handle.createDimension( 

1337 "num_environments", len(self.data_acquisition_parameters.environment_names) 

1338 ) 

1339 self.netcdf_handle.createDimension("time_samples", None) 

1340 # Create attributes 

1341 self.netcdf_handle.sample_rate = self.data_acquisition_parameters.sample_rate 

1342 self.netcdf_handle.time_per_write = ( 

1343 self.data_acquisition_parameters.samples_per_write 

1344 / self.data_acquisition_parameters.output_sample_rate 

1345 ) 

1346 self.netcdf_handle.time_per_read = ( 

1347 self.data_acquisition_parameters.samples_per_read 

1348 / self.data_acquisition_parameters.sample_rate 

1349 ) 

1350 self.netcdf_handle.hardware = self.data_acquisition_parameters.hardware 

1351 self.netcdf_handle.hardware_file = ( 

1352 "None" 

1353 if self.data_acquisition_parameters.hardware_file is None 

1354 else self.data_acquisition_parameters.hardware_file 

1355 ) 

1356 self.netcdf_handle.output_oversample = self.data_acquisition_parameters.output_oversample 

1357 for name, value in self.data_acquisition_parameters.extra_parameters.items(): 

1358 setattr(self.netcdf_handle, name, value) 

1359 # Create Variables 

1360 self.netcdf_handle.createVariable("time_data", "f8", ("response_channels", "time_samples")) 

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

1362 this_environment_index = None 

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

1364 var[i] = name 

1365 if name == self.environment_name: 

1366 this_environment_index = i 

1367 var = self.netcdf_handle.createVariable( 

1368 "environment_active_channels", 

1369 "i1", 

1370 ("response_channels", "num_environments"), 

1371 ) 

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

1373 self.data_acquisition_parameters.environment_active_channels[:, this_environment_index], 

1374 :, 

1375 ] 

1376 # Create channel table variables 

1377 labels = [ 

1378 ["node_number", str], 

1379 ["node_direction", str], 

1380 ["comment", str], 

1381 ["serial_number", str], 

1382 ["triax_dof", str], 

1383 ["sensitivity", str], 

1384 ["unit", str], 

1385 ["make", str], 

1386 ["model", str], 

1387 ["expiration", str], 

1388 ["physical_device", str], 

1389 ["physical_channel", str], 

1390 ["channel_type", str], 

1391 ["minimum_value", str], 

1392 ["maximum_value", str], 

1393 ["coupling", str], 

1394 ["excitation_source", str], 

1395 ["excitation", str], 

1396 ["feedback_device", str], 

1397 ["feedback_channel", str], 

1398 ["warning_level", str], 

1399 ["abort_level", str], 

1400 ] 

1401 for label, netcdf_datatype in labels: 

1402 var = self.netcdf_handle.createVariable( 

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

1404 ) 

1405 channel_data = [ 

1406 getattr(channel, label) for channel in self.data_acquisition_parameters.channel_list 

1407 ] 

1408 if netcdf_datatype == "i1": 

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

1410 else: 

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

1412 for i, cd in enumerate(channel_data): 

1413 if label == "node_number" and i in self.override_table: 

1414 var[i] = self.override_table[i][0] 

1415 elif label == "node_direction" and i in self.override_table: 

1416 var[i] = self.override_table[i][1] 

1417 else: 

1418 var[i] = cd 

1419 group_handle = self.netcdf_handle.createGroup(self.environment_name) 

1420 self.environment_parameters.store_to_netcdf(group_handle) 

1421 group_handle.createDimension("fft_lines", self.environment_parameters.fft_lines) 

1422 group_handle.createVariable( 

1423 "frf_data_real", 

1424 "f8", 

1425 ("fft_lines", "response_channels", "reference_channels"), 

1426 ) 

1427 group_handle.createVariable( 

1428 "frf_data_imag", 

1429 "f8", 

1430 ("fft_lines", "response_channels", "reference_channels"), 

1431 ) 

1432 group_handle.createVariable("coherence", "f8", ("fft_lines", "response_channels")) 

1433 

1434 def collect_environment_definition_parameters(self) -> AbstractMetadata: 

1435 """ 

1436 Collect the parameters from the user interface defining the environment 

1437 

1438 Returns 

1439 ------- 

1440 ModalMetadata 

1441 A metadata or parameters object containing the parameters defining 

1442 the corresponding environment. 

1443 

1444 """ 

1445 return ModalMetadata.from_ui(self) 

1446 

1447 def update_channel_names(self): 

1448 """Updates channel names based on the override channel table""" 

1449 self.channel_names = [] 

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

1451 channel_type_str = "" if channel.channel_type is None else channel.channel_type 

1452 node_num_str = ( 

1453 channel.node_number if i not in self.override_table else self.override_table[i][0] 

1454 ) 

1455 node_dir_str = ( 

1456 channel.node_direction 

1457 if i not in self.override_table 

1458 else self.override_table[i][1] 

1459 ) 

1460 self.channel_names.append( 

1461 f"{channel_type_str} {node_num_str} {node_dir_str}"[:MAXIMUM_NAME_LENGTH] 

1462 ) 

1463 self.run_widget.channel_display_area.channel_names = self.channel_names 

1464 

1465 def initialize_data_acquisition(self, data_acquisition_parameters: DataAcquisitionParameters): 

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

1467 

1468 This function is called when the Data Acquisition parameters are 

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

1470 accordingly. 

1471 

1472 Parameters 

1473 ---------- 

1474 data_acquisition_parameters : DataAcquisitionParameters : 

1475 Container containing the data acquisition parameters, including 

1476 channel table and sampling information. 

1477 

1478 """ 

1479 self.data_acquisition_parameters = data_acquisition_parameters 

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

1481 self.all_output_channel_indices = [ 

1482 index 

1483 for index, channel in enumerate(self.data_acquisition_parameters.channel_list) 

1484 if channel.feedback_device is not None 

1485 ] 

1486 self.update_channel_names() 

1487 self.definition_widget.reference_channels_selector.setRowCount(0) 

1488 self.definition_widget.trigger_channel_selector.blockSignals(True) 

1489 self.definition_widget.trigger_channel_selector.clear() 

1490 for i, channel_name in enumerate(self.channel_names): 

1491 self.definition_widget.trigger_channel_selector.addItem(channel_name) 

1492 self.definition_widget.reference_channels_selector.insertRow(i) 

1493 item = QtWidgets.QTableWidgetItem() 

1494 item.setText(channel_name) 

1495 item.setFlags(item.flags() & ~Qt.ItemIsEditable) 

1496 self.definition_widget.reference_channels_selector.setItem(i, 2, item) 

1497 ref_checkbox = QtWidgets.QCheckBox() 

1498 ref_checkbox.stateChanged.connect(self.update_reference_channels) 

1499 self.definition_widget.reference_channels_selector.setCellWidget(i, 1, ref_checkbox) 

1500 enabled_checkbox = QtWidgets.QCheckBox() 

1501 enabled_checkbox.setChecked(True) 

1502 enabled_checkbox.stateChanged.connect(self.update_reference_channels) 

1503 self.definition_widget.reference_channels_selector.setCellWidget(i, 0, enabled_checkbox) 

1504 self.definition_widget.trigger_channel_selector.blockSignals(False) 

1505 self.update_trigger_levels() 

1506 

1507 checked_state = self.definition_widget.regenerate_signal_auto_checkbox.isChecked() 

1508 self.definition_widget.regenerate_signal_auto_checkbox.setChecked(False) 

1509 self.definition_widget.signal_generator_selector.setCurrentIndex(0) 

1510 self.definition_widget.samples_per_frame_selector.setValue( 

1511 data_acquisition_parameters.sample_rate 

1512 ) 

1513 self.definition_widget.random_max_frequency_selector.setValue( 

1514 data_acquisition_parameters.sample_rate / 2 

1515 ) 

1516 self.definition_widget.random_min_frequency_selector.setValue(0) 

1517 self.definition_widget.burst_max_frequency_selector.setValue( 

1518 data_acquisition_parameters.sample_rate / 2 

1519 ) 

1520 self.definition_widget.burst_min_frequency_selector.setValue(0) 

1521 self.definition_widget.chirp_max_frequency_selector.setValue( 

1522 data_acquisition_parameters.sample_rate / 2 

1523 ) 

1524 self.definition_widget.chirp_min_frequency_selector.setValue(0) 

1525 self.definition_widget.pseudorandom_max_frequency_selector.setValue( 

1526 data_acquisition_parameters.sample_rate / 2 

1527 ) 

1528 self.definition_widget.pseudorandom_min_frequency_selector.setValue(0) 

1529 

1530 self.definition_widget.response_channels_display.setValue(len(self.channel_names)) 

1531 self.definition_widget.reference_channels_display.setValue(0) 

1532 num_outputs = len(self.output_channel_indices) 

1533 self.definition_widget.output_channels_display.setValue(num_outputs) 

1534 if num_outputs == 0: 

1535 for i in range(self.definition_widget.signal_generator_selector.count() - 1): 

1536 self.definition_widget.signal_generator_selector.setTabEnabled(i + 1, False) 

1537 

1538 self.definition_widget.output_signal_plot.getPlotItem().clear() 

1539 self.plot_data_items["signal_representation"] = multiline_plotter( 

1540 (0, 1), 

1541 np.zeros((len(self.all_output_channel_indices), 2)), 

1542 widget=self.definition_widget.output_signal_plot, 

1543 other_pen_options={"width": 1}, 

1544 names=[f"Output {i + 1}" for i in range(len(self.all_output_channel_indices))], 

1545 ) 

1546 self.definition_widget.regenerate_signal_auto_checkbox.setChecked(checked_state) 

1547 if checked_state: 

1548 self.generate_signal() 

1549 

1550 for widget in [ 

1551 self.definition_widget.random_min_frequency_selector, 

1552 self.definition_widget.random_max_frequency_selector, 

1553 self.definition_widget.burst_min_frequency_selector, 

1554 self.definition_widget.burst_max_frequency_selector, 

1555 self.definition_widget.pseudorandom_min_frequency_selector, 

1556 self.definition_widget.pseudorandom_max_frequency_selector, 

1557 self.definition_widget.chirp_min_frequency_selector, 

1558 self.definition_widget.chirp_max_frequency_selector, 

1559 self.definition_widget.square_frequency_selector, 

1560 self.definition_widget.sine_frequency_selector, 

1561 ]: 

1562 widget.setMaximum(self.data_acquisition_parameters.sample_rate / 2) 

1563 

1564 def initialize_environment(self) -> AbstractMetadata: 

1565 """ 

1566 Update the user interface with environment parameters 

1567 

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

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

1570 return the parameters class of the environment that inherits from 

1571 AbstractMetadata. 

1572 

1573 Returns 

1574 ModalMetadata 

1575 An AbstractMetadata-inheriting object that contains the parameters 

1576 defining the environment. 

1577 

1578 """ 

1579 self.environment_parameters = self.collect_environment_definition_parameters() 

1580 self.reference_channel_indices = self.environment_parameters.reference_channel_indices 

1581 self.response_channel_indices = self.environment_parameters.response_channel_indices 

1582 self.run_widget.channel_display_area.reference_channel_indices = ( 

1583 self.reference_channel_indices 

1584 ) 

1585 self.run_widget.channel_display_area.response_channel_indices = ( 

1586 self.response_channel_indices 

1587 ) 

1588 for window in self.run_widget.channel_display_area.subWindowList(): 

1589 widget = window.widget() 

1590 current_response = widget.response_coordinate_selector.currentIndex() 

1591 current_reference = widget.reference_coordinate_selector.currentIndex() 

1592 current_data_type = widget.data_type_selector.currentIndex() 

1593 widget.reference_names = np.array( 

1594 [ 

1595 widget.channel_names[i] 

1596 for i in self.run_widget.channel_display_area.reference_channel_indices 

1597 ] 

1598 ) 

1599 widget.response_names = np.array( 

1600 [ 

1601 widget.channel_names[i] 

1602 for i in self.run_widget.channel_display_area.response_channel_indices 

1603 ] 

1604 ) 

1605 widget.update_ui() 

1606 widget.response_coordinate_selector.setCurrentIndex(current_response) 

1607 widget.reference_coordinate_selector.setCurrentIndex(current_reference) 

1608 widget.data_type_selector.setCurrentIndex(current_data_type) 

1609 self.run_widget.total_averages_display.setValue(self.environment_parameters.num_averages) 

1610 self.run_widget.channel_display_area.time_abscissa = ( 

1611 np.arange(self.environment_parameters.samples_per_frame) 

1612 / self.environment_parameters.sample_rate 

1613 ) 

1614 self.run_widget.channel_display_area.frequency_abscissa = np.fft.rfftfreq( 

1615 self.environment_parameters.samples_per_frame, 

1616 1 / self.environment_parameters.sample_rate, 

1617 ) 

1618 if self.environment_parameters.frf_window == "rectangle": 

1619 window = 1 

1620 elif self.environment_parameters.frf_window == "exponential": 

1621 window_parameter = -(self.environment_parameters.samples_per_frame) / np.log( 

1622 self.environment_parameters.exponential_window_value_at_frame_end 

1623 ) 

1624 window = sig.get_window( 

1625 ("exponential", 0, window_parameter), 

1626 self.environment_parameters.samples_per_frame, 

1627 fftbins=True, 

1628 ) 

1629 else: 

1630 window = sig.get_window( 

1631 self.environment_parameters.frf_window, 

1632 self.environment_parameters.samples_per_frame, 

1633 fftbins=True, 

1634 ) 

1635 self.run_widget.channel_display_area.window_function = window 

1636 self.run_widget.channel_display_area.reciprocal_responses = ( 

1637 self.get_reciprocal_measurements() 

1638 ) 

1639 

1640 return self.environment_parameters 

1641 

1642 def retrieve_metadata( 

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

1644 ): 

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

1646 

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

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

1649 in the user interface with the proper information. 

1650 

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

1652 function in the ModalMetadata class, which will write parameters to 

1653 the netCDF file to document the metadata. 

1654 

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

1656 should collect parameters pertaining to the environment from a Group 

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

1658 

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

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

1661 

1662 Parameters 

1663 ---------- 

1664 netcdf_handle : nc4._netCDF4.Dataset : 

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

1666 a group name with the enviroment's name. 

1667 

1668 """ 

1669 netcdf_group_handle = netcdf_handle[self.environment_name] 

1670 self.definition_widget.samples_per_frame_selector.setValue( 

1671 netcdf_group_handle.samples_per_frame 

1672 ) 

1673 self.definition_widget.system_id_averaging_scheme_selector.setCurrentIndex( 

1674 self.definition_widget.system_id_averaging_scheme_selector.findText( 

1675 netcdf_group_handle.averaging_type 

1676 ) 

1677 ) 

1678 self.definition_widget.system_id_frames_to_average_selector.setValue( 

1679 netcdf_group_handle.num_averages 

1680 ) 

1681 self.definition_widget.system_id_averaging_coefficient_selector.setValue( 

1682 netcdf_group_handle.averaging_coefficient 

1683 ) 

1684 self.definition_widget.system_id_frf_technique_selector.setCurrentIndex( 

1685 self.definition_widget.system_id_frf_technique_selector.findText( 

1686 netcdf_group_handle.frf_technique 

1687 ) 

1688 ) 

1689 self.definition_widget.system_id_transfer_function_computation_window_selector.setCurrentIndex( 

1690 self.definition_widget.system_id_transfer_function_computation_window_selector.findText( 

1691 netcdf_group_handle.frf_window.capitalize() 

1692 ) 

1693 ) 

1694 self.definition_widget.system_id_overlap_percentage_selector.setValue( 

1695 netcdf_group_handle.overlap * 100 

1696 ) 

1697 self.definition_widget.triggering_type_selector.setCurrentIndex( 

1698 self.definition_widget.triggering_type_selector.findText( 

1699 netcdf_group_handle.trigger_type 

1700 ) 

1701 ) 

1702 acceptance = netcdf_group_handle.accept_type 

1703 self.definition_widget.acceptance_selector.blockSignals(True) 

1704 self.definition_widget.acceptance_selector.setCurrentIndex( 

1705 self.definition_widget.acceptance_selector.findText(acceptance) 

1706 ) 

1707 self.definition_widget.acceptance_selector.blockSignals(False) 

1708 if acceptance == "Autoreject...": 

1709 self.acceptance_function = netcdf_group_handle.acceptance_function.split(":") 

1710 else: 

1711 self.acceptance_function = None 

1712 self.definition_widget.wait_for_steady_selector.setValue( 

1713 netcdf_group_handle.wait_for_steady_state 

1714 ) 

1715 self.definition_widget.trigger_channel_selector.setCurrentIndex( 

1716 netcdf_group_handle.trigger_channel 

1717 ) 

1718 self.definition_widget.pretrigger_selector.setValue(netcdf_group_handle.pretrigger * 100) 

1719 self.definition_widget.trigger_slope_selector.setCurrentIndex( 

1720 0 if netcdf_group_handle.trigger_slope_positive == 1 else 1 

1721 ) 

1722 self.definition_widget.trigger_level_selector.setValue( 

1723 100 * netcdf_group_handle.trigger_level 

1724 ) 

1725 self.definition_widget.hysteresis_selector.setValue( 

1726 100 * netcdf_group_handle.hysteresis_level 

1727 ) 

1728 self.definition_widget.hysteresis_length_selector.setValue( 

1729 100 * netcdf_group_handle.hysteresis_length 

1730 ) 

1731 self.definition_widget.signal_generator_selector.setCurrentIndex( 

1732 [ 

1733 "none", 

1734 "random", 

1735 "burst", 

1736 "pseudorandom", 

1737 "chirp", 

1738 "square", 

1739 "sine", 

1740 ].index(netcdf_group_handle.signal_generator_type) 

1741 ) 

1742 self.definition_widget.random_rms_selector.setValue( 

1743 netcdf_group_handle.signal_generator_level 

1744 ) 

1745 self.definition_widget.random_min_frequency_selector.setValue( 

1746 netcdf_group_handle.signal_generator_min_frequency 

1747 ) 

1748 self.definition_widget.random_max_frequency_selector.setValue( 

1749 netcdf_group_handle.signal_generator_max_frequency 

1750 ) 

1751 self.definition_widget.burst_rms_selector.setValue( 

1752 netcdf_group_handle.signal_generator_level 

1753 ) 

1754 self.definition_widget.burst_min_frequency_selector.setValue( 

1755 netcdf_group_handle.signal_generator_min_frequency 

1756 ) 

1757 self.definition_widget.burst_max_frequency_selector.setValue( 

1758 netcdf_group_handle.signal_generator_max_frequency 

1759 ) 

1760 self.definition_widget.burst_on_percentage_selector.setValue( 

1761 100 * netcdf_group_handle.signal_generator_on_fraction 

1762 ) 

1763 self.definition_widget.pseudorandom_rms_selector.setValue( 

1764 netcdf_group_handle.signal_generator_level 

1765 ) 

1766 self.definition_widget.pseudorandom_min_frequency_selector.setValue( 

1767 netcdf_group_handle.signal_generator_min_frequency 

1768 ) 

1769 self.definition_widget.pseudorandom_max_frequency_selector.setValue( 

1770 netcdf_group_handle.signal_generator_max_frequency 

1771 ) 

1772 self.definition_widget.chirp_level_selector.setValue( 

1773 netcdf_group_handle.signal_generator_level 

1774 ) 

1775 self.definition_widget.chirp_min_frequency_selector.setValue( 

1776 netcdf_group_handle.signal_generator_min_frequency 

1777 ) 

1778 self.definition_widget.chirp_max_frequency_selector.setValue( 

1779 netcdf_group_handle.signal_generator_max_frequency 

1780 ) 

1781 self.definition_widget.square_level_selector.setValue( 

1782 netcdf_group_handle.signal_generator_level 

1783 ) 

1784 self.definition_widget.square_frequency_selector.setValue( 

1785 netcdf_group_handle.signal_generator_min_frequency 

1786 ) 

1787 self.definition_widget.square_percent_on_selector.setValue( 

1788 100 * netcdf_group_handle.signal_generator_on_fraction 

1789 ) 

1790 self.definition_widget.sine_level_selector.setValue( 

1791 netcdf_group_handle.signal_generator_level 

1792 ) 

1793 self.definition_widget.sine_frequency_selector.setValue( 

1794 netcdf_group_handle.signal_generator_min_frequency 

1795 ) 

1796 self.definition_widget.window_value_selector.setValue( 

1797 netcdf_group_handle.exponential_window_value_at_frame_end * 100 

1798 ) 

1799 response_inds = netcdf_group_handle.variables["response_channel_indices"][...] 

1800 reference_inds = netcdf_group_handle.variables["reference_channel_indices"][...] 

1801 for row in range(self.definition_widget.reference_channels_selector.rowCount()): 

1802 if row in reference_inds: 

1803 widget = self.definition_widget.reference_channels_selector.cellWidget(row, 1) 

1804 widget.setChecked(True) 

1805 widget = self.definition_widget.reference_channels_selector.cellWidget(row, 0) 

1806 widget.setChecked(True) 

1807 elif row in response_inds: 

1808 widget = self.definition_widget.reference_channels_selector.cellWidget(row, 0) 

1809 widget.setChecked(True) 

1810 widget = self.definition_widget.reference_channels_selector.cellWidget(row, 1) 

1811 widget.setChecked(False) 

1812 else: 

1813 widget = self.definition_widget.reference_channels_selector.cellWidget(row, 0) 

1814 widget.setChecked(False) 

1815 widget = self.definition_widget.reference_channels_selector.cellWidget(row, 1) 

1816 widget.setChecked(False) 

1817 

1818 def update_gui(self, queue_data: tuple): 

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

1820 

1821 This function will receive data from the gui_update_queue that 

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

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

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

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

1826 

1827 Parameters 

1828 ---------- 

1829 queue_data : tuple 

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

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

1832 the data used to perform the operation. 

1833 """ 

1834 # print('Got GUI Update {:}'.format(queue_data[0])) 

1835 message, data = queue_data 

1836 if message == "spectral_update": 

1837 ( 

1838 frames, 

1839 _, 

1840 _, 

1841 self.last_frf, 

1842 self.last_coherence, 

1843 last_response_cpsd, 

1844 last_reference_cpsd, 

1845 self.last_condition, 

1846 ) = data 

1847 self.run_widget.channel_display_area.last_frf = self.last_frf 

1848 self.run_widget.channel_display_area.last_coh = self.last_coherence.T 

1849 if last_response_cpsd.ndim == 3: 

1850 self.last_response_cpsd = np.einsum("fii->fi", last_response_cpsd) 

1851 else: 

1852 self.last_response_cpsd = last_response_cpsd 

1853 if last_reference_cpsd.ndim == 3: 

1854 self.last_reference_cpsd = np.einsum("fii->fi", last_reference_cpsd) 

1855 else: 

1856 self.last_reference_cpsd = last_reference_cpsd 

1857 # Assemble autospectrum 

1858 self.run_widget.channel_display_area.last_autospectrum = np.zeros( 

1859 ( 

1860 len(self.data_acquisition_parameters.channel_list), 

1861 self.last_response_cpsd.shape[0], 

1862 ) 

1863 ) 

1864 for i, index in enumerate(self.environment_parameters.reference_channel_indices): 

1865 self.run_widget.channel_display_area.last_autospectrum[index, :] = ( 

1866 self.last_reference_cpsd[:, i].real 

1867 ) 

1868 for i, index in enumerate(self.environment_parameters.response_channel_indices): 

1869 self.run_widget.channel_display_area.last_autospectrum[index, :] = ( 

1870 self.last_response_cpsd[:, i].real 

1871 ) 

1872 self.run_widget.current_average_display.setValue(frames) 

1873 for window in self.run_widget.channel_display_area.subWindowList(): 

1874 widget = window.widget() 

1875 if widget.signal_selector.currentIndex() in [3, 4, 5, 6, 7]: 

1876 widget.update_data() 

1877 if self.acquiring and self.netcdf_handle is not None: 

1878 group = self.netcdf_handle.groups[self.environment_name] 

1879 group.variables["frf_data_real"][:] = np.real(self.last_frf) 

1880 group.variables["frf_data_imag"][:] = np.imag(self.last_frf) 

1881 group.variables["coherence"][:] = self.last_coherence 

1882 if self.acquiring and frames >= self.environment_parameters.num_averages: 

1883 # print('Stopping Control') 

1884 self.stop_control() 

1885 self.acquiring = False 

1886 # else: 

1887 # print('Continuing Control') 

1888 elif message == "time_frame": 

1889 frame, accepted = data 

1890 self.run_widget.channel_display_area.last_frame = frame 

1891 self.run_widget.channel_display_area.last_spectrum = np.abs(np.fft.rfft(frame, axis=-1)) 

1892 for window in self.run_widget.channel_display_area.subWindowList(): 

1893 widget = window.widget() 

1894 if widget.signal_selector.currentIndex() not in [3, 4, 5, 6, 7]: 

1895 widget.update_data() 

1896 if self.netcdf_handle is not None and accepted: 

1897 # Get current timestep 

1898 num_timesteps = self.netcdf_handle.dimensions["time_samples"].size 

1899 current_frame = num_timesteps // self.environment_parameters.samples_per_frame 

1900 if current_frame < self.environment_parameters.num_averages: 

1901 timesteps = slice(num_timesteps, None, None) 

1902 self.netcdf_handle.variables["time_data"][:, timesteps] = frame 

1903 if self.environment_parameters.accept_type == "Manual" and not accepted: 

1904 self.run_widget.accept_average_button.setEnabled(True) 

1905 self.run_widget.reject_average_button.setEnabled(True) 

1906 

1907 elif message == "finished": 

1908 self.run_widget.stop_test_button.setEnabled(False) 

1909 self.run_widget.preview_test_button.setEnabled(True) 

1910 self.run_widget.start_test_button.setEnabled(True) 

1911 self.run_widget.select_file_button.setEnabled(True) 

1912 self.run_widget.dof_override_table.setEnabled(True) 

1913 self.run_widget.add_override_button.setEnabled(True) 

1914 self.run_widget.remove_override_button.setEnabled(True) 

1915 if self.netcdf_handle is not None: 

1916 self.netcdf_handle.close() 

1917 self.netcdf_handle = None 

1918 

1919 elif message == "enable": 

1920 widget = None 

1921 for parent in [ 

1922 self.definition_widget, 

1923 self.run_widget, 

1924 ]: 

1925 try: 

1926 widget = getattr(parent, data) 

1927 break 

1928 except AttributeError: 

1929 continue 

1930 if widget is None: 

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

1932 widget.setEnabled(True) 

1933 elif message == "disable": 

1934 widget = None 

1935 for parent in [ 

1936 self.definition_widget, 

1937 self.run_widget, 

1938 ]: 

1939 try: 

1940 widget = getattr(parent, data) 

1941 break 

1942 except AttributeError: 

1943 continue 

1944 if widget is None: 

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

1946 widget.setEnabled(False) 

1947 else: 

1948 widget = None 

1949 for parent in [self.definition_widget, self.run_widget]: 

1950 try: 

1951 widget = getattr(parent, message) 

1952 break 

1953 except AttributeError: 

1954 continue 

1955 if widget is None: 

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

1957 if isinstance(widget, QtWidgets.QDoubleSpinBox): 

1958 widget.setValue(data) 

1959 elif isinstance(widget, QtWidgets.QSpinBox): 

1960 widget.setValue(data) 

1961 elif isinstance(widget, QtWidgets.QLineEdit): 

1962 widget.setText(data) 

1963 elif isinstance(widget, QtWidgets.QListWidget): 

1964 widget.clear() 

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

1966 

1967 @staticmethod 

1968 def create_environment_template( 

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

1970 ): 

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

1972 environment. 

1973 

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

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

1976 environment. 

1977 

1978 This function is the "write" counterpart to the 

1979 ``set_parameters_from_template`` function in the ``ModalUI`` class, 

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

1981 interface. 

1982 

1983 Parameters 

1984 ---------- 

1985 environment_name : str : 

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

1987 workbook : openpyxl.workbook.workbook.Workbook : 

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

1989 

1990 """ 

1991 worksheet = workbook.create_sheet(environment_name) 

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

1993 worksheet.cell(1, 2, "Modal") 

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

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

1996 worksheet.cell(3, 1, "Averaging Type:") 

1997 worksheet.cell(3, 2, "# Averaging Type") 

1998 worksheet.cell(4, 1, "Number of Averages:") 

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

2000 worksheet.cell(5, 1, "Averaging Coefficient:") 

2001 worksheet.cell(5, 2, "# Averaging Coefficient for Exponential Averaging") 

2002 worksheet.cell(6, 1, "FRF Technique:") 

2003 worksheet.cell(6, 2, "# FRF Technique") 

2004 worksheet.cell(7, 1, "FRF Window:") 

2005 worksheet.cell(7, 2, "# Window used to compute FRF") 

2006 worksheet.cell(8, 1, "Exponential Window End Value:") 

2007 worksheet.cell( 

2008 8, 

2009 2, 

2010 "# Exponential Window Value at the end of the measurement frame (0.5 or 50%, not 50)", 

2011 ) 

2012 worksheet.cell(9, 1, "FRF Overlap:") 

2013 worksheet.cell(9, 2, "# Overlap for FRF calculations (0.5 or 50%, not 50)") 

2014 worksheet.cell(10, 1, "Triggering Type:") 

2015 worksheet.cell(10, 2, '# One of "Free Run", "First Frame", or "Every Frame"') 

2016 worksheet.cell(11, 1, "Average Acceptance:") 

2017 worksheet.cell(11, 2, '# One of "Accept All", "Manual", or "Autoreject"') 

2018 worksheet.cell(12, 1, "Trigger Channel") 

2019 worksheet.cell(12, 2, "# Channel number (1-based) to use for triggering") 

2020 worksheet.cell(13, 1, "Pretrigger") 

2021 worksheet.cell(13, 2, "# Amount of frame to use as pretrigger (0.5 or 50%, not 50)") 

2022 worksheet.cell(14, 1, "Trigger Slope") 

2023 worksheet.cell(14, 2, '# One of "Positive" or "Negative"') 

2024 worksheet.cell(15, 1, "Trigger Level") 

2025 worksheet.cell( 

2026 15, 

2027 2, 

2028 "# Level to use to trigger the test as a fraction of the total range of the channel " 

2029 "(0.5 or 50%, not 50)", 

2030 ) 

2031 worksheet.cell(16, 1, "Hysteresis Level") 

2032 worksheet.cell( 

2033 16, 

2034 2, 

2035 "# Level that a channel must fall below before another trigger can be considered " 

2036 "(0.5 or 50%, not 50)", 

2037 ) 

2038 worksheet.cell(17, 1, "Hysteresis Frame Fraction") 

2039 worksheet.cell( 

2040 17, 

2041 2, 

2042 "# Fraction of the frame that a channel maintain hysteresis condition before another " 

2043 "trigger can be considered (0.5 or 50%, not 50)", 

2044 ) 

2045 worksheet.cell(18, 1, "Signal Generator Type") 

2046 worksheet.cell( 

2047 18, 

2048 2, 

2049 '# One of "None", "Random", "Burst Random", "Pseudorandom", "Chirp", "Square", or ' 

2050 '"Sine"', 

2051 ) 

2052 worksheet.cell(19, 1, "Signal Generator Level") 

2053 worksheet.cell( 

2054 19, 

2055 2, 

2056 "# RMS voltage level for random signals, Peak voltage level for chirp, sine, and " 

2057 "square pulse", 

2058 ) 

2059 worksheet.cell(20, 1, "Signal Generator Frequency 1") 

2060 worksheet.cell( 

2061 20, 

2062 2, 

2063 "# Minimum frequency for broadband signals or frequency for sine and square pulse", 

2064 ) 

2065 worksheet.cell(21, 1, "Signal Generator Frequency 2") 

2066 worksheet.cell( 

2067 21, 

2068 2, 

2069 "# Maximum frequency for broadband signals. Ignored for sine and square pulse", 

2070 ) 

2071 worksheet.cell(22, 1, "Signal Generator On Fraction") 

2072 worksheet.cell( 

2073 22, 

2074 2, 

2075 "# Fraction of time that the burst or square wave is on (0.5 or 50%, not 50)", 

2076 ) 

2077 worksheet.cell(23, 1, "Wait Time for Steady State") 

2078 worksheet.cell( 

2079 23, 

2080 2, 

2081 "# Time to wait after output starts to allow the system to reach steady state", 

2082 ) 

2083 worksheet.cell(24, 1, "Autoaccept Script") 

2084 worksheet.cell(24, 2, "# File in which an autoacceptance function is defined") 

2085 worksheet.cell(25, 1, "Autoaccept Function") 

2086 worksheet.cell(25, 2, "# Function name in which the autoacceptance function is defined") 

2087 worksheet.cell(26, 1, "Reference Channels") 

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

2089 worksheet.cell(27, 1, "Disabled Channels") 

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

2091 

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

2093 """ 

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

2095 

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

2097 environment. Cells on this worksheet contain parameters needed to 

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

2099 update the UI widgets with those parameters. 

2100 

2101 This function is the "read" counterpart to the 

2102 ``create_environment_template`` function in the ``ModalUI`` class, 

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

2104 

2105 

2106 Parameters 

2107 ---------- 

2108 worksheet : openpyxl.worksheet.worksheet.Worksheet 

2109 An openpyxl worksheet that contains the environment template. 

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

2111 user interface. 

2112 

2113 """ 

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

2115 self.definition_widget.system_id_averaging_scheme_selector.setCurrentIndex( 

2116 self.definition_widget.system_id_averaging_scheme_selector.findText( 

2117 worksheet.cell(3, 2).value 

2118 ) 

2119 ) 

2120 self.definition_widget.system_id_frames_to_average_selector.setValue( 

2121 worksheet.cell(4, 2).value 

2122 ) 

2123 self.definition_widget.system_id_averaging_coefficient_selector.setValue( 

2124 worksheet.cell(5, 2).value 

2125 ) 

2126 self.definition_widget.system_id_frf_technique_selector.setCurrentIndex( 

2127 self.definition_widget.system_id_frf_technique_selector.findText( 

2128 worksheet.cell(6, 2).value 

2129 ) 

2130 ) 

2131 self.definition_widget.system_id_transfer_function_computation_window_selector.setCurrentIndex( 

2132 self.definition_widget.system_id_transfer_function_computation_window_selector.findText( 

2133 worksheet.cell(7, 2).value 

2134 ) 

2135 ) 

2136 self.definition_widget.window_value_selector.setValue(worksheet.cell(8, 2).value * 100) 

2137 self.definition_widget.system_id_overlap_percentage_selector.setValue( 

2138 worksheet.cell(9, 2).value * 100 

2139 ) 

2140 self.definition_widget.triggering_type_selector.setCurrentIndex( 

2141 self.definition_widget.triggering_type_selector.findText(worksheet.cell(10, 2).value) 

2142 ) 

2143 acceptance = worksheet.cell(11, 2).value 

2144 self.definition_widget.acceptance_selector.blockSignals(True) 

2145 if acceptance == "Autoreject": 

2146 self.definition_widget.acceptance_selector.setCurrentIndex(2) 

2147 self.acceptance_function = [ 

2148 worksheet.cell(24, 2).value, 

2149 worksheet.cell(25, 2).value, 

2150 ] 

2151 else: 

2152 self.definition_widget.acceptance_selector.setCurrentIndex( 

2153 self.definition_widget.acceptance_selector.findText(acceptance) 

2154 ) 

2155 self.acceptance_function = None 

2156 self.definition_widget.acceptance_selector.blockSignals(False) 

2157 self.definition_widget.trigger_channel_selector.setCurrentIndex( 

2158 worksheet.cell(12, 2).value - 1 

2159 ) 

2160 self.definition_widget.pretrigger_selector.setValue(worksheet.cell(13, 2).value * 100) 

2161 self.definition_widget.trigger_slope_selector.setCurrentIndex( 

2162 self.definition_widget.trigger_slope_selector.findText(worksheet.cell(14, 2).value) 

2163 ) 

2164 self.definition_widget.trigger_level_selector.setValue(worksheet.cell(15, 2).value * 100) 

2165 self.definition_widget.hysteresis_selector.setValue(worksheet.cell(16, 2).value * 100) 

2166 self.definition_widget.hysteresis_length_selector.setValue( 

2167 worksheet.cell(17, 2).value * 100 

2168 ) 

2169 signal_index = [ 

2170 "None", 

2171 "Random", 

2172 "Burst Random", 

2173 "Pseudorandom", 

2174 "Chirp", 

2175 "Square", 

2176 "Sine", 

2177 ].index(worksheet.cell(18, 2).value) 

2178 self.definition_widget.signal_generator_selector.setCurrentIndex(signal_index) 

2179 level = worksheet.cell(19, 2).value 

2180 freq_1 = worksheet.cell(20, 2).value 

2181 freq_2 = worksheet.cell(21, 2).value 

2182 sig_on = worksheet.cell(22, 2).value * 100 

2183 for widget in [ 

2184 self.definition_widget.random_rms_selector, 

2185 self.definition_widget.burst_rms_selector, 

2186 self.definition_widget.pseudorandom_rms_selector, 

2187 self.definition_widget.chirp_level_selector, 

2188 self.definition_widget.square_level_selector, 

2189 self.definition_widget.sine_level_selector, 

2190 ]: 

2191 widget.setValue(level) 

2192 for widget in [ 

2193 self.definition_widget.random_min_frequency_selector, 

2194 self.definition_widget.burst_min_frequency_selector, 

2195 self.definition_widget.pseudorandom_min_frequency_selector, 

2196 self.definition_widget.chirp_min_frequency_selector, 

2197 self.definition_widget.square_frequency_selector, 

2198 self.definition_widget.sine_frequency_selector, 

2199 ]: 

2200 widget.setValue(freq_1) 

2201 for widget in [ 

2202 self.definition_widget.random_max_frequency_selector, 

2203 self.definition_widget.burst_max_frequency_selector, 

2204 self.definition_widget.pseudorandom_max_frequency_selector, 

2205 self.definition_widget.chirp_max_frequency_selector, 

2206 ]: 

2207 widget.setValue(freq_2) 

2208 for widget in [ 

2209 self.definition_widget.burst_on_percentage_selector, 

2210 self.definition_widget.square_percent_on_selector, 

2211 ]: 

2212 widget.setValue(sig_on) 

2213 self.definition_widget.wait_for_steady_selector.setValue(worksheet.cell(23, 2).value) 

2214 column_index = 2 

2215 while True: 

2216 value = worksheet.cell(26, column_index).value 

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

2218 break 

2219 widget = self.definition_widget.reference_channels_selector.cellWidget( 

2220 int(value) - 1, 1 

2221 ) 

2222 widget.setChecked(True) 

2223 column_index += 1 

2224 for i in range(self.definition_widget.reference_channels_selector.rowCount()): 

2225 widget = self.definition_widget.reference_channels_selector.cellWidget(int(i), 0) 

2226 widget.setChecked(True) 

2227 column_index = 2 

2228 while True: 

2229 value = worksheet.cell(27, column_index).value 

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

2231 break 

2232 widget = self.definition_widget.reference_channels_selector.cellWidget( 

2233 int(value) - 1, 0 

2234 ) 

2235 widget.setChecked(False) 

2236 column_index += 1 

2237 

2238 

2239class ModalEnvironment(AbstractEnvironment): 

2240 """Modal Environment class defining the interface with the controller""" 

2241 

2242 def __init__( 

2243 self, 

2244 environment_name: str, 

2245 queues: ModalQueues, 

2246 acquisition_active: mp.sharedctypes.Synchronized, 

2247 output_active: mp.sharedctypes.Synchronized, 

2248 ): 

2249 super().__init__( 

2250 environment_name, 

2251 queues.environment_command_queue, 

2252 queues.gui_update_queue, 

2253 queues.controller_communication_queue, 

2254 queues.log_file_queue, 

2255 queues.data_in_queue, 

2256 queues.data_out_queue, 

2257 acquisition_active, 

2258 output_active, 

2259 ) 

2260 self.queue_container = queues 

2261 self.data_acquisition_parameters = None 

2262 self.environment_parameters = None 

2263 self.frame_number = 0 

2264 self.siggen_shutdown_achieved = False 

2265 self.collector_shutdown_achieved = False 

2266 self.spectral_shutdown_achieved = False 

2267 

2268 # Map commands 

2269 self.map_command(ModalCommands.ACCEPT_FRAME, self.accept_frame) 

2270 self.map_command(ModalCommands.START_CONTROL, self.start_environment) 

2271 self.map_command(ModalCommands.RUN_CONTROL, self.run_control) 

2272 self.map_command(ModalCommands.STOP_CONTROL, self.stop_environment) 

2273 self.map_command(ModalCommands.CHECK_FOR_COMPLETE_SHUTDOWN, self.check_for_shutdown) 

2274 self.map_command( 

2275 SignalGenerationCommands.SHUTDOWN_ACHIEVED, self.siggen_shutdown_achieved_fn 

2276 ) 

2277 self.map_command( 

2278 DataCollectorCommands.SHUTDOWN_ACHIEVED, self.collector_shutdown_achieved_fn 

2279 ) 

2280 self.map_command( 

2281 SpectralProcessingCommands.SHUTDOWN_ACHIEVED, 

2282 self.spectral_shutdown_achieved_fn, 

2283 ) 

2284 

2285 def initialize_data_acquisition_parameters( 

2286 self, data_acquisition_parameters: DataAcquisitionParameters 

2287 ): 

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

2289 

2290 The environment will receive the global data acquisition parameters from 

2291 the controller, and must set itself up accordingly. 

2292 

2293 Parameters 

2294 ---------- 

2295 data_acquisition_parameters : DataAcquisitionParameters : 

2296 A container containing data acquisition parameters, including 

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

2298 """ 

2299 self.data_acquisition_parameters = data_acquisition_parameters 

2300 

2301 def initialize_environment_test_parameters(self, environment_parameters: ModalMetadata): 

2302 """ 

2303 Initialize the environment parameters specific to this environment 

2304 

2305 The environment will recieve parameters defining itself from the 

2306 user interface and must set itself up accordingly. 

2307 

2308 Parameters 

2309 ---------- 

2310 environment_parameters : ModalMetadata 

2311 A container containing the parameters defining the environment 

2312 

2313 """ 

2314 self.environment_parameters = environment_parameters 

2315 

2316 # Set up the collector 

2317 self.queue_container.collector_command_queue.put( 

2318 self.environment_name, 

2319 ( 

2320 DataCollectorCommands.INITIALIZE_COLLECTOR, 

2321 self.get_data_collector_metadata(), 

2322 ), 

2323 ) 

2324 # Set up the signal generation 

2325 self.queue_container.signal_generation_command_queue.put( 

2326 self.environment_name, 

2327 ( 

2328 SignalGenerationCommands.INITIALIZE_PARAMETERS, 

2329 self.get_signal_generation_metadata(), 

2330 ), 

2331 ) 

2332 # Set up the spectral processing 

2333 self.queue_container.spectral_command_queue.put( 

2334 self.environment_name, 

2335 ( 

2336 SpectralProcessingCommands.INITIALIZE_PARAMETERS, 

2337 self.get_spectral_processing_metadata(), 

2338 ), 

2339 ) 

2340 

2341 def get_data_collector_metadata(self) -> CollectorMetadata: 

2342 """Collects metadata used to define the data collector""" 

2343 num_channels = len(self.data_acquisition_parameters.channel_list) 

2344 reference_channel_indices = self.environment_parameters.reference_channel_indices 

2345 response_channel_indices = self.environment_parameters.response_channel_indices 

2346 if self.environment_parameters.trigger_type == "Free Run": 

2347 acquisition_type = AcquisitionType.FREE_RUN 

2348 elif self.environment_parameters.trigger_type == "First Frame": 

2349 acquisition_type = AcquisitionType.TRIGGER_FIRST_FRAME 

2350 elif self.environment_parameters.trigger_type == "Every Frame": 

2351 acquisition_type = AcquisitionType.TRIGGER_EVERY_FRAME 

2352 else: 

2353 raise ValueError( 

2354 f"Invalid Acquisition Type: {self.environment_parameters.trigger_type}" 

2355 ) 

2356 if self.environment_parameters.accept_type == "Accept All": 

2357 acceptance = Acceptance.AUTOMATIC 

2358 acceptance_function = None 

2359 elif self.environment_parameters.accept_type == "Manual": 

2360 acceptance = Acceptance.MANUAL 

2361 acceptance_function = None 

2362 elif self.environment_parameters.accept_type == "Autoreject...": 

2363 acceptance = Acceptance.AUTOMATIC 

2364 acceptance_function = self.environment_parameters.acceptance_function 

2365 else: 

2366 raise ValueError(f"Invalid Acceptance Type: {self.environment_parameters.accept_type}") 

2367 overlap_fraction = self.environment_parameters.overlap 

2368 trigger_channel_index = self.environment_parameters.trigger_channel 

2369 trigger_slope = ( 

2370 TriggerSlope.POSITIVE 

2371 if self.environment_parameters.trigger_slope_positive 

2372 else TriggerSlope.NEGATIVE 

2373 ) 

2374 (_, trigger_level, _, trigger_hysteresis) = self.environment_parameters.get_trigger_levels( 

2375 self.data_acquisition_parameters.channel_list 

2376 ) 

2377 trigger_hysteresis_samples = self.environment_parameters.hysteresis_samples 

2378 pretrigger_fraction = self.environment_parameters.pretrigger 

2379 frame_size = self.environment_parameters.samples_per_frame 

2380 if self.environment_parameters.frf_window == "hann": 

2381 window = Window.HANN 

2382 elif self.environment_parameters.frf_window == "rectangle": 

2383 window = Window.RECTANGLE 

2384 elif self.environment_parameters.frf_window == "exponential": 

2385 window = Window.EXPONENTIAL 

2386 else: 

2387 raise ValueError(f"Invalid Window Type: {self.environment_parameters.frf_window}") 

2388 window_parameter = -(frame_size) / np.log( 

2389 self.environment_parameters.exponential_window_value_at_frame_end 

2390 ) 

2391 return CollectorMetadata( 

2392 num_channels, 

2393 response_channel_indices, 

2394 reference_channel_indices, 

2395 acquisition_type, 

2396 acceptance, 

2397 acceptance_function, 

2398 overlap_fraction, 

2399 trigger_channel_index, 

2400 trigger_slope, 

2401 trigger_level, 

2402 trigger_hysteresis, 

2403 trigger_hysteresis_samples, 

2404 pretrigger_fraction, 

2405 frame_size, 

2406 window, 

2407 response_transformation_matrix=None, 

2408 reference_transformation_matrix=None, 

2409 window_parameter_2=window_parameter, 

2410 ) 

2411 

2412 def get_spectral_processing_metadata(self) -> SpectralProcessingMetadata: 

2413 """Collects metadata to define the spectral processing""" 

2414 averaging_type = ( 

2415 AveragingTypes.LINEAR 

2416 if self.environment_parameters.averaging_type == "Linear" 

2417 else AveragingTypes.EXPONENTIAL 

2418 ) 

2419 averages = self.environment_parameters.num_averages 

2420 exponential_averaging_coefficient = self.environment_parameters.averaging_coefficient 

2421 if self.environment_parameters.frf_technique == "H1": 

2422 frf_estimator = Estimator.H1 

2423 elif self.environment_parameters.frf_technique == "H2": 

2424 frf_estimator = Estimator.H2 

2425 elif self.environment_parameters.frf_technique == "H3": 

2426 frf_estimator = Estimator.H3 

2427 elif self.environment_parameters.frf_technique == "Hv": 

2428 frf_estimator = Estimator.HV 

2429 else: 

2430 raise ValueError( 

2431 f"Invalid FRF Estimator {self.environment_parameters.frf_technique}. " 

2432 "How did you get here?" 

2433 ) 

2434 num_response_channels = len(self.environment_parameters.response_channel_indices) 

2435 num_reference_channels = len(self.environment_parameters.reference_channel_indices) 

2436 frequency_spacing = self.environment_parameters.frequency_spacing 

2437 sample_rate = self.environment_parameters.sample_rate 

2438 num_frequency_lines = self.environment_parameters.fft_lines 

2439 return SpectralProcessingMetadata( 

2440 averaging_type, 

2441 averages, 

2442 exponential_averaging_coefficient, 

2443 frf_estimator, 

2444 num_response_channels, 

2445 num_reference_channels, 

2446 frequency_spacing, 

2447 sample_rate, 

2448 num_frequency_lines, 

2449 compute_cpsd=False, 

2450 compute_apsd=True, 

2451 ) 

2452 

2453 def get_signal_generation_metadata(self) -> SignalGenerationMetadata: 

2454 """Collects metadata to define the signal generator""" 

2455 return SignalGenerationMetadata( 

2456 samples_per_write=self.data_acquisition_parameters.samples_per_write, 

2457 level_ramp_samples=1, 

2458 output_transformation_matrix=None, 

2459 disabled_signals=self.environment_parameters.disabled_signals, 

2460 ) 

2461 

2462 def get_signal_generator(self): 

2463 """Gets the signal generator object used to generate signals for the environment""" 

2464 return self.environment_parameters.get_signal_generator() 

2465 

2466 def start_environment(self, data): # pylint: disable=unused-argument 

2467 """Starts the environment 

2468 

2469 Parameters 

2470 ---------- 

2471 data : NoneType 

2472 Requred by the message/data data-passing strategy in Rattlesnake, but not needed by 

2473 this method 

2474 """ 

2475 self.log("Starting Modal") 

2476 self.siggen_shutdown_achieved = False 

2477 self.collector_shutdown_achieved = False 

2478 self.spectral_shutdown_achieved = False 

2479 

2480 # Set up the collector 

2481 self.queue_container.collector_command_queue.put( 

2482 self.environment_name, 

2483 ( 

2484 DataCollectorCommands.FORCE_INITIALIZE_COLLECTOR, 

2485 self.get_data_collector_metadata(), 

2486 ), 

2487 ) 

2488 

2489 self.queue_container.collector_command_queue.put( 

2490 self.environment_name, 

2491 ( 

2492 DataCollectorCommands.SET_TEST_LEVEL, 

2493 (self.environment_parameters.skip_frames, 1), 

2494 ), 

2495 ) 

2496 time.sleep(0.01) 

2497 

2498 # Set up the signal generation 

2499 self.queue_container.signal_generation_command_queue.put( 

2500 self.environment_name, 

2501 ( 

2502 SignalGenerationCommands.INITIALIZE_PARAMETERS, 

2503 self.get_signal_generation_metadata(), 

2504 ), 

2505 ) 

2506 

2507 self.queue_container.signal_generation_command_queue.put( 

2508 self.environment_name, 

2509 ( 

2510 SignalGenerationCommands.INITIALIZE_SIGNAL_GENERATOR, 

2511 self.get_signal_generator(), 

2512 ), 

2513 ) 

2514 

2515 self.queue_container.signal_generation_command_queue.put( 

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

2517 ) 

2518 

2519 self.queue_container.signal_generation_command_queue.put( 

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

2521 ) 

2522 

2523 # Tell the collector to start acquiring data 

2524 self.queue_container.collector_command_queue.put( 

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

2526 ) 

2527 

2528 # Tell the signal generation to start generating signals 

2529 self.queue_container.signal_generation_command_queue.put( 

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

2531 ) 

2532 

2533 # Set up the spectral processing 

2534 self.queue_container.spectral_command_queue.put( 

2535 self.environment_name, 

2536 ( 

2537 SpectralProcessingCommands.INITIALIZE_PARAMETERS, 

2538 self.get_spectral_processing_metadata(), 

2539 ), 

2540 ) 

2541 

2542 # Tell the spectral analysis to clear and start acquiring 

2543 self.queue_container.spectral_command_queue.put( 

2544 self.environment_name, 

2545 (SpectralProcessingCommands.CLEAR_SPECTRAL_PROCESSING, None), 

2546 ) 

2547 

2548 self.queue_container.spectral_command_queue.put( 

2549 self.environment_name, 

2550 (SpectralProcessingCommands.RUN_SPECTRAL_PROCESSING, None), 

2551 ) 

2552 

2553 self.queue_container.environment_command_queue.put( 

2554 self.environment_name, (ModalCommands.RUN_CONTROL, None) 

2555 ) 

2556 

2557 def run_control(self, data): # pylint: disable=unused-argument 

2558 """Runs the environment 

2559 

2560 Parameters 

2561 ---------- 

2562 data : NoneType 

2563 Requred by the message/data data-passing strategy in Rattlesnake, but not needed by 

2564 this method 

2565 """ 

2566 # Pull data off the spectral queue 

2567 spectral_data = flush_queue( 

2568 self.queue_container.updated_spectral_quantities_queue, timeout=WAIT_TIME 

2569 ) 

2570 if len(spectral_data) > 0: 

2571 self.log("Received Data") 

2572 ( 

2573 frames, 

2574 frequencies, 

2575 frf, 

2576 coherence, 

2577 response_cpsd, 

2578 reference_cpsd, 

2579 condition, 

2580 ) = spectral_data[-1] 

2581 self.gui_update_queue.put( 

2582 ( 

2583 self.environment_name, 

2584 ( 

2585 "spectral_update", 

2586 ( 

2587 frames, 

2588 self.environment_parameters.num_averages, 

2589 frequencies, 

2590 frf, 

2591 coherence, 

2592 response_cpsd, 

2593 reference_cpsd, 

2594 condition, 

2595 ), 

2596 ), 

2597 ) 

2598 ) 

2599 else: 

2600 time.sleep(WAIT_TIME) 

2601 self.queue_container.environment_command_queue.put( 

2602 self.environment_name, (ModalCommands.RUN_CONTROL, None) 

2603 ) 

2604 

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

2606 """Sets the signal generation shutdown flag to True 

2607 

2608 Parameters 

2609 ---------- 

2610 data : NoneType 

2611 Requred by the message/data data-passing strategy in Rattlesnake, but not needed by 

2612 this method 

2613 """ 

2614 self.siggen_shutdown_achieved = True 

2615 

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

2617 """Sets the collector shutdown flag to True 

2618 

2619 Parameters 

2620 ---------- 

2621 data : NoneType 

2622 Requred by the message/data data-passing strategy in Rattlesnake, but not needed by 

2623 this method 

2624 """ 

2625 self.collector_shutdown_achieved = True 

2626 

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

2628 """Sets the spectral processing shutdown flag to True 

2629 

2630 Parameters 

2631 ---------- 

2632 data : NoneType 

2633 Requred by the message/data data-passing strategy in Rattlesnake, but not needed by 

2634 this method 

2635 """ 

2636 self.spectral_shutdown_achieved = True 

2637 

2638 def check_for_shutdown(self, data): # pylint: disable=unused-argument 

2639 """Checks if all environment subprocesses have shut down successfully. 

2640 

2641 Parameters 

2642 ---------- 

2643 data : NoneType 

2644 Requred by the message/data data-passing strategy in Rattlesnake, but not needed by 

2645 this method 

2646 """ 

2647 if ( 

2648 self.siggen_shutdown_achieved 

2649 and self.collector_shutdown_achieved 

2650 and self.spectral_shutdown_achieved 

2651 ): 

2652 self.log("Shutdown Achieved") 

2653 self.gui_update_queue.put((self.environment_name, ("finished", None))) 

2654 else: 

2655 # Recheck some time later 

2656 time.sleep(1) 

2657 self.environment_command_queue.put( 

2658 self.environment_name, (ModalCommands.CHECK_FOR_COMPLETE_SHUTDOWN, None) 

2659 ) 

2660 

2661 def accept_frame(self, data): 

2662 """Accepts or rejects the previous measurement frame""" 

2663 self.queue_container.collector_command_queue.put( 

2664 self.environment_name, (DataCollectorCommands.ACCEPT, data) 

2665 ) 

2666 

2667 def stop_environment(self, data): 

2668 """Stop the environment gracefully 

2669 

2670 This function defines the operations to shut down the environment 

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

2672 or parts. 

2673 

2674 Parameters 

2675 ---------- 

2676 data : Ignored 

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

2678 due to the calling signature of functions called through the 

2679 ``command_map`` 

2680 

2681 """ 

2682 self.log("Stopping Control") 

2683 flush_queue(self.queue_container.environment_command_queue) 

2684 self.queue_container.collector_command_queue.put( 

2685 self.environment_name, (DataCollectorCommands.SET_TEST_LEVEL, (1000, 1)) 

2686 ) 

2687 self.queue_container.signal_generation_command_queue.put( 

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

2689 ) 

2690 self.queue_container.spectral_command_queue.put( 

2691 self.environment_name, 

2692 (SpectralProcessingCommands.STOP_SPECTRAL_PROCESSING, None), 

2693 ) 

2694 self.queue_container.environment_command_queue.put( 

2695 self.environment_name, (ModalCommands.CHECK_FOR_COMPLETE_SHUTDOWN, None) 

2696 ) 

2697 

2698 def quit(self, data): 

2699 """Returns True to stop the ``run`` while loop and exit the process 

2700 

2701 Parameters 

2702 ---------- 

2703 data : Ignored 

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

2705 due to the calling signature of functions called through the 

2706 ``command_map`` 

2707 

2708 Returns 

2709 ------- 

2710 True : 

2711 This function returns True to signal to the ``run`` while loop 

2712 that it is time to close down the environment. 

2713 

2714 """ 

2715 for queue in [ 

2716 self.queue_container.spectral_command_queue, 

2717 self.queue_container.signal_generation_command_queue, 

2718 self.queue_container.collector_command_queue, 

2719 ]: 

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

2721 return True 

2722 

2723 

2724def modal_process( 

2725 environment_name: str, 

2726 input_queue: VerboseMessageQueue, 

2727 gui_update_queue: Queue, 

2728 controller_communication_queue: VerboseMessageQueue, 

2729 log_file_queue: Queue, 

2730 data_in_queue: Queue, 

2731 data_out_queue: Queue, 

2732 acquisition_active: mp.sharedctypes.Synchronized, 

2733 output_active: mp.sharedctypes.Synchronized, 

2734): 

2735 """Modal environment process function called by multiprocessing 

2736 

2737 This function defines the Modal Environment process that 

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

2739 creates a ModalEnvironment object and runs it. 

2740 

2741 Parameters 

2742 ---------- 

2743 environment_name : str : 

2744 Name of the environment 

2745 input_queue : VerboseMessageQueue : 

2746 Queue containing instructions for the environment 

2747 gui_update_queue : Queue : 

2748 Queue where GUI updates are put 

2749 controller_communication_queue : Queue : 

2750 Queue for global communications with the controller 

2751 log_file_queue : Queue : 

2752 Queue for writing log file messages 

2753 data_in_queue : Queue : 

2754 Queue from which data will be read by the environment 

2755 data_out_queue : Queue : 

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

2757 

2758 """ 

2759 queue_container = ModalQueues( 

2760 environment_name, 

2761 input_queue, 

2762 gui_update_queue, 

2763 controller_communication_queue, 

2764 data_in_queue, 

2765 data_out_queue, 

2766 log_file_queue, 

2767 ) 

2768 

2769 spectral_proc = mp.Process( 

2770 target=spectral_processing_process, 

2771 args=( 

2772 environment_name, 

2773 queue_container.spectral_command_queue, 

2774 queue_container.data_for_spectral_computation_queue, 

2775 queue_container.updated_spectral_quantities_queue, 

2776 queue_container.environment_command_queue, 

2777 queue_container.gui_update_queue, 

2778 queue_container.log_file_queue, 

2779 ), 

2780 ) 

2781 spectral_proc.start() 

2782 siggen_proc = mp.Process( 

2783 target=signal_generation_process, 

2784 args=( 

2785 environment_name, 

2786 queue_container.signal_generation_command_queue, 

2787 queue_container.signal_generation_update_queue, 

2788 queue_container.data_out_queue, 

2789 queue_container.environment_command_queue, 

2790 queue_container.log_file_queue, 

2791 queue_container.gui_update_queue, 

2792 ), 

2793 ) 

2794 siggen_proc.start() 

2795 collection_proc = mp.Process( 

2796 target=data_collector_process, 

2797 args=( 

2798 environment_name, 

2799 queue_container.collector_command_queue, 

2800 queue_container.data_in_queue, 

2801 [queue_container.data_for_spectral_computation_queue], 

2802 queue_container.environment_command_queue, 

2803 queue_container.log_file_queue, 

2804 queue_container.gui_update_queue, 

2805 ), 

2806 ) 

2807 

2808 collection_proc.start() 

2809 

2810 process_class = ModalEnvironment( 

2811 environment_name, queue_container, acquisition_active, output_active 

2812 ) 

2813 process_class.run() 

2814 

2815 # Rejoin all the processes 

2816 process_class.log("Joining Subprocesses") 

2817 process_class.log("Joining Spectral Computation") 

2818 spectral_proc.join() 

2819 process_class.log("Joining Signal Generation") 

2820 siggen_proc.join() 

2821 process_class.log("Joining Data Collection") 

2822 collection_proc.join()