1# -*- coding: utf-8 -*-
2"""
3This file defines a Random Vibration Environment where a specification is
4defined and the controller solves for excitations that will cause the test
5article to match the specified response.
6
7This environment has a number of subprocesses, including CPSD and FRF
8computation, data analysis, and signal generation.
9
10Rattlesnake Vibration Control Software
11Copyright (C) 2021 National Technology & Engineering Solutions of Sandia, LLC
12(NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S.
13Government retains certain rights in this software.
14
15This program is free software: you can redistribute it and/or modify
16it under the terms of the GNU General Public License as published by
17the Free Software Foundation, either version 3 of the License, or
18(at your option) any later version.
19
20This program is distributed in the hope that it will be useful,
21but WITHOUT ANY WARRANTY; without even the implied warranty of
22MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23GNU General Public License for more details.
24
25You should have received a copy of the GNU General Public License
26along with this program. If not, see <https://www.gnu.org/licenses/>.
27"""
28
29import datetime
30import inspect
31import multiprocessing as mp
32import multiprocessing.sharedctypes # pylint: disable=unused-import
33import time
34from enum import Enum
35from multiprocessing.queues import Queue
36
37import netCDF4 as nc4
38import numpy as np
39import openpyxl
40from qtpy import QtWidgets, uic
41from qtpy.QtCore import Qt, QTimer # pylint: disable=no-name-in-module
42from qtpy.QtGui import QColor # pylint: disable=no-name-in-module
43
44# %% Imports
45from .abstract_sysid_environment import (
46 AbstractSysIdEnvironment,
47 AbstractSysIdMetadata,
48 AbstractSysIdUI,
49)
50from .environments import (
51 ControlTypes,
52 environment_definition_ui_paths,
53 environment_prediction_ui_paths,
54 environment_run_ui_paths,
55)
56from .random_vibration_sys_id_utilities import _direction_map, load_specification
57from .ui_utilities import PlotWindow, TransformationMatrixWindow, multiline_plotter
58from .utilities import (
59 DataAcquisitionParameters,
60 GlobalCommands,
61 VerboseMessageQueue,
62 db2scale,
63 error_message_qt,
64 load_python_module,
65)
66
67# %% Global Variables
68
69CONTROL_TYPE = ControlTypes.RANDOM
70MAXIMUM_NAME_LENGTH = 50
71
72
73# %% Commands
74class RandomVibrationCommands(Enum):
75 """Valid random vibration commands"""
76
77 ADJUST_TEST_LEVEL = 0
78 START_CONTROL = 1
79 STOP_CONTROL = 2
80 CHECK_FOR_COMPLETE_SHUTDOWN = 3
81 RECOMPUTE_PREDICTION = 4
82 # UPDATE_INTERACTIVE_CONTROL_PARAMETERS = 5
83
84
85# %% Queues
86
87
88class RandomVibrationQueues:
89 """A container class for the queues that random vibration will manage."""
90
91 def __init__(
92 self,
93 environment_name: str,
94 environment_command_queue: VerboseMessageQueue,
95 gui_update_queue: mp.queues.Queue,
96 controller_communication_queue: VerboseMessageQueue,
97 data_in_queue: mp.queues.Queue,
98 data_out_queue: mp.queues.Queue,
99 log_file_queue: VerboseMessageQueue,
100 ):
101 """A container class for the queues that random vibration will manage.
102
103 The environment uses many queues to pass data between the various pieces.
104 This class organizes those queues into one common namespace.
105
106
107 Parameters
108 ----------
109 environment_name : str
110 Name of the environment
111 environment_command_queue : VerboseMessageQueue
112 Queue that is read by the environment for environment commands
113 gui_update_queue : mp.queues.Queue
114 Queue where various subtasks put instructions for updating the
115 widgets in the user interface
116 controller_communication_queue : VerboseMessageQueue
117 Queue that is read by the controller for global controller commands
118 data_in_queue : mp.queues.Queue
119 Multiprocessing queue that connects the acquisition subtask to the
120 environment subtask. Each environment will retrieve acquired data
121 from this queue.
122 data_out_queue : mp.queues.Queue
123 Multiprocessing queue that connects the output subtask to the
124 environment subtask. Each environment will put data that it wants
125 the controller to generate in this queue.
126 log_file_queue : VerboseMessageQueue
127 Queue for putting logging messages that will be read by the logging
128 subtask and written to a file.
129 """
130 self.environment_command_queue = environment_command_queue
131 self.gui_update_queue = gui_update_queue
132 self.data_analysis_command_queue = VerboseMessageQueue(
133 log_file_queue, environment_name + " Data Analysis Command Queue"
134 )
135 self.signal_generation_command_queue = VerboseMessageQueue(
136 log_file_queue, environment_name + " Signal Generation Command Queue"
137 )
138 self.spectral_command_queue = VerboseMessageQueue(
139 log_file_queue, environment_name + " Spectral Computation Command Queue"
140 )
141 self.collector_command_queue = VerboseMessageQueue(
142 log_file_queue, environment_name + " Data Collector Command Queue"
143 )
144 self.controller_communication_queue = controller_communication_queue
145 self.data_in_queue = data_in_queue
146 self.data_out_queue = data_out_queue
147 self.data_for_spectral_computation_queue = mp.Queue()
148 self.updated_spectral_quantities_queue = mp.Queue()
149 self.cpsd_to_generate_queue = mp.Queue()
150 self.log_file_queue = log_file_queue
151
152
153# %% Metadata
154
155
156class RandomVibrationMetadata(AbstractSysIdMetadata):
157 """Container to hold the signal processing parameters of the environment"""
158
159 def __init__(
160 self,
161 number_of_channels,
162 sample_rate,
163 samples_per_frame,
164 test_level_ramp_time,
165 cola_window,
166 cola_overlap,
167 cola_window_exponent,
168 sigma_clip,
169 update_tf_during_control,
170 frames_in_cpsd,
171 cpsd_window,
172 cpsd_overlap,
173 percent_lines_out,
174 allow_automatic_aborts,
175 control_python_script,
176 control_python_function,
177 control_python_function_type,
178 control_python_function_parameters,
179 control_channel_indices,
180 output_channel_indices,
181 specification_frequency_lines,
182 specification_cpsd_matrix,
183 specification_warning_matrix,
184 specification_abort_matrix,
185 response_transformation_matrix,
186 output_transformation_matrix,
187 ):
188 super().__init__()
189 self.number_of_channels = number_of_channels
190 self.sample_rate = sample_rate
191 self.samples_per_frame = samples_per_frame
192 self.test_level_ramp_time = test_level_ramp_time
193 self.cpsd_overlap = cpsd_overlap
194 self.update_tf_during_control = update_tf_during_control
195 self.cola_window = cola_window
196 self.cola_overlap = cola_overlap
197 self.cola_window_exponent = cola_window_exponent
198 self.sigma_clip = sigma_clip
199 self.frames_in_cpsd = frames_in_cpsd
200 self.cpsd_window = cpsd_window
201 self.response_transformation_matrix = response_transformation_matrix
202 self.reference_transformation_matrix = output_transformation_matrix
203 self.control_python_script = control_python_script
204 self.control_python_function = control_python_function
205 self.control_python_function_type = control_python_function_type
206 self.control_python_function_parameters = control_python_function_parameters
207 self.control_channel_indices = control_channel_indices
208 self.output_channel_indices = output_channel_indices
209 self.specification_frequency_lines = specification_frequency_lines
210 self.specification_cpsd_matrix = specification_cpsd_matrix
211 self.specification_warning_matrix = specification_warning_matrix
212 self.specification_abort_matrix = specification_abort_matrix
213 self.percent_lines_out = percent_lines_out
214 self.allow_automatic_aborts = allow_automatic_aborts
215
216 @property
217 def sample_rate(self):
218 return self._sample_rate
219
220 @sample_rate.setter
221 def sample_rate(self, value):
222 self._sample_rate = value
223
224 @property
225 def number_of_channels(self):
226 return self._number_of_channels
227
228 @number_of_channels.setter
229 def number_of_channels(self, value):
230 self._number_of_channels = value
231
232 @property
233 def reference_channel_indices(self):
234 return self.output_channel_indices
235
236 @property
237 def response_channel_indices(self):
238 return self.control_channel_indices
239
240 @property
241 def response_transformation_matrix(self):
242 return self._response_transformation_matrix
243
244 @response_transformation_matrix.setter
245 def response_transformation_matrix(self, value):
246 self._response_transformation_matrix = value
247
248 @property
249 def reference_transformation_matrix(self):
250 return self._reference_transformation_matrix
251
252 @reference_transformation_matrix.setter
253 def reference_transformation_matrix(self, value):
254 self._reference_transformation_matrix = value
255
256 @property
257 def samples_per_acquire(self):
258 """Property returning the samples per acquisition step given the overlap"""
259 return int(self.samples_per_frame * (1 - self.cpsd_overlap))
260
261 @property
262 def frame_time(self):
263 """Property returning the time per measurement frame"""
264 return self.samples_per_frame / self.sample_rate
265
266 @property
267 def nyquist_frequency(self):
268 """Property returning half the sample rate"""
269 return self.sample_rate / 2
270
271 @property
272 def fft_lines(self):
273 """Property returning the frequency lines given the sampling parameters"""
274 return self.samples_per_frame // 2 + 1
275
276 @property
277 def frequency_spacing(self):
278 """Property returning frequency line spacing given the sampling parameters"""
279 return self.sample_rate / self.samples_per_frame
280
281 @property
282 def samples_per_output(self):
283 """Property returning the samples per output given the COLA overlap"""
284 return int(self.samples_per_frame * (1 - self.cola_overlap))
285
286 @property
287 def overlapped_output_samples(self):
288 """Property returning the number of output samples that are overlapped."""
289 return self.samples_per_frame - self.samples_per_output
290
291 @property
292 def skip_frames(self):
293 """Property returning the number of frames to skip when changing levels"""
294 return int(
295 np.ceil(
296 self.test_level_ramp_time
297 * self.sample_rate
298 / (self.samples_per_frame * (1 - self.cpsd_overlap))
299 )
300 )
301
302 def store_to_netcdf(
303 self,
304 netcdf_group_handle: nc4._netCDF4.Group, # pylint: disable=c-extension-no-member
305 ):
306 """Store parameters to a group in a netCDF streaming file.
307
308 This function stores parameters from the environment into the netCDF
309 file in a group with the environment's name as its name. The function
310 will receive a reference to the group within the dataset and should
311 store the environment's parameters into that group in the form of
312 attributes, dimensions, or variables.
313
314 This function is the "write" counterpart to the retrieve_metadata
315 function in the RandomVibrationUI class, which will read parameters from
316 the netCDF file to populate the parameters in the user interface.
317
318 Parameters
319 ----------
320 netcdf_group_handle : nc4._netCDF4.Group
321 A reference to the Group within the netCDF dataset where the
322 environment's metadata is stored.
323
324 """
325 super().store_to_netcdf(netcdf_group_handle)
326 netcdf_group_handle.samples_per_frame = self.samples_per_frame
327 netcdf_group_handle.test_level_ramp_time = self.test_level_ramp_time
328 netcdf_group_handle.cpsd_overlap = self.cpsd_overlap
329 netcdf_group_handle.update_tf_during_control = 1 if self.update_tf_during_control else 0
330 netcdf_group_handle.cola_window = self.cola_window
331 netcdf_group_handle.cola_overlap = self.cola_overlap
332 netcdf_group_handle.cola_window_exponent = self.cola_window_exponent
333 netcdf_group_handle.frames_in_cpsd = self.frames_in_cpsd
334 netcdf_group_handle.cpsd_window = self.cpsd_window
335 netcdf_group_handle.control_python_script = self.control_python_script
336 netcdf_group_handle.control_python_function = self.control_python_function
337 netcdf_group_handle.control_python_function_type = self.control_python_function_type
338 netcdf_group_handle.control_python_function_parameters = (
339 self.control_python_function_parameters
340 )
341 netcdf_group_handle.allow_automatic_aborts = 1 if self.allow_automatic_aborts else 0
342 # Specifications
343 netcdf_group_handle.createDimension("fft_lines", self.fft_lines)
344 netcdf_group_handle.createDimension("two", 2)
345 netcdf_group_handle.createDimension(
346 "specification_channels", self.specification_cpsd_matrix.shape[-1]
347 )
348 var = netcdf_group_handle.createVariable(
349 "specification_frequency_lines", "f8", ("fft_lines",)
350 )
351 var[...] = self.specification_frequency_lines
352 var = netcdf_group_handle.createVariable(
353 "specification_cpsd_matrix_real",
354 "f8",
355 ("fft_lines", "specification_channels", "specification_channels"),
356 )
357 var[...] = self.specification_cpsd_matrix.real
358 var = netcdf_group_handle.createVariable(
359 "specification_cpsd_matrix_imag",
360 "f8",
361 ("fft_lines", "specification_channels", "specification_channels"),
362 )
363 var[...] = self.specification_cpsd_matrix.imag
364 var = netcdf_group_handle.createVariable(
365 "specification_warning_matrix",
366 "f8",
367 ("two", "fft_lines", "specification_channels"),
368 )
369 var[...] = self.specification_warning_matrix.real
370 var = netcdf_group_handle.createVariable(
371 "specification_abort_matrix",
372 "f8",
373 ("two", "fft_lines", "specification_channels"),
374 )
375 var[...] = self.specification_abort_matrix.real
376 # Transformation matrices
377 if self.response_transformation_matrix is not None:
378 netcdf_group_handle.createDimension(
379 "response_transformation_rows",
380 self.response_transformation_matrix.shape[0],
381 )
382 netcdf_group_handle.createDimension(
383 "response_transformation_cols",
384 self.response_transformation_matrix.shape[1],
385 )
386 var = netcdf_group_handle.createVariable(
387 "response_transformation_matrix",
388 "f8",
389 ("response_transformation_rows", "response_transformation_cols"),
390 )
391 var[...] = self.response_transformation_matrix
392 if self.reference_transformation_matrix is not None:
393 netcdf_group_handle.createDimension(
394 "reference_transformation_rows",
395 self.reference_transformation_matrix.shape[0],
396 )
397 netcdf_group_handle.createDimension(
398 "reference_transformation_cols",
399 self.reference_transformation_matrix.shape[1],
400 )
401 var = netcdf_group_handle.createVariable(
402 "reference_transformation_matrix",
403 "f8",
404 ("reference_transformation_rows", "reference_transformation_cols"),
405 )
406 var[...] = self.reference_transformation_matrix
407 # Control channels
408 netcdf_group_handle.createDimension("control_channels", len(self.control_channel_indices))
409 var = netcdf_group_handle.createVariable(
410 "control_channel_indices", "i4", ("control_channels")
411 )
412 var[...] = self.control_channel_indices
413
414
415# %% UI
416
417from .abstract_interactive_control_law import ( # noqa: E402 pylint: disable=wrong-import-position
418 AbstractControlLawComputation,
419)
420from .data_collector import ( # noqa: E402 pylint: disable=wrong-import-position
421 Acceptance,
422 AcquisitionType,
423 CollectorMetadata,
424 DataCollectorCommands,
425 TriggerSlope,
426 Window,
427 data_collector_process,
428)
429from .random_vibration_sys_id_data_analysis import ( # noqa: E402 pylint: disable=wrong-import-position
430 RandomVibrationDataAnalysisCommands,
431 random_data_analysis_process,
432)
433from .signal_generation import ( # noqa: E402 pylint: disable=wrong-import-position
434 CPSDSignalGenerator,
435)
436from .signal_generation_process import ( # noqa: E402 pylint: disable=wrong-import-position
437 SignalGenerationCommands,
438 SignalGenerationMetadata,
439 signal_generation_process,
440)
441from .spectral_processing import ( # noqa: E402 pylint: disable=wrong-import-position
442 AveragingTypes,
443 Estimator,
444 SpectralProcessingCommands,
445 SpectralProcessingMetadata,
446 spectral_processing_process,
447)
448
449
450class RandomVibrationUI(AbstractSysIdUI):
451 """Class defining the user interface for a Random Vibration environment.
452
453 This class will contain four main UIs, the environment definition,
454 system identification, test prediction, and run. The widgets corresponding
455 to these interfaces are stored in TabWidgets in the main UI.
456
457 This class defines all the call backs and user interface operations required
458 for the Random Vibration environment."""
459
460 def __init__(
461 self,
462 environment_name: str,
463 definition_tabwidget: QtWidgets.QTabWidget,
464 system_id_tabwidget: QtWidgets.QTabWidget,
465 test_predictions_tabwidget: QtWidgets.QTabWidget,
466 run_tabwidget: QtWidgets.QTabWidget,
467 environment_command_queue: VerboseMessageQueue,
468 controller_communication_queue: VerboseMessageQueue,
469 log_file_queue: Queue,
470 ):
471 """
472 Constructs a Random Vibration User Interface
473
474 Given the tab widgets from the main interface as well as communication
475 queues, this class assembles the user interface components specific to
476 the Random Vibration Environment
477
478 Parameters
479 ----------
480 definition_tabwidget : QtWidgets.QTabWidget
481 QTabWidget containing the environment subtabs on the Control
482 Definition main tab
483 system_id_tabwidget : QtWidgets.QTabWidget
484 QTabWidget containing the environment subtabs on the System
485 Identification main tab
486 test_predictions_tabwidget : QtWidgets.QTabWidget
487 QTabWidget containing the environment subtabs on the Test Predictions
488 main tab
489 run_tabwidget : QtWidgets.QTabWidget
490 QTabWidget containing the environment subtabs on the Run
491 main tab.
492 environment_command_queue : VerboseMessageQueue
493 Queue for sending commands to the Random Vibration Environment
494 controller_communication_queue : VerboseMessageQueue
495 Queue for sending global commands to the controller
496 log_file_queue : Queue
497 Queue where log file messages can be written.
498
499 """
500 super().__init__(
501 environment_name,
502 environment_command_queue,
503 controller_communication_queue,
504 log_file_queue,
505 system_id_tabwidget,
506 )
507 # Add the page to the control definition tabwidget
508 self.definition_widget = QtWidgets.QWidget()
509 uic.loadUi(environment_definition_ui_paths[CONTROL_TYPE], self.definition_widget)
510 definition_tabwidget.addTab(self.definition_widget, self.environment_name)
511 # Add the page to the control prediction tabwidget
512 self.prediction_widget = QtWidgets.QWidget()
513 uic.loadUi(environment_prediction_ui_paths[CONTROL_TYPE], self.prediction_widget)
514 test_predictions_tabwidget.addTab(self.prediction_widget, self.environment_name)
515 # Add the page to the run tabwidget
516 self.run_widget = QtWidgets.QWidget()
517 uic.loadUi(environment_run_ui_paths[CONTROL_TYPE], self.run_widget)
518 run_tabwidget.addTab(self.run_widget, self.environment_name)
519
520 self.plot_data_items = {}
521 self.plot_windows = []
522 self.run_start_time = None
523 self.run_level_start_time = None
524 self.run_timer = QTimer()
525 self.response_transformation_matrix = None
526 self.output_transformation_matrix = None
527 self.python_control_module = None
528 self.specification_frequency_lines = None
529 self.specification_cpsd_matrix = None
530 self.specification_warning_matrix = None
531 self.specification_abort_matrix = None
532 self.physical_channel_names = None
533 self.physical_output_indices = None
534 self.excitation_prediction = None
535 self.response_prediction = None
536 self.rms_voltage_prediction = None
537 self.rms_db_error_prediction = None
538 self.interactive_control_law_widget = None
539 self.interactive_control_law_window = None
540 self.control_selector_widgets = [
541 self.definition_widget.specification_row_selector,
542 self.definition_widget.specification_column_selector,
543 self.prediction_widget.response_row_selector,
544 self.prediction_widget.response_column_selector,
545 self.run_widget.control_channel_1_selector,
546 self.run_widget.control_channel_2_selector,
547 ]
548 self.output_selector_widgets = [
549 self.prediction_widget.excitation_row_selector,
550 self.prediction_widget.excitation_column_selector,
551 ]
552 self.system_id_widget.samplesPerFrameSpinBox.setReadOnly(True)
553 self.system_id_widget.samplesPerFrameSpinBox.setButtonSymbols(
554 QtWidgets.QAbstractSpinBox.ButtonSymbols.NoButtons
555 )
556 self.system_id_widget.levelRampTimeDoubleSpinBox.setReadOnly(True)
557 self.system_id_widget.levelRampTimeDoubleSpinBox.setButtonSymbols(
558 QtWidgets.QAbstractSpinBox.ButtonSymbols.NoButtons
559 )
560
561 # Set common look and feel for plots
562 plot_widgets = [
563 self.definition_widget.specification_single_plot,
564 self.definition_widget.specification_sum_asds_plot,
565 self.prediction_widget.excitation_display_plot,
566 self.prediction_widget.response_display_plot,
567 self.run_widget.global_test_performance_plot,
568 ]
569 for plot_widget in plot_widgets:
570 plot_item = plot_widget.getPlotItem()
571 plot_item.showGrid(True, True, 0.25)
572 plot_item.enableAutoRange()
573 plot_item.getViewBox().enableAutoRange(enable=True)
574 logscale_plot_widgets = [
575 self.definition_widget.specification_single_plot,
576 self.definition_widget.specification_sum_asds_plot,
577 self.prediction_widget.excitation_display_plot,
578 self.prediction_widget.response_display_plot,
579 self.run_widget.global_test_performance_plot,
580 ]
581 for plot_widget in logscale_plot_widgets:
582 plot_item = plot_widget.getPlotItem()
583 plot_item.setLogMode(False, True)
584
585 self.connect_callbacks()
586
587 # Complete the profile commands
588 self.command_map["Set Test Level"] = self.change_test_level_from_profile
589 self.command_map["Change Specification"] = self.change_specification_from_profile
590 self.command_map["Save Control Data"] = self.save_control_data_from_profile
591
592 def connect_callbacks(self):
593 """Connects callback functions to the UI Widgets"""
594 # Definition
595 self.definition_widget.samples_per_frame_selector.valueChanged.connect(
596 self.update_parameters_and_clear_spec
597 )
598 self.definition_widget.cpsd_overlap_selector.valueChanged.connect(self.update_parameters)
599 self.definition_widget.cola_overlap_percentage_selector.valueChanged.connect(
600 self.update_parameters
601 )
602 self.definition_widget.transformation_matrices_button.clicked.connect(
603 self.define_transformation_matrices
604 )
605 self.definition_widget.control_script_load_file_button.clicked.connect(
606 self.select_python_module
607 )
608 self.definition_widget.control_function_input.currentIndexChanged.connect(
609 self.update_generator_selector
610 )
611 self.definition_widget.load_spec_button.clicked.connect(self.select_spec_file)
612 self.definition_widget.specification_row_selector.currentIndexChanged.connect(
613 self.show_specification
614 )
615 self.definition_widget.specification_column_selector.currentIndexChanged.connect(
616 self.show_specification
617 )
618 self.definition_widget.control_channels_selector.itemChanged.connect(
619 self.update_control_channels
620 )
621 self.definition_widget.check_selected_button.clicked.connect(
622 self.check_selected_control_channels
623 )
624 self.definition_widget.uncheck_selected_button.clicked.connect(
625 self.uncheck_selected_control_channels
626 )
627 # Prediction
628 self.prediction_widget.excitation_row_selector.currentIndexChanged.connect(
629 self.update_control_predictions
630 )
631 self.prediction_widget.excitation_column_selector.currentIndexChanged.connect(
632 self.update_control_predictions
633 )
634 self.prediction_widget.response_row_selector.currentIndexChanged.connect(
635 self.update_control_predictions
636 )
637 self.prediction_widget.response_column_selector.currentIndexChanged.connect(
638 self.update_control_predictions
639 )
640 self.prediction_widget.maximum_voltage_button.clicked.connect(
641 self.show_max_voltage_prediction
642 )
643 self.prediction_widget.minimum_voltage_button.clicked.connect(
644 self.show_min_voltage_prediction
645 )
646 self.prediction_widget.maximum_error_button.clicked.connect(self.show_max_error_prediction)
647 self.prediction_widget.minimum_error_button.clicked.connect(self.show_min_error_prediction)
648 self.prediction_widget.response_error_list.itemClicked.connect(
649 self.update_response_error_prediction_selector
650 )
651 self.prediction_widget.excitation_voltage_list.itemClicked.connect(
652 self.update_excitation_prediction_selector
653 )
654 self.prediction_widget.recompute_prediction_button.clicked.connect(
655 self.recompute_prediction
656 )
657 # Run Test
658 self.run_widget.current_test_level_selector.valueChanged.connect(
659 self.change_control_test_level
660 )
661 self.run_widget.start_test_button.clicked.connect(self.start_control)
662 self.run_widget.stop_test_button.clicked.connect(self.stop_control)
663 self.run_widget.create_window_button.clicked.connect(self.create_window)
664 self.run_widget.show_all_asds_button.clicked.connect(self.show_all_asds)
665 self.run_widget.show_all_csds_phscoh_button.clicked.connect(self.show_all_csds_phscoh)
666 self.run_widget.show_all_csds_realimag_button.clicked.connect(self.show_all_csds_realimag)
667 self.run_widget.tile_windows_button.clicked.connect(self.tile_windows)
668 self.run_widget.close_windows_button.clicked.connect(self.close_windows)
669 self.run_timer.timeout.connect(self.update_run_time)
670 self.run_widget.test_response_error_list.itemDoubleClicked.connect(
671 self.show_magnitude_window
672 )
673 self.run_widget.save_current_spectral_data_button.clicked.connect(self.save_spectral_data)
674
675 # %% Initialize Data Aquisition
676
677 def initialize_data_acquisition(self, data_acquisition_parameters: DataAcquisitionParameters):
678 """Update the user interface with data acquisition parameters
679
680 This function is called when the Data Acquisition parameters are
681 initialized. This function should set up the environment user interface
682 accordingly.
683
684 Parameters
685 ----------
686 data_acquisition_parameters : DataAcquisitionParameters :
687 Container containing the data acquisition parameters, including
688 channel table and sampling information.
689
690 """
691 super().initialize_data_acquisition(data_acquisition_parameters)
692 # Initialize the plots
693 # Clear plots if there is anything on them
694 self.definition_widget.specification_single_plot.getPlotItem().clear()
695 self.definition_widget.specification_sum_asds_plot.getPlotItem().clear()
696 self.run_widget.global_test_performance_plot.getPlotItem().clear()
697
698 # Now add initial lines that we can update later
699 self.definition_widget.specification_single_plot.getPlotItem().addLegend()
700 self.plot_data_items[
701 "specification_real"
702 ] = self.definition_widget.specification_single_plot.getPlotItem().plot(
703 np.array([0, data_acquisition_parameters.sample_rate / 2]),
704 np.zeros(2),
705 pen={"color": "b", "width": 1},
706 name="Real Part",
707 )
708 self.plot_data_items[
709 "specification_imag"
710 ] = self.definition_widget.specification_single_plot.getPlotItem().plot(
711 np.array([0, data_acquisition_parameters.sample_rate / 2]),
712 np.zeros(2),
713 pen={"color": "r", "width": 1},
714 name="Imaginary Part",
715 )
716 self.plot_data_items[
717 "specification_warning_upper"
718 ] = self.definition_widget.specification_single_plot.getPlotItem().plot(
719 np.array([0, data_acquisition_parameters.sample_rate / 2]),
720 np.zeros(2),
721 pen={"color": PlotWindow.WARNING_COLOR, "width": 0.25},
722 name="Warning",
723 )
724 self.plot_data_items[
725 "specification_warning_lower"
726 ] = self.definition_widget.specification_single_plot.getPlotItem().plot(
727 np.array([0, data_acquisition_parameters.sample_rate / 2]),
728 np.zeros(2),
729 pen={"color": PlotWindow.WARNING_COLOR, "width": 0.25},
730 )
731 self.plot_data_items[
732 "specification_abort_upper"
733 ] = self.definition_widget.specification_single_plot.getPlotItem().plot(
734 np.array([0, data_acquisition_parameters.sample_rate / 2]),
735 np.zeros(2),
736 pen={"color": PlotWindow.ABORT_COLOR, "width": 0.25},
737 name="Abort",
738 )
739 self.plot_data_items[
740 "specification_abort_lower"
741 ] = self.definition_widget.specification_single_plot.getPlotItem().plot(
742 np.array([0, data_acquisition_parameters.sample_rate / 2]),
743 np.zeros(2),
744 pen={"color": PlotWindow.ABORT_COLOR, "width": 0.25},
745 )
746 self.plot_data_items[
747 "specification_sum"
748 ] = self.definition_widget.specification_sum_asds_plot.getPlotItem().plot(
749 np.array([0, data_acquisition_parameters.sample_rate / 2]),
750 np.zeros(2),
751 pen={"color": "b", "width": 1},
752 )
753 self.run_widget.global_test_performance_plot.getPlotItem().addLegend()
754 self.plot_data_items[
755 "specification_sum_control"
756 ] = self.run_widget.global_test_performance_plot.getPlotItem().plot(
757 np.array([0, data_acquisition_parameters.sample_rate / 2]),
758 np.zeros(2),
759 pen={"color": "b", "width": 1},
760 name="Specification",
761 )
762 self.plot_data_items[
763 "sum_asds_control"
764 ] = self.run_widget.global_test_performance_plot.getPlotItem().plot(
765 np.array([0, data_acquisition_parameters.sample_rate / 2]),
766 np.zeros(2),
767 pen={"color": "r", "width": 1},
768 name="Response",
769 )
770
771 # Set up channel names
772 self.physical_channel_names = [
773 (
774 f"{'' if channel.channel_type is None else channel.channel_type} "
775 f"{channel.node_number} "
776 f"{'' if channel.node_direction is None else channel.node_direction}"
777 )[:MAXIMUM_NAME_LENGTH]
778 for channel in data_acquisition_parameters.channel_list
779 ]
780 self.physical_output_indices = [
781 i
782 for i, channel in enumerate(data_acquisition_parameters.channel_list)
783 if channel.feedback_device
784 ]
785 # Set up widgets
786 self.definition_widget.sample_rate_display.setValue(data_acquisition_parameters.sample_rate)
787 self.definition_widget.samples_per_frame_selector.setValue(
788 data_acquisition_parameters.sample_rate
789 )
790 self.definition_widget.control_channels_selector.clear()
791 for channel_name in self.physical_channel_names:
792 item = QtWidgets.QListWidgetItem()
793 item.setText(channel_name)
794 item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
795 item.setCheckState(Qt.Unchecked)
796 self.definition_widget.control_channels_selector.addItem(item)
797 self.definition_widget.input_channels_display.setValue(len(self.physical_channel_names))
798 self.definition_widget.output_channels_display.setValue(len(self.physical_output_indices))
799 self.definition_widget.control_channels_display.setValue(0)
800 self.response_transformation_matrix = None
801 self.output_transformation_matrix = None
802 self.define_transformation_matrices(None, False)
803
804 @property
805 def physical_output_names(self):
806 """Names of the physical output channels"""
807 return [self.physical_channel_names[i] for i in self.physical_output_indices]
808
809 # %% Define Environments
810
811 @property
812 def physical_control_indices(self):
813 """Indices corresponding to the physical channels that are used as outputs"""
814 return [
815 i
816 for i in range(self.definition_widget.control_channels_selector.count())
817 if self.definition_widget.control_channels_selector.item(i).checkState() == Qt.Checked
818 ]
819
820 @property
821 def physical_control_names(self):
822 """Names of the physical control channels"""
823 return [self.physical_channel_names[i] for i in self.physical_control_indices]
824
825 @property
826 def initialized_control_names(self):
827 if self.environment_parameters.response_transformation_matrix is None:
828 return [
829 self.physical_channel_names[i]
830 for i in self.environment_parameters.control_channel_indices
831 ]
832 else:
833 return [
834 f"Transformed Response {i + 1}"
835 for i in range(self.environment_parameters.response_transformation_matrix.shape[0])
836 ]
837
838 @property
839 def initialized_output_names(self):
840 if self.environment_parameters.reference_transformation_matrix is None:
841 return self.physical_output_names
842 else:
843 return [
844 f"Transformed Drive {i + 1}"
845 for i in range(self.environment_parameters.reference_transformation_matrix.shape[0])
846 ]
847
848 def select_spec_file(self, clicked, filename=None): # pylint: disable=unused-argument
849 """Loads a specification using a dialog or the specified filename
850
851 Parameters
852 ----------
853 clicked :
854 The clicked event that triggered the callback.
855 filename :
856 File name defining the specification for bypassing the callback when
857 loading from a file (Default value = None).
858
859 """
860 if filename is None:
861 filename, _ = QtWidgets.QFileDialog.getOpenFileName(
862 self.definition_widget,
863 "Select Specification File",
864 filter="Numpyz or Mat (*.npz *.mat)",
865 )
866 if filename == "":
867 return
868 self.definition_widget.specification_file_name_display.setText(filename)
869 coord_dtype = np.dtype([("node", "<u8"), ("direction", "i1")])
870 if self.response_transformation_matrix is not None:
871 control_coordinate = None
872 else:
873 control_coordinate = np.array(
874 [
875 (
876 self.data_acquisition_parameters.channel_list[i].node_number,
877 _direction_map[
878 self.data_acquisition_parameters.channel_list[i].node_direction
879 ],
880 )
881 for i in self.physical_control_indices
882 ],
883 dtype=coord_dtype,
884 )
885 try:
886 (
887 self.specification_frequency_lines,
888 self.specification_cpsd_matrix,
889 self.specification_warning_matrix,
890 self.specification_abort_matrix,
891 ) = load_specification(
892 filename,
893 self.definition_widget.fft_lines_display.value(),
894 self.definition_widget.frequency_spacing_display.value(),
895 control_coordinate,
896 )
897 except ValueError as e:
898 error_message_qt(type(e).__name__, str(e))
899 return
900
901 if np.all(np.isnan(self.specification_abort_matrix)):
902 self.definition_widget.auto_abort_checkbox.setChecked(False)
903 self.definition_widget.auto_abort_checkbox.setEnabled(False)
904 else:
905 self.definition_widget.auto_abort_checkbox.setEnabled(True)
906 self.show_specification()
907
908 def select_python_module(self, clicked, filename=None): # pylint: disable=unused-argument
909 """Loads a Python module using a dialog or the specified filename
910
911 Parameters
912 ----------
913 clicked :
914 The clicked event that triggered the callback.
915 filename :
916 File name defining the Python module for bypassing the callback when
917 loading from a file (Default value = None).
918
919 """
920 if filename is None:
921 filename, _ = QtWidgets.QFileDialog.getOpenFileName(
922 self.definition_widget,
923 "Select Python Module",
924 filter="Python Modules (*.py)",
925 )
926 if filename == "":
927 return
928 self.python_control_module = load_python_module(filename)
929 functions = [
930 function
931 for function in inspect.getmembers(self.python_control_module)
932 if (
933 inspect.isfunction(function[1])
934 and len(inspect.signature(function[1]).parameters) >= 12
935 )
936 or inspect.isgeneratorfunction(function[1])
937 or (
938 inspect.isclass(function[1])
939 and all(
940 [
941 (
942 method in function[1].__dict__
943 and not (
944 hasattr(function[1].__dict__[method], "__isabstractmethod__")
945 and function[1].__dict__[method].__isabstractmethod__
946 )
947 )
948 for method in ["system_id_update", "control"]
949 ]
950 )
951 )
952 ]
953 self.log(
954 f"Loaded module {self.python_control_module.__name__} with "
955 f"functions {[function[0] for function in functions]}"
956 )
957 self.definition_widget.control_function_input.clear()
958 self.definition_widget.control_script_file_path_input.setText(filename)
959 for function in functions:
960 self.definition_widget.control_function_input.addItem(function[0])
961
962 def update_generator_selector(self):
963 """Updates the function/generator selector based on the function selected"""
964 if self.python_control_module is None:
965 return
966 try:
967 function = getattr(
968 self.python_control_module,
969 self.definition_widget.control_function_input.itemText(
970 self.definition_widget.control_function_input.currentIndex()
971 ),
972 )
973 except AttributeError:
974 return
975 if inspect.isgeneratorfunction(function):
976 self.definition_widget.control_function_generator_selector.setCurrentIndex(1)
977 elif inspect.isclass(function) and issubclass(function, AbstractControlLawComputation):
978 self.definition_widget.control_function_generator_selector.setCurrentIndex(3)
979 elif inspect.isclass(function):
980 self.definition_widget.control_function_generator_selector.setCurrentIndex(2)
981 else:
982 self.definition_widget.control_function_generator_selector.setCurrentIndex(0)
983
984 def show_specification(self):
985 """Show the specification on the GUI"""
986 if self.specification_cpsd_matrix is None:
987 self.plot_data_items["specification_real"].setData(
988 np.array([0, self.definition_widget.sample_rate_display.value() / 2]),
989 np.zeros(2),
990 )
991 self.plot_data_items["specification_imag"].setData(
992 np.array([0, self.definition_widget.sample_rate_display.value() / 2]),
993 np.zeros(2),
994 )
995 self.plot_data_items["specification_sum"].setData(
996 np.array([0, self.definition_widget.sample_rate_display.value() / 2]),
997 np.zeros(2),
998 )
999 self.plot_data_items["specification_warning_upper"].setData(
1000 np.array([0, self.definition_widget.sample_rate_display.value() / 2]),
1001 np.zeros(2),
1002 )
1003 self.plot_data_items["specification_warning_lower"].setData(
1004 np.array([0, self.definition_widget.sample_rate_display.value() / 2]),
1005 np.zeros(2),
1006 )
1007 self.plot_data_items["specification_abort_upper"].setData(
1008 np.array([0, self.definition_widget.sample_rate_display.value() / 2]),
1009 np.zeros(2),
1010 )
1011 self.plot_data_items["specification_abort_lower"].setData(
1012 np.array([0, self.definition_widget.sample_rate_display.value() / 2]),
1013 np.zeros(2),
1014 )
1015 # enabled_state = self.run_widget.isEnabled()
1016 # self.run_widget.setEnabled(True)
1017 self.plot_data_items["specification_sum_control"].setData(
1018 np.array([0, self.definition_widget.sample_rate_display.value() / 2]),
1019 np.zeros(2),
1020 )
1021 # self.run_widget.setEnabled(enabled_state)
1022 else:
1023 row = self.definition_widget.specification_row_selector.currentIndex()
1024 column = self.definition_widget.specification_column_selector.currentIndex()
1025 spec_real = abs(self.specification_cpsd_matrix[:, row, column].real)
1026 spec_imag = abs(self.specification_cpsd_matrix[:, row, column].imag)
1027 spec_sum = abs(
1028 np.nansum(
1029 self.specification_cpsd_matrix[
1030 :,
1031 np.arange(self.specification_cpsd_matrix.shape[-1]),
1032 np.arange(self.specification_cpsd_matrix.shape[-1]),
1033 ],
1034 axis=-1,
1035 )
1036 )
1037 self.plot_data_items["specification_real"].setData(
1038 self.specification_frequency_lines[spec_real > 0.0],
1039 spec_real[spec_real > 0.0],
1040 )
1041 self.plot_data_items["specification_imag"].setData(
1042 self.specification_frequency_lines[spec_imag > 0.0],
1043 spec_imag[spec_imag > 0.0],
1044 )
1045 if row == column:
1046 warning_upper = abs(self.specification_warning_matrix[1, :, row])
1047 warning_lower = abs(self.specification_warning_matrix[0, :, row])
1048 abort_upper = abs(self.specification_abort_matrix[1, :, row])
1049 abort_lower = abs(self.specification_abort_matrix[0, :, row])
1050 self.plot_data_items["specification_warning_upper"].setData(
1051 self.specification_frequency_lines, warning_upper
1052 )
1053 self.plot_data_items["specification_warning_lower"].setData(
1054 self.specification_frequency_lines, warning_lower
1055 )
1056 self.plot_data_items["specification_abort_upper"].setData(
1057 self.specification_frequency_lines, abort_upper
1058 )
1059 self.plot_data_items["specification_abort_lower"].setData(
1060 self.specification_frequency_lines, abort_lower
1061 )
1062 else:
1063 self.plot_data_items["specification_warning_upper"].setData(
1064 np.array([0, self.definition_widget.sample_rate_display.value() / 2]),
1065 np.zeros(2),
1066 )
1067 self.plot_data_items["specification_warning_lower"].setData(
1068 np.array([0, self.definition_widget.sample_rate_display.value() / 2]),
1069 np.zeros(2),
1070 )
1071 self.plot_data_items["specification_abort_upper"].setData(
1072 np.array([0, self.definition_widget.sample_rate_display.value() / 2]),
1073 np.zeros(2),
1074 )
1075 self.plot_data_items["specification_abort_lower"].setData(
1076 np.array([0, self.definition_widget.sample_rate_display.value() / 2]),
1077 np.zeros(2),
1078 )
1079 self.plot_data_items["specification_sum"].setData(
1080 self.specification_frequency_lines[spec_sum > 0.0],
1081 spec_sum[spec_sum > 0.0],
1082 )
1083 # enabled_state = self.run_widget.isEnabled()
1084 # self.run_widget.setEnabled(True)
1085 self.plot_data_items["specification_sum_control"].setData(
1086 self.specification_frequency_lines[spec_sum > 0.0],
1087 spec_sum[spec_sum > 0.0],
1088 )
1089 # self.run_widget.setEnabled(enabled_state)
1090
1091 def check_selected_control_channels(self):
1092 """Checks the selected channels to make them control channels"""
1093 for item in self.definition_widget.control_channels_selector.selectedItems():
1094 item.setCheckState(Qt.Checked)
1095
1096 def uncheck_selected_control_channels(self):
1097 """Unchecks the selected channels to make them no longer control channels"""
1098 for item in self.definition_widget.control_channels_selector.selectedItems():
1099 item.setCheckState(Qt.Unchecked)
1100
1101 def update_control_channels(self):
1102 """Resets the definition UI when the number of control channels has changed"""
1103 self.response_transformation_matrix = None
1104 self.output_transformation_matrix = None
1105 self.specification_abort_matrix = None
1106 self.specification_warning_matrix = None
1107 self.specification_cpsd_matrix = None
1108 self.specification_frequency_lines = None
1109 self.definition_widget.control_channels_display.setValue(len(self.physical_control_indices))
1110 self.definition_widget.specification_row_selector.blockSignals(True)
1111 self.definition_widget.specification_column_selector.blockSignals(True)
1112 self.definition_widget.specification_row_selector.clear()
1113 self.definition_widget.specification_column_selector.clear()
1114 for i, control_name in enumerate(self.physical_control_names):
1115 self.definition_widget.specification_row_selector.addItem(f"{i + 1}: {control_name}")
1116 self.definition_widget.specification_column_selector.addItem(f"{i + 1}: {control_name}")
1117 self.definition_widget.specification_row_selector.blockSignals(False)
1118 self.definition_widget.specification_column_selector.blockSignals(False)
1119 self.define_transformation_matrices(None, False)
1120 self.show_specification()
1121
1122 def define_transformation_matrices(
1123 self, clicked, dialog=True
1124 ): # pylint: disable=unused-argument
1125 """Defines the transformation matrices using the dialog box"""
1126 if dialog:
1127 (response_transformation, output_transformation, result) = (
1128 TransformationMatrixWindow.define_transformation_matrices(
1129 self.response_transformation_matrix,
1130 self.definition_widget.control_channels_display.value(),
1131 self.output_transformation_matrix,
1132 self.definition_widget.output_channels_display.value(),
1133 self.definition_widget,
1134 )
1135 )
1136 else:
1137 response_transformation = self.response_transformation_matrix
1138 output_transformation = self.output_transformation_matrix
1139 result = True
1140 if result:
1141 # Update the control names
1142 for widget in self.control_selector_widgets:
1143 widget.blockSignals(True)
1144 widget.clear()
1145 if response_transformation is None:
1146 for i, control_name in enumerate(self.physical_control_names):
1147 for widget in self.control_selector_widgets:
1148 widget.addItem(f"{i + 1}: {control_name}")
1149 self.definition_widget.transform_channels_display.setValue(
1150 len(self.physical_control_names)
1151 )
1152 else:
1153 for i in range(response_transformation.shape[0]):
1154 for widget in self.control_selector_widgets:
1155 widget.addItem(f"{i + 1}: Transformed Response")
1156 self.definition_widget.transform_channels_display.setValue(
1157 response_transformation.shape[0]
1158 )
1159 for widget in self.control_selector_widgets:
1160 widget.blockSignals(False)
1161 # Update the output names
1162 for widget in self.output_selector_widgets:
1163 widget.blockSignals(True)
1164 widget.clear()
1165 if output_transformation is None:
1166 for i, drive_name in enumerate(self.physical_output_names):
1167 for widget in self.output_selector_widgets:
1168 widget.addItem(f"{i + 1}: {drive_name}")
1169 self.definition_widget.transform_outputs_display.setValue(
1170 len(self.physical_output_names)
1171 )
1172 else:
1173 for i in range(output_transformation.shape[0]):
1174 for widget in self.output_selector_widgets:
1175 widget.addItem(f"{i + 1}: Transformed Drive")
1176 self.definition_widget.transform_outputs_display.setValue(
1177 output_transformation.shape[0]
1178 )
1179 for widget in self.output_selector_widgets:
1180 widget.blockSignals(False)
1181
1182 self.response_transformation_matrix = response_transformation
1183 self.output_transformation_matrix = output_transformation
1184 self.update_parameters_and_clear_spec()
1185
1186 def update_parameters(self):
1187 """Recompute derived parameters from updated sampling parameters"""
1188 data = self.collect_environment_definition_parameters()
1189 self.definition_widget.samples_per_acquire_display.setValue(data.samples_per_acquire)
1190 self.definition_widget.frame_time_display.setValue(data.frame_time)
1191 self.definition_widget.nyquist_frequency_display.setValue(data.nyquist_frequency)
1192 self.definition_widget.fft_lines_display.setValue(data.fft_lines)
1193 self.definition_widget.frequency_spacing_display.setValue(data.frequency_spacing)
1194 self.definition_widget.samples_per_write_display.setValue(data.samples_per_output)
1195
1196 def update_parameters_and_clear_spec(self):
1197 """Clears the specification data and updates parameters"""
1198 samples_per_frame = self.definition_widget.samples_per_frame_selector.value()
1199 if samples_per_frame % 2 != 0:
1200 self.definition_widget.samples_per_frame_selector.blockSignals(True)
1201 self.definition_widget.samples_per_frame_selector.setValue(samples_per_frame + 1)
1202 self.definition_widget.samples_per_frame_selector.blockSignals(False)
1203 self.specification_frequency_lines = None
1204 self.specification_cpsd_matrix = None
1205 self.specification_warning_matrix = None
1206 self.specification_abort_matrix = None
1207 self.definition_widget.specification_file_name_display.setText("")
1208 self.show_specification()
1209 self.update_parameters()
1210
1211 def collect_environment_definition_parameters(self) -> RandomVibrationMetadata:
1212 """
1213 Collect the parameters from the user interface defining the environment
1214
1215 Returns
1216 -------
1217 RandomVibrationMetadata
1218 A metadata or parameters object containing the parameters defining
1219 the corresponding environment.
1220
1221 """
1222 if self.python_control_module is None:
1223 control_module = None
1224 control_function = None
1225 control_function_type = None
1226 control_function_parameters = None
1227 else:
1228 control_module = self.definition_widget.control_script_file_path_input.text()
1229 control_function = self.definition_widget.control_function_input.itemText(
1230 self.definition_widget.control_function_input.currentIndex()
1231 )
1232 control_function_type = (
1233 self.definition_widget.control_function_generator_selector.currentIndex()
1234 )
1235 control_function_parameters = (
1236 self.definition_widget.control_parameters_text_input.toPlainText()
1237 )
1238 return RandomVibrationMetadata(
1239 number_of_channels=len(self.data_acquisition_parameters.channel_list),
1240 sample_rate=self.definition_widget.sample_rate_display.value(),
1241 samples_per_frame=self.definition_widget.samples_per_frame_selector.value(),
1242 test_level_ramp_time=self.definition_widget.ramp_time_spinbox.value(),
1243 cola_window=self.definition_widget.cola_window_selector.itemText(
1244 self.definition_widget.cola_window_selector.currentIndex()
1245 ),
1246 cola_overlap=self.definition_widget.cola_overlap_percentage_selector.value() / 100,
1247 cola_window_exponent=self.definition_widget.cola_exponent_selector.value(),
1248 sigma_clip=self.definition_widget.sigma_clipping_selector.value(),
1249 update_tf_during_control=self.definition_widget.update_transfer_function_during_control_selector.isChecked(),
1250 frames_in_cpsd=self.definition_widget.cpsd_frames_selector.value(),
1251 cpsd_window=self.definition_widget.cpsd_computation_window_selector.itemText(
1252 self.definition_widget.cpsd_computation_window_selector.currentIndex()
1253 ),
1254 cpsd_overlap=self.definition_widget.cpsd_overlap_selector.value() / 100,
1255 response_transformation_matrix=self.response_transformation_matrix,
1256 output_transformation_matrix=self.output_transformation_matrix,
1257 control_python_script=control_module,
1258 control_python_function=control_function,
1259 control_python_function_type=control_function_type,
1260 control_python_function_parameters=control_function_parameters,
1261 control_channel_indices=self.physical_control_indices,
1262 output_channel_indices=self.physical_output_indices,
1263 specification_frequency_lines=self.specification_frequency_lines,
1264 specification_cpsd_matrix=self.specification_cpsd_matrix,
1265 specification_warning_matrix=self.specification_warning_matrix,
1266 specification_abort_matrix=self.specification_abort_matrix,
1267 percent_lines_out=self.definition_widget.frequency_lines_out_spinbox.value(),
1268 allow_automatic_aborts=self.definition_widget.auto_abort_checkbox.isChecked(),
1269 )
1270
1271 def initialize_environment(self) -> RandomVibrationMetadata:
1272 """
1273 Update the user interface with environment parameters
1274
1275 This function is called when the Environment parameters are initialized.
1276 This function should set up the user interface accordingly. It must
1277 return the parameters class of the environment that inherits from
1278 AbstractMetadata.
1279
1280 Returns
1281 -------
1282 AbstractMetadata
1283 An AbstractMetadata-inheriting object that contains the parameters
1284 defining the environment.
1285
1286 """
1287 self.system_id_widget.samplesPerFrameSpinBox.setMaximum(
1288 self.definition_widget.samples_per_frame_selector.value()
1289 )
1290 self.system_id_widget.samplesPerFrameSpinBox.setValue(
1291 self.definition_widget.samples_per_frame_selector.value()
1292 )
1293 self.system_id_widget.levelRampTimeDoubleSpinBox.setValue(
1294 self.definition_widget.ramp_time_spinbox.value()
1295 )
1296 super().initialize_environment()
1297 for widget in [
1298 self.prediction_widget.response_row_selector,
1299 self.prediction_widget.response_column_selector,
1300 self.run_widget.control_channel_1_selector,
1301 self.run_widget.control_channel_2_selector,
1302 ]:
1303 widget.blockSignals(True)
1304 widget.clear()
1305 for i, control_name in enumerate(self.initialized_control_names):
1306 widget.addItem(f"{i + 1}: {control_name}")
1307 widget.blockSignals(False)
1308 for widget in [
1309 self.prediction_widget.excitation_row_selector,
1310 self.prediction_widget.excitation_column_selector,
1311 ]:
1312 widget.blockSignals(True)
1313 widget.clear()
1314 for i, drive_name in enumerate(self.initialized_output_names):
1315 widget.addItem(f"{i + 1}: {drive_name}")
1316 widget.blockSignals(False)
1317 # Set up the prediction plots
1318 self.prediction_widget.excitation_display_plot.getPlotItem().clear()
1319 self.prediction_widget.response_display_plot.getPlotItem().clear()
1320 self.prediction_widget.excitation_display_plot.getPlotItem().addLegend()
1321 self.prediction_widget.response_display_plot.getPlotItem().addLegend()
1322 self.plot_data_items["response_prediction"] = multiline_plotter(
1323 np.arange(self.environment_parameters.fft_lines)
1324 * self.environment_parameters.frequency_spacing,
1325 np.zeros((4, self.environment_parameters.fft_lines)),
1326 widget=self.prediction_widget.response_display_plot,
1327 other_pen_options={"width": 2},
1328 names=["Real Prediction", "Real Spec", "Imag Prediction", "Imag Spec"],
1329 )
1330 self.plot_data_items[
1331 "prediction_warning_upper"
1332 ] = self.prediction_widget.response_display_plot.getPlotItem().plot(
1333 np.array([0, self.data_acquisition_parameters.sample_rate / 2]),
1334 np.zeros(2),
1335 pen={
1336 "color": PlotWindow.WARNING_COLOR,
1337 "width": PlotWindow.WARNING_LINEWIDTH,
1338 "style": PlotWindow.WARNING_LINESTYLE,
1339 },
1340 name="Warning",
1341 )
1342 self.plot_data_items[
1343 "prediction_warning_lower"
1344 ] = self.prediction_widget.response_display_plot.getPlotItem().plot(
1345 np.array([0, self.data_acquisition_parameters.sample_rate / 2]),
1346 np.zeros(2),
1347 pen={
1348 "color": PlotWindow.WARNING_COLOR,
1349 "width": PlotWindow.WARNING_LINEWIDTH,
1350 "style": PlotWindow.WARNING_LINESTYLE,
1351 },
1352 )
1353 self.plot_data_items[
1354 "prediction_abort_upper"
1355 ] = self.prediction_widget.response_display_plot.getPlotItem().plot(
1356 np.array([0, self.data_acquisition_parameters.sample_rate / 2]),
1357 np.zeros(2),
1358 pen={
1359 "color": PlotWindow.ABORT_COLOR,
1360 "width": PlotWindow.ABORT_LINEWIDTH,
1361 "style": PlotWindow.ABORT_LINESTYLE,
1362 },
1363 name="Abort",
1364 )
1365 self.plot_data_items[
1366 "prediction_abort_lower"
1367 ] = self.prediction_widget.response_display_plot.getPlotItem().plot(
1368 np.array([0, self.data_acquisition_parameters.sample_rate / 2]),
1369 np.zeros(2),
1370 pen={
1371 "color": PlotWindow.ABORT_COLOR,
1372 "width": PlotWindow.ABORT_LINEWIDTH,
1373 "style": PlotWindow.ABORT_LINESTYLE,
1374 },
1375 )
1376 self.plot_data_items["excitation_prediction"] = multiline_plotter(
1377 np.arange(self.environment_parameters.fft_lines)
1378 * self.environment_parameters.frequency_spacing,
1379 np.zeros((2, self.environment_parameters.fft_lines)),
1380 widget=self.prediction_widget.excitation_display_plot,
1381 other_pen_options={"width": 1},
1382 names=["Real Prediction", "Imag Prediction"],
1383 )
1384 # Create the interactive control law if necessary
1385 if self.definition_widget.control_function_generator_selector.currentIndex() == 3:
1386 control_class = getattr(
1387 self.python_control_module,
1388 self.definition_widget.control_function_input.itemText(
1389 self.definition_widget.control_function_input.currentIndex()
1390 ),
1391 )
1392 self.log(f"Building Interactive UI for class {control_class.__name__}")
1393 ui_class = control_class.get_ui_class()
1394 if ui_class == self.interactive_control_law_widget.__class__:
1395 print("initializing data acquisition and environment parameters")
1396 self.interactive_control_law_widget.initialize_parameters(
1397 self.data_acquisition_parameters, self.environment_parameters
1398 )
1399 else:
1400 if self.interactive_control_law_widget is not None:
1401 self.interactive_control_law_widget.close()
1402 self.interactive_control_law_window = QtWidgets.QDialog(self.definition_widget)
1403 self.interactive_control_law_widget = ui_class(
1404 self.log_name,
1405 self.environment_command_queue,
1406 self.interactive_control_law_window,
1407 self,
1408 self.data_acquisition_parameters,
1409 self.environment_parameters,
1410 )
1411 self.interactive_control_law_window.show()
1412 return self.environment_parameters
1413
1414 # %% Test Predictions
1415
1416 def show_max_voltage_prediction(self):
1417 """Shows the prediction with the largest RMS voltage"""
1418 widget = self.prediction_widget.excitation_voltage_list
1419 index = np.argmax([float(widget.item(v).text()) for v in range(widget.count())])
1420 self.prediction_widget.excitation_row_selector.setCurrentIndex(index)
1421 self.prediction_widget.excitation_column_selector.setCurrentIndex(index)
1422
1423 def show_min_voltage_prediction(self):
1424 """Shows the prediction with the smallest RMS voltage"""
1425 widget = self.prediction_widget.excitation_voltage_list
1426 index = np.argmin([float(widget.item(v).text()) for v in range(widget.count())])
1427 self.prediction_widget.excitation_row_selector.setCurrentIndex(index)
1428 self.prediction_widget.excitation_column_selector.setCurrentIndex(index)
1429
1430 def show_max_error_prediction(self):
1431 """Shows the prediction with the largest error"""
1432 widget = self.prediction_widget.response_error_list
1433 index = np.argmax([float(widget.item(v).text()) for v in range(widget.count())])
1434 self.prediction_widget.response_row_selector.setCurrentIndex(index)
1435 self.prediction_widget.response_column_selector.setCurrentIndex(index)
1436
1437 def show_min_error_prediction(self):
1438 """Shows the prediction with the smallest error"""
1439 widget = self.prediction_widget.response_error_list
1440 index = np.argmin([float(widget.item(v).text()) for v in range(widget.count())])
1441 self.prediction_widget.response_row_selector.setCurrentIndex(index)
1442 self.prediction_widget.response_column_selector.setCurrentIndex(index)
1443
1444 def update_response_error_prediction_selector(self, item):
1445 """Updates the selection when an item is double-clicked"""
1446 index = self.prediction_widget.response_error_list.row(item)
1447 self.prediction_widget.response_row_selector.setCurrentIndex(index)
1448 self.prediction_widget.response_column_selector.setCurrentIndex(index)
1449
1450 def update_excitation_prediction_selector(self, item):
1451 """Updates the selection when an item is double-clicked"""
1452 index = self.prediction_widget.excitation_voltage_list.row(item)
1453 self.prediction_widget.excitation_row_selector.setCurrentIndex(index)
1454 self.prediction_widget.excitation_column_selector.setCurrentIndex(index)
1455
1456 def update_control_predictions(self):
1457 """Updates the control prediction with new data"""
1458 excite_row_index = self.prediction_widget.excitation_row_selector.currentIndex()
1459 excite_column_index = self.prediction_widget.excitation_column_selector.currentIndex()
1460 self.plot_data_items["excitation_prediction"][0].setData(
1461 self.frequencies,
1462 np.abs(np.real(self.excitation_prediction[:, excite_row_index, excite_column_index])),
1463 )
1464 row_index = self.prediction_widget.response_row_selector.currentIndex()
1465 column_index = self.prediction_widget.response_column_selector.currentIndex()
1466 self.plot_data_items["response_prediction"][0].setData(
1467 self.frequencies,
1468 np.abs(np.real(self.response_prediction[:, row_index, column_index])),
1469 )
1470 if row_index == column_index:
1471 warning_upper = abs(
1472 self.environment_parameters.specification_warning_matrix[1, :, row_index]
1473 )
1474 warning_lower = abs(
1475 self.environment_parameters.specification_warning_matrix[0, :, row_index]
1476 )
1477 abort_upper = abs(
1478 self.environment_parameters.specification_abort_matrix[1, :, row_index]
1479 )
1480 abort_lower = abs(
1481 self.environment_parameters.specification_abort_matrix[0, :, row_index]
1482 )
1483 self.plot_data_items["prediction_warning_upper"].setData(
1484 self.specification_frequency_lines, warning_upper
1485 )
1486 self.plot_data_items["prediction_warning_lower"].setData(
1487 self.specification_frequency_lines, warning_lower
1488 )
1489 self.plot_data_items["prediction_abort_upper"].setData(
1490 self.specification_frequency_lines, abort_upper
1491 )
1492 self.plot_data_items["prediction_abort_lower"].setData(
1493 self.specification_frequency_lines, abort_lower
1494 )
1495 self.plot_data_items["excitation_prediction"][1].setData(
1496 self.frequencies, np.zeros(self.frequencies.shape)
1497 )
1498 self.plot_data_items["response_prediction"][2].setData(
1499 self.frequencies, np.zeros(self.frequencies.shape)
1500 )
1501 self.plot_data_items["response_prediction"][3].setData(
1502 self.frequencies, np.zeros(self.frequencies.shape)
1503 )
1504 else:
1505 self.plot_data_items["prediction_warning_upper"].setData(
1506 np.array([0, self.definition_widget.sample_rate_display.value() / 2]),
1507 np.zeros(2),
1508 )
1509 self.plot_data_items["prediction_warning_lower"].setData(
1510 np.array([0, self.definition_widget.sample_rate_display.value() / 2]),
1511 np.zeros(2),
1512 )
1513 self.plot_data_items["prediction_abort_upper"].setData(
1514 np.array([0, self.definition_widget.sample_rate_display.value() / 2]),
1515 np.zeros(2),
1516 )
1517 self.plot_data_items["prediction_abort_lower"].setData(
1518 np.array([0, self.definition_widget.sample_rate_display.value() / 2]),
1519 np.zeros(2),
1520 )
1521 self.plot_data_items["excitation_prediction"][1].setData(
1522 self.frequencies,
1523 np.abs(
1524 np.imag(self.excitation_prediction[:, excite_row_index, excite_column_index])
1525 ),
1526 )
1527 self.plot_data_items["response_prediction"][2].setData(
1528 self.frequencies,
1529 np.abs(np.imag(self.response_prediction[:, row_index, column_index])),
1530 )
1531 self.plot_data_items["response_prediction"][3].setData(
1532 self.frequencies,
1533 np.abs(
1534 np.imag(
1535 self.environment_parameters.specification_cpsd_matrix[
1536 :, row_index, column_index
1537 ]
1538 )
1539 ),
1540 )
1541 self.plot_data_items["response_prediction"][1].setData(
1542 self.frequencies,
1543 np.abs(
1544 np.real(
1545 self.environment_parameters.specification_cpsd_matrix[
1546 :, row_index, column_index
1547 ]
1548 )
1549 ),
1550 )
1551
1552 def recompute_prediction(self):
1553 """Sends a message to the environment process to recompute the prediction"""
1554 self.environment_command_queue.put(
1555 self.log_name, (RandomVibrationCommands.RECOMPUTE_PREDICTION, None)
1556 )
1557
1558 # %% Run Control
1559
1560 def start_control(self):
1561 """Runs the corresponding environment in the controller"""
1562 self.enable_control(False)
1563 self.controller_communication_queue.put(
1564 self.log_name, (GlobalCommands.START_ENVIRONMENT, self.environment_name)
1565 )
1566 self.environment_command_queue.put(
1567 self.log_name,
1568 (
1569 RandomVibrationCommands.START_CONTROL,
1570 db2scale(self.run_widget.current_test_level_selector.value()),
1571 ),
1572 )
1573 self.run_timer.start(250)
1574 self.run_start_time = time.time()
1575 self.run_level_start_time = self.run_start_time
1576 self.run_widget.test_progress_bar.setValue(0)
1577 if (
1578 self.run_widget.current_test_level_selector.value()
1579 >= self.run_widget.target_test_level_selector.value()
1580 ):
1581 self.controller_communication_queue.put(
1582 self.log_name, (GlobalCommands.AT_TARGET_LEVEL, self.environment_name)
1583 )
1584
1585 def stop_control(self):
1586 """Stops the corresponding environment in the controller"""
1587 self.run_widget.stop_test_button.setEnabled(False)
1588 self.environment_command_queue.put(
1589 self.log_name, (RandomVibrationCommands.STOP_CONTROL, None)
1590 )
1591 self.run_timer.stop()
1592
1593 def enable_control(self, enabled):
1594 """Enables or disables widgets to start or stop control if the control is running or not"""
1595 for widget in [
1596 self.run_widget.test_time_selector,
1597 self.run_widget.time_test_at_target_level_checkbox,
1598 self.run_widget.timed_test_radiobutton,
1599 self.run_widget.continuous_test_radiobutton,
1600 self.run_widget.target_test_level_selector,
1601 self.run_widget.start_test_button,
1602 ]:
1603 widget.setEnabled(enabled)
1604 for widget in [self.run_widget.stop_test_button]:
1605 widget.setEnabled(not enabled)
1606 if enabled:
1607 self.run_timer.stop()
1608
1609 def update_run_time(self):
1610 """Updates the time that the control has been running on the GUI"""
1611 # Update the total run time
1612 current_time = time.time()
1613 time_elapsed = current_time - self.run_start_time
1614 time_at_level_elapsed = current_time - self.run_level_start_time
1615 self.run_widget.total_test_time_display.setText(
1616 str(datetime.timedelta(seconds=time_elapsed)).split(".", maxsplit=1)[0]
1617 )
1618 self.run_widget.time_at_level_display.setText(
1619 str(datetime.timedelta(seconds=time_at_level_elapsed)).split(".", maxsplit=1)[0]
1620 )
1621 # Check if we need to stop the test due to timeout
1622 if self.run_widget.timed_test_radiobutton.isChecked():
1623 check_time = self.run_widget.test_time_selector.time()
1624 check_time_seconds = (
1625 check_time.hour() * 3600 + check_time.minute() * 60 + check_time.second()
1626 )
1627 if self.run_widget.time_test_at_target_level_checkbox.isChecked():
1628 if (
1629 self.run_widget.current_test_level_selector.value()
1630 >= self.run_widget.target_test_level_selector.value()
1631 ):
1632 self.run_widget.test_progress_bar.setValue(
1633 int(time_at_level_elapsed / check_time_seconds * 100)
1634 )
1635 if time_at_level_elapsed > check_time_seconds:
1636 self.run_widget.test_progress_bar.setValue(100)
1637 self.stop_control()
1638 else:
1639 self.run_widget.test_progress_bar.setValue(0)
1640 else:
1641 self.run_widget.test_progress_bar.setValue(
1642 int(time_elapsed / check_time_seconds * 100)
1643 )
1644 if time_elapsed > check_time_seconds:
1645 self.stop_control()
1646
1647 def change_control_test_level(self):
1648 """Updates the test level of the control."""
1649 self.environment_command_queue.put(
1650 self.log_name,
1651 (
1652 RandomVibrationCommands.ADJUST_TEST_LEVEL,
1653 db2scale(self.run_widget.current_test_level_selector.value()),
1654 ),
1655 )
1656 self.run_level_start_time = time.time()
1657 # Check and see if we need to start streaming data
1658 if (
1659 self.run_widget.current_test_level_selector.value()
1660 >= self.run_widget.target_test_level_selector.value()
1661 ):
1662 self.controller_communication_queue.put(
1663 self.log_name, (GlobalCommands.AT_TARGET_LEVEL, self.environment_name)
1664 )
1665
1666 def change_test_level_from_profile(self, test_level):
1667 """Sets the test level from a profile instruction
1668
1669 Parameters
1670 ----------
1671 test_level :
1672 Value to set the test level to.
1673 """
1674 self.run_widget.current_test_level_selector.setValue(float(test_level))
1675
1676 def change_specification_from_profile(self, new_specification_file):
1677 """
1678 Loads in a new specification and starts controlling to it
1679
1680 Parameters
1681 ----------
1682 new_specification_file : str
1683 File path to a new specification file
1684
1685 """
1686 self.select_spec_file(None, new_specification_file)
1687 environment_parameters = self.initialize_environment()
1688 self.environment_command_queue.put(
1689 self.log_name,
1690 (GlobalCommands.INITIALIZE_ENVIRONMENT_PARAMETERS, environment_parameters),
1691 )
1692
1693 def show_magnitude_window(self, item):
1694 """Creates a window showing the magnitude of a signal when an item is double-clicked"""
1695 index = self.run_widget.test_response_error_list.row(item)
1696 self.create_window(None, index, index, 0)
1697
1698 def create_window(
1699 self, event, row_index=None, column_index=None, datatype_index=None
1700 ): # pylint: disable=unused-argument
1701 """Creates a subwindow to show a specific channel information
1702
1703 Parameters
1704 ----------
1705 event :
1706
1707 row_index :
1708 Row index in the CPSD matrix to display (Default value = None)
1709 column_index :
1710 Column index in the CPSD matrix to display (Default value = None)
1711 datatype_index :
1712 Data type to display (real,imag,mag,phase,etc) (Default value = None)
1713
1714 """
1715 if row_index is None:
1716 row_index = self.run_widget.control_channel_1_selector.currentIndex()
1717 if column_index is None:
1718 column_index = self.run_widget.control_channel_2_selector.currentIndex()
1719 if datatype_index is None:
1720 datatype_index = self.run_widget.data_type_selector.currentIndex()
1721 self.plot_windows.append(
1722 PlotWindow(
1723 None,
1724 row_index,
1725 column_index,
1726 datatype_index,
1727 (self.specification_frequency_lines, self.specification_cpsd_matrix),
1728 self.run_widget.control_channel_1_selector.itemText(row_index),
1729 self.run_widget.control_channel_2_selector.itemText(column_index),
1730 self.run_widget.data_type_selector.itemText(datatype_index),
1731 (
1732 self.specification_warning_matrix
1733 if row_index == column_index and datatype_index == 0
1734 else None
1735 ),
1736 (
1737 self.specification_abort_matrix
1738 if row_index == column_index and datatype_index == 0
1739 else None
1740 ),
1741 )
1742 )
1743
1744 def show_all_asds(self):
1745 """Creates a subwindow for each ASD in the CPSD matrix"""
1746 for i in range(self.specification_cpsd_matrix.shape[-1]):
1747 self.create_window(None, i, i, 0)
1748 self.tile_windows()
1749
1750 def show_all_csds_phscoh(self):
1751 """Creates a subwindow for each entry in the CPSD matrix showing phase and coherence"""
1752 for i in range(self.specification_cpsd_matrix.shape[-1]):
1753 for j in range(self.specification_cpsd_matrix.shape[-1]):
1754 if i == j:
1755 datatype_index = 0
1756 elif i < j:
1757 datatype_index = 1
1758 elif i > j:
1759 datatype_index = 2
1760 else:
1761 raise ValueError("Invalid situation. How did you get here?!")
1762 self.create_window(None, i, j, datatype_index)
1763 self.tile_windows()
1764
1765 def show_all_csds_realimag(self):
1766 """Creates a subwindow for each entry in the CPSD matrix showing real and imaginary"""
1767 for i in range(self.specification_cpsd_matrix.shape[-1]):
1768 for j in range(self.specification_cpsd_matrix.shape[-1]):
1769 if i == j:
1770 datatype_index = 0
1771 elif i < j:
1772 datatype_index = 3
1773 elif i > j:
1774 datatype_index = 4
1775 else:
1776 raise ValueError("Invalid situation. How did you get here?!")
1777 self.create_window(None, i, j, datatype_index)
1778 self.tile_windows()
1779
1780 def tile_windows(self):
1781 """Tile subwindow equally across the screen"""
1782 screen_rect = QtWidgets.QApplication.desktop().screenGeometry()
1783 # Go through and remove any closed windows
1784 self.plot_windows = [window for window in self.plot_windows if window.isVisible()]
1785 num_windows = len(self.plot_windows)
1786 ncols = int(np.ceil(np.sqrt(num_windows)))
1787 nrows = int(np.ceil(num_windows / ncols))
1788 window_width = int(screen_rect.width() / ncols)
1789 window_height = int(screen_rect.height() / nrows)
1790 for index, window in enumerate(self.plot_windows):
1791 window.resize(window_width, window_height)
1792 row_ind = index // ncols
1793 col_ind = index % ncols
1794 window.move(col_ind * window_width, row_ind * window_height)
1795
1796 def close_windows(self):
1797 """Close all subwindows"""
1798 for window in self.plot_windows:
1799 window.close()
1800
1801 def save_control_data_from_profile(self, filename):
1802 """Saves the control data to a file when requested by a profile argument"""
1803 self.save_spectral_data(None, filename)
1804
1805 def save_spectral_data(self, clicked, filename=None): # pylint: disable=unused-argument
1806 """Save Spectral Data from the Controller"""
1807 if filename is None:
1808 filename, _ = QtWidgets.QFileDialog.getSaveFileName(
1809 self.definition_widget,
1810 "Select File to Save Spectral Data",
1811 filter="NetCDF File (*.nc4)",
1812 )
1813 if filename == "":
1814 return
1815 labels = [
1816 ["node_number", str],
1817 ["node_direction", str],
1818 ["comment", str],
1819 ["serial_number", str],
1820 ["triax_dof", str],
1821 ["sensitivity", str],
1822 ["unit", str],
1823 ["make", str],
1824 ["model", str],
1825 ["expiration", str],
1826 ["physical_device", str],
1827 ["physical_channel", str],
1828 ["channel_type", str],
1829 ["minimum_value", str],
1830 ["maximum_value", str],
1831 ["coupling", str],
1832 ["excitation_source", str],
1833 ["excitation", str],
1834 ["feedback_device", str],
1835 ["feedback_channel", str],
1836 ["warning_level", str],
1837 ["abort_level", str],
1838 ]
1839 global_data_parameters: DataAcquisitionParameters
1840 global_data_parameters = self.data_acquisition_parameters
1841 netcdf_handle = nc4.Dataset( # pylint: disable=no-member
1842 filename, "w", format="NETCDF4", clobber=True
1843 )
1844 # Create dimensions
1845 netcdf_handle.createDimension("response_channels", len(global_data_parameters.channel_list))
1846 netcdf_handle.createDimension(
1847 "output_channels",
1848 len(
1849 [
1850 channel
1851 for channel in global_data_parameters.channel_list
1852 if channel.feedback_device is not None
1853 ]
1854 ),
1855 )
1856 netcdf_handle.createDimension("time_samples", None)
1857 netcdf_handle.createDimension(
1858 "num_environments", len(global_data_parameters.environment_names)
1859 )
1860 # Create attributes
1861 netcdf_handle.file_version = "3.0.0"
1862 netcdf_handle.sample_rate = global_data_parameters.sample_rate
1863 netcdf_handle.time_per_write = (
1864 global_data_parameters.samples_per_write / global_data_parameters.output_sample_rate
1865 )
1866 netcdf_handle.time_per_read = (
1867 global_data_parameters.samples_per_read / global_data_parameters.sample_rate
1868 )
1869 netcdf_handle.hardware = global_data_parameters.hardware
1870 netcdf_handle.hardware_file = (
1871 "None"
1872 if global_data_parameters.hardware_file is None
1873 else global_data_parameters.hardware_file
1874 )
1875 netcdf_handle.output_oversample = global_data_parameters.output_oversample
1876 for key, value in global_data_parameters.extra_parameters.items():
1877 setattr(netcdf_handle, key, value)
1878 # Create Variables
1879 var = netcdf_handle.createVariable("environment_names", str, ("num_environments",))
1880 this_environment_index = None
1881 for i, name in enumerate(global_data_parameters.environment_names):
1882 var[i] = name
1883 if name == self.environment_name:
1884 this_environment_index = i
1885 var = netcdf_handle.createVariable(
1886 "environment_active_channels",
1887 "i1",
1888 ("response_channels", "num_environments"),
1889 )
1890 var[...] = global_data_parameters.environment_active_channels.astype("int8")[
1891 global_data_parameters.environment_active_channels[:, this_environment_index],
1892 :,
1893 ]
1894 # Create channel table variables
1895 for label, netcdf_datatype in labels:
1896 var = netcdf_handle.createVariable(
1897 "/channels/" + label, netcdf_datatype, ("response_channels",)
1898 )
1899 channel_data = [
1900 getattr(channel, label) for channel in global_data_parameters.channel_list
1901 ]
1902 if netcdf_datatype == "i1":
1903 channel_data = np.array([1 if val else 0 for val in channel_data])
1904 else:
1905 channel_data = ["" if val is None else val for val in channel_data]
1906 for i, cd in enumerate(channel_data):
1907 var[i] = cd
1908 group_handle = netcdf_handle.createGroup(self.environment_name)
1909 self.environment_parameters.store_to_netcdf(group_handle)
1910 # Create Variables for Spectral Data
1911 group_handle.createDimension("drive_channels", self.last_transfer_function.shape[2])
1912 var = group_handle.createVariable(
1913 "frf_data_real",
1914 "f8",
1915 ("fft_lines", "specification_channels", "drive_channels"),
1916 )
1917 var[...] = self.last_transfer_function.real
1918 var = group_handle.createVariable(
1919 "frf_data_imag",
1920 "f8",
1921 ("fft_lines", "specification_channels", "drive_channels"),
1922 )
1923 var[...] = self.last_transfer_function.imag
1924 var = group_handle.createVariable(
1925 "frf_coherence", "f8", ("fft_lines", "specification_channels")
1926 )
1927 var[...] = self.last_coherence.real
1928 var = group_handle.createVariable(
1929 "response_cpsd_real",
1930 "f8",
1931 ("fft_lines", "specification_channels", "specification_channels"),
1932 )
1933 var[...] = self.last_response_cpsd.real
1934 var = group_handle.createVariable(
1935 "response_cpsd_imag",
1936 "f8",
1937 ("fft_lines", "specification_channels", "specification_channels"),
1938 )
1939 var[...] = self.last_response_cpsd.imag
1940 var = group_handle.createVariable(
1941 "drive_cpsd_real", "f8", ("fft_lines", "drive_channels", "drive_channels")
1942 )
1943 var[...] = self.last_reference_cpsd.real
1944 var = group_handle.createVariable(
1945 "drive_cpsd_imag", "f8", ("fft_lines", "drive_channels", "drive_channels")
1946 )
1947 var[...] = self.last_reference_cpsd.imag
1948 var = group_handle.createVariable(
1949 "response_noise_cpsd_real",
1950 "f8",
1951 ("fft_lines", "specification_channels", "specification_channels"),
1952 )
1953 var[...] = self.last_response_noise.real
1954 var = group_handle.createVariable(
1955 "response_noise_cpsd_imag",
1956 "f8",
1957 ("fft_lines", "specification_channels", "specification_channels"),
1958 )
1959 var[...] = self.last_response_noise.imag
1960 var = group_handle.createVariable(
1961 "drive_noise_cpsd_real",
1962 "f8",
1963 ("fft_lines", "drive_channels", "drive_channels"),
1964 )
1965 var[...] = self.last_reference_noise.real
1966 var = group_handle.createVariable(
1967 "drive_noise_cpsd_imag",
1968 "f8",
1969 ("fft_lines", "drive_channels", "drive_channels"),
1970 )
1971 var[...] = self.last_reference_noise.imag
1972 netcdf_handle.close()
1973
1974 # %% Miscellaneous
1975
1976 def retrieve_metadata(
1977 self,
1978 netcdf_handle: nc4._netCDF4.Dataset = None, # pylint: disable=c-extension-no-member
1979 environment_name: str = None,
1980 ):
1981 """Collects environment parameters from a netCDF dataset.
1982
1983 This function retrieves parameters from a netCDF dataset that was written
1984 by the controller during streaming. It must populate the widgets
1985 in the user interface with the proper information.
1986
1987 This function is the "read" counterpart to the store_to_netcdf
1988 function in the AbstractMetadata class, which will write parameters to
1989 the netCDF file to document the metadata.
1990
1991 Note that the entire dataset is passed to this function, so the function
1992 should collect parameters pertaining to the environment from a Group
1993 in the dataset sharing the environment's name, e.g.
1994
1995 ``group = netcdf_handle.groups[self.environment_name]``
1996 ``self.definition_widget.parameter_selector.setValue(group.parameter)``
1997
1998 Parameters
1999 ----------
2000 netcdf_handle : nc4._netCDF4.Dataset
2001 The netCDF dataset from which the data will be read. It should have
2002 a group name with the enviroment's name.
2003 environment_name : str (optional)
2004 name of environment from which to retrieve metadata. Only needed if
2005 different from current environment.
2006
2007 """
2008 group = super().retrieve_metadata(netcdf_handle, environment_name)
2009
2010 # Control channels
2011 try:
2012 for i in group.variables["control_channel_indices"][...]:
2013 item = self.definition_widget.control_channels_selector.item(i)
2014 item.setCheckState(Qt.Checked)
2015 except KeyError:
2016 print("no variable control_channel_indices, please select control channels manually")
2017 # Other data
2018 try:
2019 self.response_transformation_matrix = group.variables["response_transformation_matrix"][
2020 ...
2021 ].data
2022 except KeyError:
2023 self.response_transformation_matrix = None
2024 try:
2025 self.output_transformation_matrix = group.variables["reference_transformation_matrix"][
2026 ...
2027 ].data
2028 except KeyError:
2029 self.output_transformation_matrix = None
2030 self.define_transformation_matrices(None, dialog=False)
2031
2032 # environment_name is passed when the saved environment doesn't match the
2033 # current environment
2034 if environment_name is None:
2035 # Spinboxes
2036 self.definition_widget.samples_per_frame_selector.setValue(group.samples_per_frame)
2037 self.definition_widget.ramp_time_spinbox.setValue(group.test_level_ramp_time)
2038 self.definition_widget.cola_overlap_percentage_selector.setValue(
2039 group.cola_overlap * 100
2040 )
2041 self.definition_widget.cola_exponent_selector.setValue(group.cola_window_exponent)
2042 self.definition_widget.cpsd_overlap_selector.setValue(group.cpsd_overlap * 100)
2043 self.definition_widget.cpsd_frames_selector.setValue(group.frames_in_cpsd)
2044 # Checkboxes
2045 self.definition_widget.update_transfer_function_during_control_selector.setChecked(
2046 bool(group.update_tf_during_control)
2047 )
2048 self.definition_widget.auto_abort_checkbox.setChecked(
2049 bool(group.allow_automatic_aborts)
2050 )
2051 # Comboboxes
2052 self.definition_widget.cola_window_selector.setCurrentIndex(
2053 self.definition_widget.cola_window_selector.findText(group.cola_window)
2054 )
2055 self.definition_widget.cpsd_computation_window_selector.setCurrentIndex(
2056 self.definition_widget.cpsd_computation_window_selector.findText(group.cpsd_window)
2057 )
2058 # Specification
2059 self.specification_frequency_lines = group.variables["specification_frequency_lines"][
2060 ...
2061 ].data
2062 self.specification_cpsd_matrix = (
2063 group.variables["specification_cpsd_matrix_real"][...].data
2064 + 1j * group.variables["specification_cpsd_matrix_imag"][...].data
2065 )
2066 self.specification_warning_matrix = group.variables["specification_warning_matrix"][
2067 ...
2068 ].data
2069 self.specification_abort_matrix = group.variables["specification_abort_matrix"][
2070 ...
2071 ].data
2072 self.select_python_module(None, group.control_python_script)
2073 index = self.definition_widget.control_function_input.findText(
2074 group.control_python_function
2075 )
2076 if (
2077 index == -1
2078 ): # error handling (older revisions of rattlesnake may be missing newer control laws)
2079 index = 0
2080 default = self.definition_widget.control_function_input.itemText(index)
2081 print(
2082 f'Warning: control function "{group.control_python_function}" not found, '
2083 f'defaulting to "{default}"'
2084 )
2085 self.definition_widget.control_function_input.setCurrentIndex(index)
2086 self.definition_widget.control_parameters_text_input.setText(
2087 group.control_python_function_parameters
2088 )
2089 self.show_specification()
2090
2091 def update_gui(self, queue_data: tuple):
2092 """Update the environment's graphical user interface
2093
2094 This function will receive data from the gui_update_queue that
2095 specifies how the user interface should be updated. Data will usually
2096 be received as ``(instruction,data)`` pairs, where the ``instruction`` notes
2097 what operation should be taken or which widget should be modified, and
2098 the ``data`` notes what data should be used in the update.
2099
2100 Parameters
2101 ----------
2102 queue_data : tuple
2103 A tuple containing ``(instruction,data)`` pairs where ``instruction``
2104 defines and operation or widget to be modified and ``data`` contains
2105 the data used to perform the operation.
2106 """
2107 if super().update_gui(queue_data):
2108 return
2109 message, data = queue_data
2110 if message == "control_predictions":
2111 (
2112 _,
2113 self.excitation_prediction,
2114 self.response_prediction,
2115 _,
2116 rms_voltage_prediction,
2117 rms_db_error_prediction,
2118 ) = data
2119 self.update_control_predictions()
2120 for widget, widget_data in zip(
2121 [
2122 self.prediction_widget.excitation_voltage_list,
2123 self.prediction_widget.response_error_list,
2124 ],
2125 [rms_voltage_prediction, rms_db_error_prediction],
2126 ):
2127 widget.clear()
2128 widget.addItems([f"{d:.3f}" for d in widget_data])
2129 # Now compute if any channels are erroring or not
2130 with np.errstate(invalid="ignore"):
2131 lines_out = (
2132 self.environment_parameters.percent_lines_out / 100
2133 ) * self.environment_parameters.fft_lines
2134 for i in range(self.prediction_widget.response_error_list.count()):
2135 item = self.prediction_widget.response_error_list.item(i)
2136 if (
2137 sum(
2138 self.response_prediction[:, i, i]
2139 > self.environment_parameters.specification_abort_matrix[1, :, i]
2140 )
2141 > lines_out
2142 ):
2143 item.setBackground(QColor(255, 125, 125))
2144 elif (
2145 sum(
2146 self.response_prediction[:, i, i]
2147 < self.environment_parameters.specification_abort_matrix[0, :, i]
2148 )
2149 > lines_out
2150 ):
2151 item.setBackground(QColor(255, 125, 125))
2152 elif (
2153 sum(
2154 self.response_prediction[:, i, i]
2155 > self.environment_parameters.specification_warning_matrix[1, :, i]
2156 )
2157 > lines_out
2158 ):
2159 item.setBackground(QColor(255, 255, 125))
2160 elif (
2161 sum(
2162 self.response_prediction[:, i, i]
2163 < self.environment_parameters.specification_warning_matrix[0, :, i]
2164 )
2165 > lines_out
2166 ):
2167 item.setBackground(QColor(255, 255, 125))
2168 else:
2169 item.setBackground(QColor(255, 255, 255))
2170 elif message == "control_update":
2171 (
2172 frames,
2173 total_frames,
2174 self.frequencies,
2175 self.last_transfer_function,
2176 self.last_coherence,
2177 self.last_response_cpsd,
2178 self.last_reference_cpsd,
2179 self.last_condition,
2180 ) = data
2181 self.update_sysid_plots(
2182 update_time=False, update_transfer_function=True, update_noise=True
2183 )
2184 self.system_id_widget.current_frames_spinbox.setValue(frames)
2185 self.system_id_widget.total_frames_spinbox.setValue(total_frames)
2186 self.system_id_widget.progressBar.setValue(int(frames / total_frames * 100))
2187 self.plot_data_items["sum_asds_control"].setData(
2188 self.frequencies, np.einsum("ijj", self.last_response_cpsd).real
2189 )
2190 # Go through and remove any closed windows
2191 self.plot_windows = [window for window in self.plot_windows if window.isVisible()]
2192 for window in self.plot_windows:
2193 window.update_plot(self.last_response_cpsd)
2194 elif message == "interactive_control_sysid_update":
2195 if self.interactive_control_law_widget is not None:
2196 self.interactive_control_law_widget.update_ui_sysid(*data)
2197 elif message == "interactive_control_update":
2198 if self.interactive_control_law_widget is not None:
2199 self.interactive_control_law_widget.update_ui_control(data)
2200 elif message == "update_test_response_error_list":
2201 rms_db_error, warning_channels, abort_channels = data
2202 self.run_widget.test_response_error_list.clear()
2203 self.run_widget.test_response_error_list.addItems([f"{d:.3f}" for d in rms_db_error])
2204 for index in warning_channels:
2205 item = self.run_widget.test_response_error_list.item(index)
2206 item.setBackground(QColor(255, 255, 125))
2207 for index in abort_channels:
2208 item = self.run_widget.test_response_error_list.item(index)
2209 item.setBackground(QColor(255, 125, 125))
2210 elif message == "enable_control":
2211 self.enable_control(True)
2212 elif message == "enable":
2213 widget = None
2214 for parent in [
2215 self.definition_widget,
2216 self.system_id_widget,
2217 self.prediction_widget,
2218 self.run_widget,
2219 ]:
2220 try:
2221 widget = getattr(parent, data)
2222 break
2223 except AttributeError:
2224 continue
2225 if widget is None:
2226 raise ValueError(f"Cannot Enable Widget {data}: not found in UI")
2227 widget.setEnabled(True)
2228 elif message == "disable":
2229 widget = None
2230 for parent in [
2231 self.definition_widget,
2232 self.system_id_widget,
2233 self.prediction_widget,
2234 self.run_widget,
2235 ]:
2236 try:
2237 widget = getattr(parent, data)
2238 break
2239 except AttributeError:
2240 continue
2241 if widget is None:
2242 raise ValueError(f"Cannot Disable Widget {data}: not found in UI")
2243 widget.setEnabled(False)
2244 else:
2245 widget = None
2246 for parent in [
2247 self.definition_widget,
2248 self.system_id_widget,
2249 self.prediction_widget,
2250 self.run_widget,
2251 ]:
2252 try:
2253 widget = getattr(parent, message)
2254 break
2255 except AttributeError:
2256 continue
2257 if widget is None:
2258 raise ValueError(f"Cannot Update Widget {message}: not found in UI")
2259 if isinstance(widget, QtWidgets.QDoubleSpinBox):
2260 widget.setValue(data)
2261 elif isinstance(widget, QtWidgets.QSpinBox):
2262 widget.setValue(data)
2263 elif isinstance(widget, QtWidgets.QLineEdit):
2264 widget.setText(data)
2265 elif isinstance(widget, QtWidgets.QListWidget):
2266 widget.clear()
2267 widget.addItems([f"{d:.3f}" for d in data])
2268
2269 @staticmethod
2270 def create_environment_template(
2271 environment_name: str, workbook: openpyxl.workbook.workbook.Workbook
2272 ):
2273 """Creates a template worksheet in an Excel workbook defining the
2274 environment.
2275
2276 This function creates a template worksheet in an Excel workbook that
2277 when filled out could be read by the controller to re-create the
2278 environment.
2279
2280 This function is the "write" counterpart to the
2281 ``set_parameters_from_template`` function in the ``RandomVibrationUI`` class,
2282 which reads the values from the template file to populate the user
2283 interface.
2284
2285 Parameters
2286 ----------
2287 environment_name : str :
2288 The name of the environment that will specify the worksheet's name
2289 workbook : openpyxl.workbook.workbook.Workbook :
2290 A reference to an ``openpyxl`` workbook.
2291
2292 """
2293 worksheet = workbook.create_sheet(environment_name)
2294 worksheet.cell(1, 1, "Control Type")
2295 worksheet.cell(1, 2, "Random")
2296 worksheet.cell(2, 1, "Samples Per Frame:")
2297 worksheet.cell(2, 2, "# Number of Samples per Measurement Frame")
2298 worksheet.cell(3, 1, "Test Level Ramp Time:")
2299 worksheet.cell(3, 2, "# Time taken to Ramp between test levels")
2300 worksheet.cell(4, 1, "COLA Window:")
2301 worksheet.cell(4, 2, "# Window used for Constant Overlap and Add process")
2302 worksheet.cell(5, 1, "COLA Overlap %:")
2303 worksheet.cell(5, 2, "# Overlap used in Constant Overlap and Add process")
2304 worksheet.cell(6, 1, "COLA Window Exponent:")
2305 worksheet.cell(
2306 6,
2307 2,
2308 "# Exponent Applied to the COLA Window (use 0.5 unless you "
2309 "are sure you don't want to!)",
2310 )
2311 worksheet.cell(7, 1, "Update System ID During Control:")
2312 worksheet.cell(
2313 7,
2314 2,
2315 "# Continue updating transfer function while the controller is controlling (Y/N)",
2316 )
2317 worksheet.cell(8, 1, "Frames in CPSD:")
2318 worksheet.cell(8, 2, "# Frames used to compute the CPSD matrix")
2319 worksheet.cell(9, 1, "CPSD Window:")
2320 worksheet.cell(9, 2, "# Window used to compute the CPSD matrix")
2321 worksheet.cell(10, 1, "CPSD Overlap %:")
2322 worksheet.cell(10, 2, "# Overlap percentage for CPSD calculations")
2323 worksheet.cell(11, 1, "Allow Automatic Aborts")
2324 worksheet.cell(12, 1, "Control Python Script:")
2325 worksheet.cell(12, 2, "# Path to the Python script containing the control law")
2326 worksheet.cell(13, 1, "Control Python Function:")
2327 worksheet.cell(
2328 13,
2329 2,
2330 "# Function or class name within the Python Script that will serve as the control law",
2331 )
2332 worksheet.cell(14, 1, "Control Parameters:")
2333 worksheet.cell(14, 2, "# Extra parameters used in the control law")
2334 worksheet.cell(15, 1, "Control Channels (1-based):")
2335 worksheet.cell(16, 1, "System ID Averaging:")
2336 worksheet.cell(
2337 16,
2338 2,
2339 "# Averaging Type used for system ID. Should be Linear or Exponential",
2340 )
2341 worksheet.cell(17, 1, "Noise Averages:")
2342 worksheet.cell(17, 2, "# Number of Averages used when characterizing noise")
2343 worksheet.cell(18, 1, "System ID Averages:")
2344 worksheet.cell(18, 2, "# Number of Averages used when computing the FRF")
2345 worksheet.cell(19, 1, "Exponential Averaging Coefficient:")
2346 worksheet.cell(19, 2, "# Averaging Coefficient for Exponential Averaging (if used)")
2347 worksheet.cell(20, 1, "System ID Estimator:")
2348 worksheet.cell(
2349 20,
2350 2,
2351 "# Technique used to compute system ID. Should be one of H1, H2, H3, or Hv.",
2352 )
2353 worksheet.cell(21, 1, "System ID Level (V RMS):")
2354 worksheet.cell(
2355 21,
2356 2,
2357 "# RMS Value of Flat Voltage Spectrum used for System Identification.",
2358 )
2359 worksheet.cell(22, 1, "System ID Signal Type:")
2360 worksheet.cell(23, 1, "System ID Window:")
2361 worksheet.cell(
2362 23,
2363 2,
2364 "# Window used to compute FRFs during system ID. Should be one of Hann or None",
2365 )
2366 worksheet.cell(24, 1, "System ID Overlap %:")
2367 worksheet.cell(24, 2, "# Overlap to use in the system identification")
2368 worksheet.cell(25, 1, "System ID Burst On %:")
2369 worksheet.cell(25, 2, "# Percentage of a frame that the burst random is on for")
2370 worksheet.cell(26, 1, "System ID Burst Pretrigger %:")
2371 worksheet.cell(
2372 26,
2373 2,
2374 "# Percentage of a frame that occurs before the burst starts in a burst random signal",
2375 )
2376 worksheet.cell(27, 1, "System ID Ramp Fraction %:")
2377 worksheet.cell(
2378 27,
2379 2,
2380 '# Percentage of the "System ID Burst On %" that will be used to ramp up to full level',
2381 )
2382 worksheet.cell(28, 1, "Specification File:")
2383 worksheet.cell(28, 2, "# Path to the file containing the Specification")
2384 worksheet.cell(29, 1, "Response Transformation Matrix:")
2385 worksheet.cell(
2386 29,
2387 2,
2388 "# Transformation matrix to apply to the response channels. Type None if there "
2389 "is none. Otherwise, make this a 2D array in the spreadsheet and move the Output "
2390 "Transformation Matrix line down so it will fit. The number of columns should be the "
2391 "number of physical control channels.",
2392 )
2393 worksheet.cell(30, 1, "Output Transformation Matrix:")
2394 worksheet.cell(
2395 30,
2396 2,
2397 "# Transformation matrix to apply to the outputs. Type None if there is none. "
2398 "Otherwise, make this a 2D array in the spreadsheet. The number of columns should be "
2399 "the number of physical output channels in the environment.",
2400 )
2401
2402 def set_parameters_from_template(self, worksheet: openpyxl.worksheet.worksheet.Worksheet):
2403 """
2404 Collects parameters for the user interface from the Excel template file
2405
2406 This function reads a filled out template worksheet to create an
2407 environment. Cells on this worksheet contain parameters needed to
2408 specify the environment, so this function should read those cells and
2409 update the UI widgets with those parameters.
2410
2411 This function is the "read" counterpart to the
2412 ``create_environment_template`` function in the ``RandomVibrationUI`` class,
2413 which writes a template file that can be filled out by a user.
2414
2415
2416 Parameters
2417 ----------
2418 worksheet : openpyxl.worksheet.worksheet.Worksheet
2419 An openpyxl worksheet that contains the environment template.
2420 Cells on this worksheet should contain the parameters needed for the
2421 user interface.
2422
2423 """
2424 self.definition_widget.samples_per_frame_selector.setValue(int(worksheet.cell(2, 2).value))
2425 self.definition_widget.ramp_time_spinbox.setValue(float(worksheet.cell(3, 2).value))
2426 self.definition_widget.cola_window_selector.setCurrentIndex(
2427 self.definition_widget.cola_window_selector.findText(worksheet.cell(4, 2).value)
2428 )
2429 self.definition_widget.cola_overlap_percentage_selector.setValue(
2430 float(worksheet.cell(5, 2).value)
2431 )
2432 self.definition_widget.cola_exponent_selector.setValue(float(worksheet.cell(6, 2).value))
2433 self.definition_widget.update_transfer_function_during_control_selector.setChecked(
2434 worksheet.cell(7, 2).value.upper() == "Y"
2435 )
2436 self.definition_widget.cpsd_frames_selector.setValue(int(worksheet.cell(8, 2).value))
2437 self.definition_widget.cpsd_computation_window_selector.setCurrentIndex(
2438 self.definition_widget.cpsd_computation_window_selector.findText(
2439 worksheet.cell(9, 2).value
2440 )
2441 )
2442 self.definition_widget.cpsd_overlap_selector.setValue(float(worksheet.cell(10, 2).value))
2443 self.definition_widget.auto_abort_checkbox.setChecked(
2444 worksheet.cell(11, 2).value.upper() == "Y"
2445 )
2446 self.select_python_module(None, worksheet.cell(12, 2).value)
2447 self.definition_widget.control_function_input.setCurrentIndex(
2448 self.definition_widget.control_function_input.findText(worksheet.cell(13, 2).value)
2449 )
2450 self.definition_widget.control_parameters_text_input.setText(
2451 "" if worksheet.cell(14, 2).value is None else str(worksheet.cell(14, 2).value)
2452 )
2453 column_index = 2
2454 while True:
2455 value = worksheet.cell(15, column_index).value
2456 if value is None or (isinstance(value, str) and value.strip() == ""):
2457 break
2458 item = self.definition_widget.control_channels_selector.item(int(value) - 1)
2459 item.setCheckState(Qt.Checked)
2460 column_index += 1
2461 self.system_id_widget.averagingTypeComboBox.setCurrentIndex(
2462 self.system_id_widget.averagingTypeComboBox.findText(worksheet.cell(16, 2).value)
2463 )
2464 self.system_id_widget.noiseAveragesSpinBox.setValue(int(worksheet.cell(17, 2).value))
2465 self.system_id_widget.systemIDAveragesSpinBox.setValue(int(worksheet.cell(18, 2).value))
2466 self.system_id_widget.averagingCoefficientDoubleSpinBox.setValue(
2467 float(worksheet.cell(19, 2).value)
2468 )
2469 self.system_id_widget.estimatorComboBox.setCurrentIndex(
2470 self.system_id_widget.estimatorComboBox.findText(worksheet.cell(20, 2).value)
2471 )
2472 self.system_id_widget.levelDoubleSpinBox.setValue(float(worksheet.cell(21, 2).value))
2473 # this should be a temporary solution - template file rework needed
2474 low, high = worksheet.cell(21, 3).value, worksheet.cell(21, 4).value
2475 sigma = worksheet.cell(21, 5).value
2476 if low is not None:
2477 self.system_id_widget.lowFreqCutoffSpinBox.setValue(int(low))
2478 if high is not None:
2479 self.system_id_widget.highFreqCutoffSpinBox.setValue(int(high))
2480 if sigma is not None:
2481 self.definition_widget.sigma_clipping_selector.setValue(
2482 float(sigma)
2483 ) # TODO: sigma clipping and bandwidths should get
2484 # their own rows, but how to maintain backward compatibility?
2485 self.system_id_widget.signalTypeComboBox.setCurrentIndex(
2486 self.system_id_widget.signalTypeComboBox.findText(worksheet.cell(22, 2).value)
2487 )
2488 self.system_id_widget.windowComboBox.setCurrentIndex(
2489 self.system_id_widget.windowComboBox.findText(worksheet.cell(23, 2).value)
2490 )
2491 self.system_id_widget.overlapDoubleSpinBox.setValue(float(worksheet.cell(24, 2).value))
2492 self.system_id_widget.onFractionDoubleSpinBox.setValue(float(worksheet.cell(25, 2).value))
2493 self.system_id_widget.pretriggerDoubleSpinBox.setValue(float(worksheet.cell(26, 2).value))
2494 self.system_id_widget.rampFractionDoubleSpinBox.setValue(float(worksheet.cell(27, 2).value))
2495
2496 # Now we need to find the transformation matrices' sizes
2497 response_channels = self.definition_widget.control_channels_display.value()
2498 output_channels = self.definition_widget.output_channels_display.value()
2499 output_transform_row = 30
2500 if (
2501 isinstance(worksheet.cell(29, 2).value, str)
2502 and worksheet.cell(29, 2).value.lower() == "none"
2503 ):
2504 self.response_transformation_matrix = None
2505 else:
2506 while True:
2507 if worksheet.cell(output_transform_row, 1).value == "Output Transformation Matrix:":
2508 break
2509 output_transform_row += 1
2510 response_size = output_transform_row - 29
2511 response_transformation = []
2512 for i in range(response_size):
2513 response_transformation.append([])
2514 for j in range(response_channels):
2515 response_transformation[-1].append(float(worksheet.cell(29 + i, 2 + j).value))
2516 self.response_transformation_matrix = np.array(response_transformation)
2517 if (
2518 isinstance(worksheet.cell(output_transform_row, 2).value, str)
2519 and worksheet.cell(output_transform_row, 2).value.lower() == "none"
2520 ):
2521 self.output_transformation_matrix = None
2522 else:
2523 output_transformation = []
2524 i = 0
2525 while True:
2526 if worksheet.cell(output_transform_row + i, 2).value is None or (
2527 isinstance(worksheet.cell(output_transform_row + i, 2).value, str)
2528 and worksheet.cell(output_transform_row + i, 2).value.strip() == ""
2529 ):
2530 break
2531 output_transformation.append([])
2532 for j in range(output_channels):
2533 output_transformation[-1].append(
2534 float(worksheet.cell(output_transform_row + i, 2 + j).value)
2535 )
2536 i += 1
2537 self.output_transformation_matrix = np.array(output_transformation)
2538 self.define_transformation_matrices(None, dialog=False)
2539 self.select_spec_file(None, worksheet.cell(28, 2).value)
2540
2541
2542# %% Environment
2543
2544
2545class RandomVibrationEnvironment(AbstractSysIdEnvironment):
2546 """Random Environment class defining the interface with the controller"""
2547
2548 def __init__(
2549 self,
2550 environment_name: str,
2551 queue_container: RandomVibrationQueues,
2552 acquisition_active: mp.sharedctypes.Synchronized,
2553 output_active: mp.sharedctypes.Synchronized,
2554 ):
2555 """
2556 Random Vibration Environment Constructor that fills out the ``command_map``
2557
2558 Parameters
2559 ----------
2560 environment_name : str
2561 Name of the environment.
2562 queue_container : RandomVibrationQueues
2563 Container of queues used by the Random Vibration Environment.
2564
2565 """
2566 super().__init__(
2567 environment_name,
2568 queue_container.environment_command_queue,
2569 queue_container.gui_update_queue,
2570 queue_container.controller_communication_queue,
2571 queue_container.log_file_queue,
2572 queue_container.collector_command_queue,
2573 queue_container.signal_generation_command_queue,
2574 queue_container.spectral_command_queue,
2575 queue_container.data_analysis_command_queue,
2576 queue_container.data_in_queue,
2577 queue_container.data_out_queue,
2578 acquisition_active,
2579 output_active,
2580 )
2581 self.map_command(RandomVibrationCommands.START_CONTROL, self.start_control)
2582 self.map_command(RandomVibrationCommands.STOP_CONTROL, self.stop_environment)
2583 self.map_command(RandomVibrationCommands.ADJUST_TEST_LEVEL, self.adjust_test_level)
2584 self.map_command(
2585 RandomVibrationCommands.CHECK_FOR_COMPLETE_SHUTDOWN,
2586 self.check_for_control_shutdown,
2587 )
2588 self.map_command(RandomVibrationCommands.RECOMPUTE_PREDICTION, self.recompute_prediction)
2589 self.map_command(
2590 GlobalCommands.UPDATE_INTERACTIVE_CONTROL_PARAMETERS,
2591 self.update_interactive_control_parameters,
2592 )
2593 self.map_command(GlobalCommands.SEND_INTERACTIVE_COMMAND, self.send_interactive_command)
2594 self.queue_container = queue_container
2595
2596 def initialize_environment_test_parameters(
2597 self, environment_parameters: RandomVibrationMetadata
2598 ):
2599 """
2600 Initialize the environment parameters specific to this environment
2601
2602 The environment will recieve parameters defining itself from the
2603 user interface and must set itself up accordingly.
2604
2605 Parameters
2606 ----------
2607 environment_parameters : RandomVibrationMetadata
2608 A container containing the parameters defining the environment
2609
2610 """
2611 super().initialize_environment_test_parameters(environment_parameters)
2612
2613 # Set up the collector
2614 self.queue_container.collector_command_queue.put(
2615 self.environment_name,
2616 (
2617 DataCollectorCommands.INITIALIZE_COLLECTOR,
2618 self.get_data_collector_metadata(),
2619 ),
2620 )
2621 # Set up the signal generation
2622 self.queue_container.signal_generation_command_queue.put(
2623 self.environment_name,
2624 (
2625 SignalGenerationCommands.INITIALIZE_PARAMETERS,
2626 self.get_signal_generation_metadata(),
2627 ),
2628 )
2629 # Set up the spectral processing
2630 self.queue_container.spectral_command_queue.put(
2631 self.environment_name,
2632 (
2633 SpectralProcessingCommands.INITIALIZE_PARAMETERS,
2634 self.get_spectral_processing_metadata(),
2635 ),
2636 )
2637 # Set up the data analysis
2638 self.queue_container.data_analysis_command_queue.put(
2639 self.environment_name,
2640 (
2641 RandomVibrationDataAnalysisCommands.INITIALIZE_PARAMETERS,
2642 self.environment_parameters,
2643 ),
2644 )
2645
2646 def update_interactive_control_parameters(self, parameters):
2647 """Sends updated parameters to the interactive control law on the data analysis process"""
2648 self.queue_container.data_analysis_command_queue.put(
2649 self.environment_name,
2650 (GlobalCommands.UPDATE_INTERACTIVE_CONTROL_PARAMETERS, parameters),
2651 )
2652
2653 def send_interactive_command(self, command):
2654 """General method that can be used by an interactive UI object to pass commands and data to
2655 its corresponding computation object"""
2656 if self.environment_parameters.control_python_function_type == 3: # Interactive
2657 self.queue_container.data_analysis_command_queue.put(
2658 self.environment_name,
2659 (GlobalCommands.SEND_INTERACTIVE_COMMAND, command),
2660 )
2661 else:
2662 raise ValueError(
2663 "Received an SEND_INTERACTIVE_COMMAND signal without an interactive control law. "
2664 "How did this happen?"
2665 )
2666
2667 def system_id_complete(self, data):
2668 """Triggered when system identification has been completed, starting control predictions"""
2669 super().system_id_complete(data)
2670 self.queue_container.data_analysis_command_queue.put(
2671 self.environment_name,
2672 (RandomVibrationDataAnalysisCommands.PERFORM_CONTROL_PREDICTION, None),
2673 )
2674
2675 def get_data_collector_metadata(self):
2676 """Gets relevant metadata for the data collector process"""
2677 num_channels = self.environment_parameters.number_of_channels
2678 response_channel_indices = self.environment_parameters.response_channel_indices
2679 reference_channel_indices = self.environment_parameters.reference_channel_indices
2680 acquisition_type = AcquisitionType.FREE_RUN
2681 acceptance = Acceptance.AUTOMATIC
2682 acceptance_function = None
2683 overlap_fraction = self.environment_parameters.cpsd_overlap
2684 trigger_channel_index = 0
2685 trigger_slope = TriggerSlope.POSITIVE
2686 trigger_level = 0
2687 trigger_hysteresis = 0
2688 trigger_hysteresis_samples = 0
2689 pretrigger_fraction = 0
2690 frame_size = self.environment_parameters.samples_per_frame
2691 window = Window.HANN if self.environment_parameters.cpsd_window == "Hann" else None
2692 # use number of sysid averages as kurtosis buffer size
2693 # (could maybe make this match the test duration if user is using the "Time at Level"
2694 # function, would need to pass info from the RandomVibrationUI object)
2695 kurtosis_buffer_length = self.environment_parameters.sysid_averages
2696
2697 return CollectorMetadata(
2698 num_channels,
2699 response_channel_indices,
2700 reference_channel_indices,
2701 acquisition_type,
2702 acceptance,
2703 acceptance_function,
2704 overlap_fraction,
2705 trigger_channel_index,
2706 trigger_slope,
2707 trigger_level,
2708 trigger_hysteresis,
2709 trigger_hysteresis_samples,
2710 pretrigger_fraction,
2711 frame_size,
2712 window,
2713 kurtosis_buffer_length=kurtosis_buffer_length,
2714 response_transformation_matrix=self.environment_parameters.response_transformation_matrix,
2715 reference_transformation_matrix=self.environment_parameters.reference_transformation_matrix,
2716 )
2717
2718 def get_signal_generation_metadata(self):
2719 """Gets relevant metadata for the signal generation process"""
2720 return SignalGenerationMetadata(
2721 samples_per_write=self.data_acquisition_parameters.samples_per_write,
2722 level_ramp_samples=self.environment_parameters.test_level_ramp_time
2723 * self.environment_parameters.sample_rate
2724 * self.data_acquisition_parameters.output_oversample,
2725 output_transformation_matrix=self.environment_parameters.reference_transformation_matrix,
2726 )
2727
2728 def get_signal_generator(self):
2729 """Gets the signal generator object that will generate signals for the environment"""
2730 return CPSDSignalGenerator(
2731 self.environment_parameters.sample_rate,
2732 self.environment_parameters.samples_per_frame,
2733 self.environment_parameters.num_reference_channels,
2734 None,
2735 self.environment_parameters.cola_overlap,
2736 self.environment_parameters.cola_window,
2737 self.environment_parameters.cola_window_exponent,
2738 self.environment_parameters.sigma_clip,
2739 self.data_acquisition_parameters.output_oversample,
2740 )
2741
2742 def get_spectral_processing_metadata(self):
2743 """Gets the required metadata for the spectral processing process"""
2744 averaging_type = AveragingTypes.LINEAR
2745 averages = self.environment_parameters.frames_in_cpsd
2746 exponential_averaging_coefficient = 0
2747 if self.environment_parameters.sysid_estimator == "H1":
2748 frf_estimator = Estimator.H1
2749 elif self.environment_parameters.sysid_estimator == "H2":
2750 frf_estimator = Estimator.H2
2751 elif self.environment_parameters.sysid_estimator == "H3":
2752 frf_estimator = Estimator.H3
2753 elif self.environment_parameters.sysid_estimator == "Hv":
2754 frf_estimator = Estimator.HV
2755 else:
2756 raise ValueError(f"Invalid FRF Estimator {self.environment_parameters.sysid_estimator}")
2757 num_response_channels = self.environment_parameters.num_response_channels
2758 num_reference_channels = self.environment_parameters.num_reference_channels
2759 frequency_spacing = self.environment_parameters.frequency_spacing
2760 sample_rate = self.environment_parameters.sample_rate
2761 num_frequency_lines = self.environment_parameters.fft_lines
2762 return SpectralProcessingMetadata(
2763 averaging_type,
2764 averages,
2765 exponential_averaging_coefficient,
2766 frf_estimator,
2767 num_response_channels,
2768 num_reference_channels,
2769 frequency_spacing,
2770 sample_rate,
2771 num_frequency_lines,
2772 )
2773
2774 def recompute_prediction(self, data): # pylint: disable=unused-argument
2775 """Sends a signal to the data analysis process to recompute test predictions"""
2776 self.queue_container.data_analysis_command_queue.put(
2777 self.environment_name,
2778 (RandomVibrationDataAnalysisCommands.PERFORM_CONTROL_PREDICTION, None),
2779 )
2780
2781 def start_control(self, data):
2782 """Starts the environment at the specified test level"""
2783 self.log("Starting Control")
2784 self.siggen_shutdown_achieved = False
2785 self.collector_shutdown_achieved = False
2786 self.spectral_shutdown_achieved = False
2787 self.analysis_shutdown_achieved = False
2788 self.queue_container.controller_communication_queue.put(
2789 self.environment_name,
2790 (GlobalCommands.START_ENVIRONMENT, self.environment_name),
2791 )
2792 # Set up the collector
2793 self.queue_container.collector_command_queue.put(
2794 self.environment_name,
2795 (
2796 DataCollectorCommands.INITIALIZE_COLLECTOR,
2797 self.get_data_collector_metadata(),
2798 ),
2799 )
2800
2801 self.queue_container.collector_command_queue.put(
2802 self.environment_name,
2803 (
2804 DataCollectorCommands.SET_TEST_LEVEL,
2805 (self.environment_parameters.skip_frames, data),
2806 ),
2807 )
2808 time.sleep(0.01)
2809
2810 # Set up the signal generation
2811 self.queue_container.signal_generation_command_queue.put(
2812 self.environment_name,
2813 (
2814 SignalGenerationCommands.INITIALIZE_PARAMETERS,
2815 self.get_signal_generation_metadata(),
2816 ),
2817 )
2818
2819 self.queue_container.signal_generation_command_queue.put(
2820 self.environment_name,
2821 (
2822 SignalGenerationCommands.INITIALIZE_SIGNAL_GENERATOR,
2823 self.get_signal_generator(),
2824 ),
2825 )
2826
2827 self.queue_container.signal_generation_command_queue.put(
2828 self.environment_name, (SignalGenerationCommands.MUTE, None)
2829 )
2830
2831 self.queue_container.signal_generation_command_queue.put(
2832 self.environment_name, (SignalGenerationCommands.ADJUST_TEST_LEVEL, data)
2833 )
2834
2835 # Tell the collector to start acquiring data
2836 self.queue_container.collector_command_queue.put(
2837 self.environment_name, (DataCollectorCommands.ACQUIRE, None)
2838 )
2839
2840 # Tell the signal generation to start generating signals
2841 self.queue_container.signal_generation_command_queue.put(
2842 self.environment_name, (SignalGenerationCommands.GENERATE_SIGNALS, None)
2843 )
2844
2845 # # Set up the data analysis
2846 # self.queue_container.data_analysis_command_queue.put(
2847 # self.environment_name,
2848 # (RandomVibrationDataAnalysisCommands.INITIALIZE_PARAMETERS,
2849 # self.environment_parameters))
2850
2851 # Start the data analysis running
2852 self.queue_container.data_analysis_command_queue.put(
2853 self.environment_name,
2854 (RandomVibrationDataAnalysisCommands.RUN_CONTROL, None),
2855 )
2856
2857 # Set up the spectral processing
2858 self.queue_container.spectral_command_queue.put(
2859 self.environment_name,
2860 (
2861 SpectralProcessingCommands.INITIALIZE_PARAMETERS,
2862 self.get_spectral_processing_metadata(),
2863 ),
2864 )
2865
2866 # Tell the spectral analysis to clear and start acquiring
2867 self.queue_container.spectral_command_queue.put(
2868 self.environment_name,
2869 (SpectralProcessingCommands.CLEAR_SPECTRAL_PROCESSING, None),
2870 )
2871
2872 self.queue_container.spectral_command_queue.put(
2873 self.environment_name,
2874 (SpectralProcessingCommands.RUN_SPECTRAL_PROCESSING, None),
2875 )
2876
2877 def stop_environment(self, data):
2878 """Stop the environment gracefully
2879
2880 This function defines the operations to shut down the environment
2881 gracefully so there is no hard stop that might damage test equipment
2882 or parts.
2883
2884 Parameters
2885 ----------
2886 data : Ignored
2887 This parameter is not used by the function but must be present
2888 due to the calling signature of functions called through the
2889 ``command_map``
2890
2891 """
2892 self.log("Stopping Control")
2893 self.queue_container.collector_command_queue.put(
2894 self.environment_name,
2895 (
2896 DataCollectorCommands.SET_TEST_LEVEL,
2897 (self.environment_parameters.skip_frames * 10, 1),
2898 ),
2899 )
2900 self.queue_container.signal_generation_command_queue.put(
2901 self.environment_name, (SignalGenerationCommands.START_SHUTDOWN, None)
2902 )
2903 self.queue_container.spectral_command_queue.put(
2904 self.environment_name,
2905 (SpectralProcessingCommands.STOP_SPECTRAL_PROCESSING, None),
2906 )
2907 self.queue_container.data_analysis_command_queue.put(
2908 self.environment_name,
2909 (RandomVibrationDataAnalysisCommands.STOP_CONTROL, None),
2910 )
2911 self.queue_container.environment_command_queue.put(
2912 self.environment_name,
2913 (RandomVibrationCommands.CHECK_FOR_COMPLETE_SHUTDOWN, None),
2914 )
2915
2916 def check_for_control_shutdown(self, data): # pylint: disable=unused-argument
2917 """Checks the different processes to see if the controller has shut down gracefully"""
2918 if (
2919 self.siggen_shutdown_achieved
2920 and self.collector_shutdown_achieved
2921 and self.spectral_shutdown_achieved
2922 and self.analysis_shutdown_achieved
2923 ):
2924 self.log("Shutdown Achieved")
2925 self.gui_update_queue.put((self.environment_name, ("enable_control", None)))
2926 else:
2927 # Recheck some time later
2928 time.sleep(1)
2929 self.environment_command_queue.put(
2930 self.environment_name,
2931 (RandomVibrationCommands.CHECK_FOR_COMPLETE_SHUTDOWN, None),
2932 )
2933
2934 def adjust_test_level(self, data):
2935 """Adjusts the test level of the environment to the specified level"""
2936 self.queue_container.signal_generation_command_queue.put(
2937 self.environment_name, (SignalGenerationCommands.ADJUST_TEST_LEVEL, data)
2938 )
2939 self.queue_container.collector_command_queue.put(
2940 self.environment_name,
2941 (
2942 DataCollectorCommands.SET_TEST_LEVEL,
2943 (self.environment_parameters.skip_frames, data),
2944 ),
2945 )
2946
2947 def quit(self, data):
2948 """Closes down the environment permanently as the software is exiting"""
2949 for queue in [
2950 self.queue_container.spectral_command_queue,
2951 self.queue_container.data_analysis_command_queue,
2952 self.queue_container.signal_generation_command_queue,
2953 self.queue_container.collector_command_queue,
2954 ]:
2955 queue.put(self.environment_name, (GlobalCommands.QUIT, None))
2956 # Return true to stop the task
2957 return True
2958
2959
2960# %% Process
2961
2962
2963def random_vibration_process(
2964 environment_name: str,
2965 input_queue: VerboseMessageQueue,
2966 gui_update_queue: Queue,
2967 controller_communication_queue: VerboseMessageQueue,
2968 log_file_queue: Queue,
2969 data_in_queue: Queue,
2970 data_out_queue: Queue,
2971 acquisition_active: mp.sharedctypes.Synchronized,
2972 output_active: mp.sharedctypes.Synchronized,
2973):
2974 """Random vibration environment process function called by multiprocessing
2975
2976 This function defines the Random Vibration Environment process that
2977 gets run by the multiprocessing module when it creates a new process. It
2978 creates a RandomVibrationEnvironment object and runs it.
2979
2980 Parameters
2981 ----------
2982 environment_name : str :
2983 Name of the environment
2984 input_queue : VerboseMessageQueue :
2985 Queue containing instructions for the environment
2986 gui_update_queue : Queue :
2987 Queue where GUI updates are put
2988 controller_communication_queue : Queue :
2989 Queue for global communications with the controller
2990 log_file_queue : Queue :
2991 Queue for writing log file messages
2992 data_in_queue : Queue :
2993 Queue from which data will be read by the environment
2994 data_out_queue : Queue :
2995 Queue to which data will be written that will be output by the hardware.
2996 acquisition_active : mp.sharedctypes.Synchronized
2997 A synchronized value that indicates when the acquisition is active
2998 output_active : mp.sharedctypes.Synchronized
2999 A synchronized value that indicates when the output is active
3000 """
3001 # Create vibration queues
3002 queue_container = RandomVibrationQueues(
3003 environment_name,
3004 input_queue,
3005 gui_update_queue,
3006 controller_communication_queue,
3007 data_in_queue,
3008 data_out_queue,
3009 log_file_queue,
3010 )
3011
3012 spectral_proc = mp.Process(
3013 target=spectral_processing_process,
3014 args=(
3015 environment_name,
3016 queue_container.spectral_command_queue,
3017 queue_container.data_for_spectral_computation_queue,
3018 queue_container.updated_spectral_quantities_queue,
3019 queue_container.environment_command_queue,
3020 queue_container.gui_update_queue,
3021 queue_container.log_file_queue,
3022 ),
3023 )
3024 spectral_proc.start()
3025 analysis_proc = mp.Process(
3026 target=random_data_analysis_process,
3027 args=(
3028 environment_name,
3029 queue_container.data_analysis_command_queue,
3030 queue_container.updated_spectral_quantities_queue,
3031 queue_container.cpsd_to_generate_queue,
3032 queue_container.environment_command_queue,
3033 queue_container.gui_update_queue,
3034 queue_container.log_file_queue,
3035 ),
3036 )
3037 analysis_proc.start()
3038 siggen_proc = mp.Process(
3039 target=signal_generation_process,
3040 args=(
3041 environment_name,
3042 queue_container.signal_generation_command_queue,
3043 queue_container.cpsd_to_generate_queue,
3044 queue_container.data_out_queue,
3045 queue_container.environment_command_queue,
3046 queue_container.log_file_queue,
3047 queue_container.gui_update_queue,
3048 ),
3049 )
3050 siggen_proc.start()
3051 collection_proc = mp.Process(
3052 target=data_collector_process,
3053 args=(
3054 environment_name,
3055 queue_container.collector_command_queue,
3056 queue_container.data_in_queue,
3057 [queue_container.data_for_spectral_computation_queue],
3058 queue_container.environment_command_queue,
3059 queue_container.log_file_queue,
3060 queue_container.gui_update_queue,
3061 ),
3062 )
3063
3064 collection_proc.start()
3065 process_class = RandomVibrationEnvironment(
3066 environment_name, queue_container, acquisition_active, output_active
3067 )
3068 process_class.run()
3069
3070 # Rejoin all the processes
3071 process_class.log("Joining Subprocesses")
3072 process_class.log("Joining Spectral Computation")
3073 spectral_proc.join()
3074 process_class.log("Joining Data Analysis")
3075 analysis_proc.join()
3076 process_class.log("Joining Signal Generation")
3077 siggen_proc.join()
3078 process_class.log("Joining Data Collection")
3079 collection_proc.join()