1# -*- coding: utf-8 -*-
2"""
3Abstract environment that can be used to create new environment control strategies
4in the controller that use system identification.
5
6Rattlesnake Vibration Control Software
7Copyright (C) 2021 National Technology & Engineering Solutions of Sandia, LLC
8(NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S.
9Government retains certain rights in this software.
10
11This program is free software: you can redistribute it and/or modify
12it under the terms of the GNU General Public License as published by
13the Free Software Foundation, either version 3 of the License, or
14(at your option) any later version.
15
16This program is distributed in the hope that it will be useful,
17but WITHOUT ANY WARRANTY; without even the implied warranty of
18MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19GNU General Public License for more details.
20
21You should have received a copy of the GNU General Public License
22along with this program. If not, see <https://www.gnu.org/licenses/>.
23"""
24
25import multiprocessing as mp
26import multiprocessing.sharedctypes # pylint: disable=unused-import
27import time
28from abc import abstractmethod
29from copy import deepcopy
30from enum import Enum
31from multiprocessing.queues import Queue
32
33import netCDF4 as nc4
34import numpy as np
35import openpyxl
36import pyqtgraph as pg
37from qtpy import QtWidgets, uic
38from scipy.io import loadmat, savemat
39
40from .abstract_environment import AbstractEnvironment, AbstractMetadata, AbstractUI
41from .data_collector import (
42 Acceptance,
43 AcquisitionType,
44 CollectorMetadata,
45 DataCollectorCommands,
46 TriggerSlope,
47 Window,
48)
49from .environments import system_identification_ui_path
50from .signal_generation import (
51 BurstRandomSignalGenerator,
52 ChirpSignalGenerator,
53 PseudorandomSignalGenerator,
54 RandomSignalGenerator,
55 SignalGenerator,
56)
57from .signal_generation_process import (
58 SignalGenerationCommands,
59 SignalGenerationMetadata,
60)
61from .spectral_processing import (
62 AveragingTypes,
63 Estimator,
64 SpectralProcessingCommands,
65 SpectralProcessingMetadata,
66)
67from .utilities import (
68 DataAcquisitionParameters,
69 GlobalCommands,
70 VerboseMessageQueue,
71 error_message_qt,
72)
73
74
75class SystemIdCommands(Enum):
76 """Enumeration of commands that could be sent to the system identification environment"""
77
78 PREVIEW_NOISE = 0
79 PREVIEW_TRANSFER_FUNCTION = 1
80 START_SYSTEM_ID = 2
81 STOP_SYSTEM_ID = 3
82 CHECK_FOR_COMPLETE_SHUTDOWN = 4
83
84
85class AbstractSysIdMetadata(AbstractMetadata):
86 """Abstract class for storing metadata for an environment.
87
88 This class is used as a storage container for parameters used by an
89 environment. It is returned by the environment UI's
90 ``collect_environment_definition_parameters`` function as well as its
91 ``initialize_environment`` function. Various parts of the controller and
92 environment will query the class's data members for parameter values.
93
94 Classes inheriting from AbstractMetadata must define:
95 1. store_to_netcdf - A function defining the way the parameters are
96 stored to a netCDF file saved during streaming operations.
97 """
98
99 def __init__(self):
100 self.sysid_frame_size = None
101 self.sysid_averaging_type = None
102 self.sysid_noise_averages = None
103 self.sysid_averages = None
104 self.sysid_exponential_averaging_coefficient = None
105 self.sysid_estimator = None
106 self.sysid_level = None
107 self.sysid_level_ramp_time = None
108 self.sysid_signal_type = None
109 self.sysid_window = None
110 self.sysid_overlap = None
111 self.sysid_burst_on = None
112 self.sysid_pretrigger = None
113 self.sysid_burst_ramp_fraction = None
114 self.sysid_low_frequency_cutoff = None
115 self.sysid_high_frequency_cutoff = None
116
117 @property
118 @abstractmethod
119 def number_of_channels(self):
120 """Number of channels in the environment"""
121
122 @property
123 @abstractmethod
124 def response_channel_indices(self):
125 """Indices corresponding to the response or control channels in the environment"""
126
127 @property
128 @abstractmethod
129 def reference_channel_indices(self):
130 """Indices corresponding to the excitation channels in the environment"""
131
132 @property
133 def num_response_channels(self):
134 """Gets the total number of control channels including transformation effects"""
135 return (
136 len(self.response_channel_indices)
137 if self.response_transformation_matrix is None
138 else self.response_transformation_matrix.shape[0]
139 )
140
141 @property
142 def num_reference_channels(self):
143 """Gets the total number of excitation channels including transformation effects"""
144 return (
145 len(self.reference_channel_indices)
146 if self.reference_transformation_matrix is None
147 else self.reference_transformation_matrix.shape[0]
148 )
149
150 @property
151 @abstractmethod
152 def response_transformation_matrix(self):
153 """Gets the response transformation matrix"""
154
155 @property
156 @abstractmethod
157 def reference_transformation_matrix(self):
158 """Gets the excitation transformation matrix"""
159
160 @property
161 def sysid_frequency_spacing(self):
162 """Frequency spacing in spectral quantities computed by system identification"""
163 return self.sample_rate / self.sysid_frame_size
164
165 @property
166 @abstractmethod
167 def sample_rate(self):
168 """Sample rate (not oversampled) of the data acquisition system"""
169
170 @property
171 def sysid_fft_lines(self):
172 """Number of frequency lines in the FFT"""
173 return self.sysid_frame_size // 2 + 1
174
175 @property
176 def sysid_skip_frames(self):
177 """Number of frames to skip in the time stream due to ramp time"""
178 return int(
179 np.ceil(
180 self.sysid_level_ramp_time
181 * self.sample_rate
182 / (self.sysid_frame_size * (1 - self.sysid_overlap))
183 )
184 )
185
186 @abstractmethod
187 def store_to_netcdf(
188 self, netcdf_group_handle: nc4._netCDF4.Group # pylint: disable=c-extension-no-member
189 ):
190 """Store parameters to a group in a netCDF streaming file.
191
192 This function stores parameters from the environment into the netCDF
193 file in a group with the environment's name as its name. The function
194 will receive a reference to the group within the dataset and should
195 store the environment's parameters into that group in the form of
196 attributes, dimensions, or variables.
197
198 This function is the "write" counterpart to the retrieve_metadata
199 function in the AbstractUI class, which will read parameters from
200 the netCDF file to populate the parameters in the user interface.
201
202 Parameters
203 ----------
204 netcdf_group_handle : nc4._netCDF4.Group
205 A reference to the Group within the netCDF dataset where the
206 environment's metadata is stored.
207
208 """
209 netcdf_group_handle.sysid_frame_size = self.sysid_frame_size
210 netcdf_group_handle.sysid_averaging_type = self.sysid_averaging_type
211 netcdf_group_handle.sysid_noise_averages = self.sysid_noise_averages
212 netcdf_group_handle.sysid_averages = self.sysid_averages
213 netcdf_group_handle.sysid_exponential_averaging_coefficient = (
214 self.sysid_exponential_averaging_coefficient
215 )
216 netcdf_group_handle.sysid_estimator = self.sysid_estimator
217 netcdf_group_handle.sysid_level = self.sysid_level
218 netcdf_group_handle.sysid_level_ramp_time = self.sysid_level_ramp_time
219 netcdf_group_handle.sysid_signal_type = self.sysid_signal_type
220 netcdf_group_handle.sysid_window = self.sysid_window
221 netcdf_group_handle.sysid_overlap = self.sysid_overlap
222 netcdf_group_handle.sysid_burst_on = self.sysid_burst_on
223 netcdf_group_handle.sysid_pretrigger = self.sysid_pretrigger
224 netcdf_group_handle.sysid_burst_ramp_fraction = self.sysid_burst_ramp_fraction
225 netcdf_group_handle.sysid_low_frequency_cutoff = self.sysid_low_frequency_cutoff
226 netcdf_group_handle.sysid_high_frequency_cutoff = self.sysid_high_frequency_cutoff
227
228 def __eq__(self, other):
229 try:
230 return np.all(
231 [np.all(value == other.__dict__[field]) for field, value in self.__dict__.items()]
232 )
233 except (AttributeError, KeyError):
234 return False
235
236
237class RotatedAxisItem(pg.AxisItem): # pylint: disable=abstract-method
238 """Plot axis labels that can be rotated by some value"""
239
240 def __init__(self, *args, **kwargs):
241 super().__init__(*args, **kwargs)
242 self._original_height = self.height()
243 self._angle = None
244
245 def setAngle(self, angle): # pylint: disable=invalid-name
246 """Sets the angle and ensures it's between -180 and 180"""
247 self._angle = angle
248 self._angle = (self._angle + 180) % 360 - 180
249
250 def drawPicture(self, p, axisSpec, tickSpecs, textSpecs):
251 """UPdated draw picture method that includes the rotation of the text"""
252 profiler = pg.debug.Profiler()
253 max_width = 0
254
255 # draw long line along axis
256 pen, p1, p2 = axisSpec
257 p.setPen(pen)
258 p.drawLine(p1, p2)
259 # draw ticks
260 for pen, p1, p2 in tickSpecs:
261 p.setPen(pen)
262 p.drawLine(p1, p2)
263 profiler("draw ticks")
264
265 for rect, flags, text in textSpecs:
266 p.save() # save the painter state
267
268 p.translate(rect.center()) # move coordinate system to center of text rect
269 p.rotate(self._angle) # rotate text
270 p.translate(-rect.center()) # revert coordinate system
271
272 x_offset = np.ceil(np.fabs(np.sin(np.radians(self._angle)) * rect.width()))
273 if self._angle < 0:
274 x_offset = -x_offset
275 p.translate(x_offset / 2, 0) # Move the coordinate system (relatively) downwards
276
277 p.drawText(rect, flags, text)
278 p.restore() # restore the painter state
279 offset = np.fabs(x_offset)
280 max_width = offset if max_width < offset else max_width
281
282 profiler("draw text")
283 # Adjust the height
284 self.setHeight(self._original_height + max_width)
285
286 def boundingRect(self):
287 """Sets the bounding rectangle of the item to give more space at the bottom"""
288 rect = super().boundingRect()
289 rect.adjust(0, 0, 0, 20) # Add 20 pixels to bottom
290 return rect
291
292
293from .abstract_sysid_data_analysis import ( # noqa: E402 pylint: disable=wrong-import-position
294 SysIDDataAnalysisCommands,
295)
296
297
298class AbstractSysIdUI(AbstractUI):
299 """Abstract User Interface class defining the interface with the controller
300
301 This class is used to define the interface between the User Interface of a
302 environment in the controller and the main controller."""
303
304 @abstractmethod
305 def __init__(
306 self,
307 environment_name: str,
308 environment_command_queue: VerboseMessageQueue,
309 controller_communication_queue: VerboseMessageQueue,
310 log_file_queue: Queue,
311 system_id_tabwidget: QtWidgets.QTabWidget,
312 ):
313 """
314 Stores data required by the controller to interact with the UI
315
316 This class stores data required by the controller to interact with the
317 user interface for a given environment. This includes the environment
318 name and queues to pass information between the controller and
319 environment. It additionally initializes the ``command_map`` which is
320 used by the Test Profile functionality to map profile instructions to
321 operations on the user interface.
322
323
324 Parameters
325 ----------
326 environment_name : str
327 The name of the environment
328 environment_command_queue : VerboseMessageQueue
329 A queue that will provide instructions to the corresponding
330 environment
331 controller_communication_queue : VerboseMessageQueue
332 The queue that relays global communication messages to the controller
333 log_file_queue : Queue
334 The queue that will be used to put messages to the log file.
335
336
337 """
338 super().__init__(
339 environment_name,
340 environment_command_queue,
341 controller_communication_queue,
342 log_file_queue,
343 )
344 # Add the page to the system id tabwidget
345 self.system_id_widget = QtWidgets.QWidget()
346 uic.loadUi(system_identification_ui_path, self.system_id_widget)
347 system_id_tabwidget.addTab(self.system_id_widget, self.environment_name)
348 self.connect_sysid_callbacks()
349
350 self.data_acquisition_parameters = None
351 self.environment_parameters = None
352 self.frequencies = None
353 self.last_time_response = None
354 self.last_transfer_function = None
355 self.last_response_noise = None
356 self.last_reference_noise = None
357 self.last_response_cpsd = None
358 self.last_reference_cpsd = None
359 self.last_coherence = None
360 self.last_condition = None
361 self.last_kurtosis = None
362
363 self.time_response_plot = self.system_id_widget.time_data_graphicslayout.addPlot(
364 row=0, column=0
365 )
366 self.time_response_plot.setLabel("left", "Response")
367 self.time_response_plot.setLabel("bottom", "Time (s)")
368 self.time_reference_plot = self.system_id_widget.time_data_graphicslayout.addPlot(
369 row=0, column=1
370 )
371 self.time_reference_plot.setLabel("left", "Reference")
372 self.time_reference_plot.setLabel("bottom", "Time (s)")
373 self.level_response_plot = self.system_id_widget.levels_graphicslayout.addPlot(
374 row=0, column=0
375 )
376 self.level_response_plot.setLabel("left", "Response PSD")
377 self.level_response_plot.setLabel("bottom", "Frequency (Hz)")
378 self.level_reference_plot = self.system_id_widget.levels_graphicslayout.addPlot(
379 row=0, column=1
380 )
381 self.level_reference_plot.setLabel("left", "Reference PSD")
382 self.level_reference_plot.setLabel("bottom", "Frequency (Hz)")
383 self.transfer_function_phase_plot = (
384 self.system_id_widget.transfer_function_graphics_layout.addPlot(row=0, column=0)
385 )
386 self.transfer_function_phase_plot.setLabel("left", "Phase")
387 self.transfer_function_phase_plot.setLabel("bottom", "Frequency (Hz)")
388 self.transfer_function_magnitude_plot = (
389 self.system_id_widget.transfer_function_graphics_layout.addPlot(row=0, column=1)
390 )
391 self.transfer_function_magnitude_plot.setLabel("left", "Amplitude")
392 self.transfer_function_magnitude_plot.setLabel("bottom", "Frequency (Hz)")
393 self.impulse_response_plot = self.system_id_widget.impulse_graphicslayout.addPlot(
394 row=0, column=0
395 )
396 self.impulse_response_plot.setLabel("left", "Impulse Response")
397 self.impulse_response_plot.setLabel("bottom", "Time (s)")
398 self.coherence_plot = self.system_id_widget.coherence_graphicslayout.addPlot(
399 row=0, column=0
400 )
401 self.coherence_plot.setLabel("left", "Multiple Coherence")
402 self.coherence_plot.setLabel("bottom", "Frequency (Hz)")
403 self.condition_plot = self.system_id_widget.coherence_graphicslayout.addPlot(
404 row=0, column=1
405 )
406 self.condition_plot.setLabel("left", "Condition Number")
407 self.condition_plot.setLabel("bottom", "Frequency (Hz)")
408 self.coherence_plot.vb.setLimits(yMin=0, yMax=1)
409 self.coherence_plot.vb.disableAutoRange(axis="y")
410 # Set up kurtosis plots
411 self.response_nodes = []
412 self.reference_nodes = []
413 self.all_response_indices = []
414 self.all_reference_indices = []
415 self.kurtosis_response_plot = self.system_id_widget.kurtosis_graphicslayout.addPlot(
416 row=0, column=0
417 )
418 self.kurtosis_reference_plot = self.system_id_widget.kurtosis_graphicslayout.addPlot(
419 row=0, column=1
420 )
421 self.kurtosis_response_plot.setLabel("left", "Response")
422 self.kurtosis_reference_plot.setLabel("left", "Reference")
423 response_axis = RotatedAxisItem("bottom")
424 reference_axis = RotatedAxisItem("bottom")
425 response_axis.setAngle(-60)
426 reference_axis.setAngle(-60)
427 self.kurtosis_response_plot.setAxisItems({"bottom": response_axis})
428 self.kurtosis_reference_plot.setAxisItems({"bottom": reference_axis})
429 for plot in [
430 self.level_response_plot,
431 self.level_reference_plot,
432 self.transfer_function_magnitude_plot,
433 self.condition_plot,
434 ]:
435 plot.setLogMode(False, True)
436 self.show_hide_coherence()
437 self.show_hide_impulse()
438 self.show_hide_levels()
439 self.show_hide_time_data()
440 self.show_hide_transfer_function()
441 self.show_hide_kurtosis()
442
443 def connect_sysid_callbacks(self):
444 """Connects the callback functions to the system identification widgets"""
445 self.system_id_widget.preview_noise_button.clicked.connect(self.preview_noise)
446 self.system_id_widget.preview_system_id_button.clicked.connect(
447 self.preview_transfer_function
448 )
449 self.system_id_widget.start_button.clicked.connect(self.acquire_transfer_function)
450 self.system_id_widget.stop_button.clicked.connect(self.stop_system_id)
451 self.system_id_widget.select_transfer_function_stream_file_button.clicked.connect(
452 self.select_transfer_function_stream_file
453 )
454 self.system_id_widget.response_selector.itemSelectionChanged.connect(
455 self.update_sysid_plots
456 )
457 self.system_id_widget.reference_selector.itemSelectionChanged.connect(
458 self.update_sysid_plots
459 )
460 self.system_id_widget.coherence_checkbox.stateChanged.connect(self.show_hide_coherence)
461 self.system_id_widget.levels_checkbox.stateChanged.connect(self.show_hide_levels)
462 self.system_id_widget.time_data_checkbox.stateChanged.connect(self.show_hide_time_data)
463 self.system_id_widget.impulse_checkbox.stateChanged.connect(self.show_hide_impulse)
464 self.system_id_widget.transfer_function_checkbox.stateChanged.connect(
465 self.show_hide_transfer_function
466 )
467 self.system_id_widget.kurtosis_checkbox.stateChanged.connect(self.show_hide_kurtosis)
468 self.system_id_widget.signalTypeComboBox.currentIndexChanged.connect(
469 self.update_signal_type
470 )
471 self.system_id_widget.save_system_id_matrices_button.clicked.connect(
472 self.save_sysid_matrix_file
473 )
474 self.system_id_widget.load_system_id_matrices_button.clicked.connect(
475 self.load_sysid_matrix_file
476 )
477
478 @abstractmethod
479 def initialize_data_acquisition(self, data_acquisition_parameters: DataAcquisitionParameters):
480 """Update the user interface with data acquisition parameters
481
482 This function is called when the Data Acquisition parameters are
483 initialized. This function should set up the environment user interface
484 accordingly.
485
486 Parameters
487 ----------
488 data_acquisition_parameters : DataAcquisitionParameters :
489 Container containing the data acquisition parameters, including
490 channel table and sampling information.
491
492 """
493 self.log("Initializing Data Acquisition")
494 # Store for later
495 self.data_acquisition_parameters = data_acquisition_parameters
496 self.system_id_widget.highFreqCutoffSpinBox.setMaximum(
497 data_acquisition_parameters.sample_rate // 2
498 )
499 # finish setting up kurtosis plots using node number + direction
500 for i, channel in enumerate(self.data_acquisition_parameters.channel_list):
501 node = channel.node_number + (
502 "" if channel.node_direction is None else channel.node_direction
503 )
504 if channel.feedback_device is None:
505 self.response_nodes.append(node)
506 self.all_response_indices.append(i)
507 else:
508 self.reference_nodes.append(node)
509 self.all_reference_indices.append(i)
510 response_ax = self.kurtosis_response_plot.getAxis("bottom")
511 reference_ax = self.kurtosis_reference_plot.getAxis("bottom")
512 response_ax.setTicks([list(enumerate(self.response_nodes))])
513 reference_ax.setTicks([list(enumerate(self.reference_nodes))])
514 self.system_id_widget.kurtosis_graphicslayout.ci.layout.setColumnStretchFactor(
515 0, len(self.all_response_indices) * 2 + len(self.all_reference_indices)
516 )
517 self.system_id_widget.kurtosis_graphicslayout.ci.layout.setColumnStretchFactor(
518 1, len(self.all_reference_indices) * 2 + len(self.all_response_indices)
519 )
520
521 @abstractmethod
522 def collect_environment_definition_parameters(self) -> AbstractSysIdMetadata:
523 """
524 Collect the parameters from the user interface defining the environment
525
526 Returns
527 -------
528 AbstractSysIdMetadata
529 A metadata or parameters object containing the parameters defining
530 the corresponding environment.
531
532 """
533
534 def update_sysid_metadata(self, metadata: AbstractSysIdMetadata):
535 """Updates the provided system identification metadata based on current UI widget values"""
536 metadata.sysid_frame_size = self.system_id_widget.samplesPerFrameSpinBox.value()
537 metadata.sysid_averaging_type = self.system_id_widget.averagingTypeComboBox.itemText(
538 self.system_id_widget.averagingTypeComboBox.currentIndex()
539 )
540 metadata.sysid_noise_averages = self.system_id_widget.noiseAveragesSpinBox.value()
541 metadata.sysid_averages = self.system_id_widget.systemIDAveragesSpinBox.value()
542 metadata.sysid_exponential_averaging_coefficient = (
543 self.system_id_widget.averagingCoefficientDoubleSpinBox.value()
544 )
545 metadata.sysid_estimator = self.system_id_widget.estimatorComboBox.itemText(
546 self.system_id_widget.estimatorComboBox.currentIndex()
547 )
548 metadata.sysid_level = self.system_id_widget.levelDoubleSpinBox.value()
549 metadata.sysid_level_ramp_time = self.system_id_widget.levelRampTimeDoubleSpinBox.value()
550 metadata.sysid_signal_type = self.system_id_widget.signalTypeComboBox.itemText(
551 self.system_id_widget.signalTypeComboBox.currentIndex()
552 )
553 metadata.sysid_window = self.system_id_widget.windowComboBox.itemText(
554 self.system_id_widget.windowComboBox.currentIndex()
555 )
556 metadata.sysid_overlap = (
557 self.system_id_widget.overlapDoubleSpinBox.value() / 100
558 if metadata.sysid_signal_type == "Random"
559 else 0.0
560 )
561 metadata.sysid_burst_on = self.system_id_widget.onFractionDoubleSpinBox.value() / 100
562 metadata.sysid_pretrigger = self.system_id_widget.pretriggerDoubleSpinBox.value() / 100
563 metadata.sysid_burst_ramp_fraction = (
564 self.system_id_widget.rampFractionDoubleSpinBox.value() / 100
565 )
566 metadata.sysid_low_frequency_cutoff = self.system_id_widget.lowFreqCutoffSpinBox.value()
567 metadata.sysid_high_frequency_cutoff = self.system_id_widget.highFreqCutoffSpinBox.value()
568 # for key in dir(metadata):
569 # if '__' == key[:2]:
570 # continue
571 # print('Key: {:}'.format(key))
572 # print('Value: {:}'.format(getattr(metadata,key)))
573
574 @property
575 @abstractmethod
576 def initialized_control_names(self):
577 """Names of control channels that have been initialized and will be used in displays"""
578
579 @property
580 @abstractmethod
581 def initialized_output_names(self):
582 """Names of output channels that have been initialized and will be used in displays"""
583
584 @abstractmethod
585 def initialize_environment(self) -> AbstractMetadata:
586 """
587 Update the user interface with environment parameters
588
589 This function is called when the Environment parameters are initialized.
590 This function should set up the user interface accordingly. It must
591 return the parameters class of the environment that inherits from
592 AbstractMetadata.
593
594 Returns
595 -------
596 AbstractMetadata
597 An AbstractMetadata-inheriting object that contains the parameters
598 defining the environment.
599
600 """
601 self.environment_parameters = self.collect_environment_definition_parameters()
602 self.update_sysid_metadata(self.environment_parameters)
603 self.system_id_widget.reference_selector.blockSignals(True)
604 self.system_id_widget.response_selector.blockSignals(True)
605 self.system_id_widget.reference_selector.clear()
606 self.system_id_widget.response_selector.clear()
607 for i, control_name in enumerate(self.initialized_control_names):
608 self.system_id_widget.response_selector.addItem(f"{i + 1}: {control_name}")
609 for i, drive_name in enumerate(self.initialized_output_names):
610 self.system_id_widget.reference_selector.addItem(f"{i + 1}: {drive_name}")
611 self.system_id_widget.reference_selector.blockSignals(False)
612 self.system_id_widget.response_selector.blockSignals(False)
613 self.system_id_widget.reference_selector.setCurrentRow(0)
614 self.system_id_widget.response_selector.setCurrentRow(0)
615 self.update_signal_type()
616 return self.environment_parameters
617
618 def preview_noise(self):
619 """Starts the noise preview"""
620 self.log("Starting Noise Preview")
621 self.update_sysid_metadata(self.environment_parameters)
622 for widget in [
623 self.system_id_widget.preview_noise_button,
624 self.system_id_widget.preview_system_id_button,
625 self.system_id_widget.start_button,
626 self.system_id_widget.samplesPerFrameSpinBox,
627 self.system_id_widget.averagingTypeComboBox,
628 self.system_id_widget.noiseAveragesSpinBox,
629 self.system_id_widget.systemIDAveragesSpinBox,
630 self.system_id_widget.averagingCoefficientDoubleSpinBox,
631 self.system_id_widget.estimatorComboBox,
632 self.system_id_widget.levelDoubleSpinBox,
633 self.system_id_widget.signalTypeComboBox,
634 self.system_id_widget.windowComboBox,
635 self.system_id_widget.overlapDoubleSpinBox,
636 self.system_id_widget.onFractionDoubleSpinBox,
637 self.system_id_widget.pretriggerDoubleSpinBox,
638 self.system_id_widget.rampFractionDoubleSpinBox,
639 self.system_id_widget.stream_transfer_function_data_checkbox,
640 self.system_id_widget.select_transfer_function_stream_file_button,
641 self.system_id_widget.transfer_function_stream_file_display,
642 self.system_id_widget.levelRampTimeDoubleSpinBox,
643 self.system_id_widget.save_system_id_matrices_button,
644 self.system_id_widget.load_system_id_matrices_button,
645 self.system_id_widget.lowFreqCutoffSpinBox,
646 self.system_id_widget.highFreqCutoffSpinBox,
647 ]:
648 widget.setEnabled(False)
649 for widget in [self.system_id_widget.stop_button]:
650 widget.setEnabled(True)
651 self.environment_command_queue.put(
652 self.log_name, (SystemIdCommands.PREVIEW_NOISE, self.environment_parameters)
653 )
654
655 def preview_transfer_function(self):
656 """Starts previewing the system identification transfer function calculation"""
657 self.log("Starting System ID Preview")
658 self.update_sysid_metadata(self.environment_parameters)
659 for widget in [
660 self.system_id_widget.preview_noise_button,
661 self.system_id_widget.preview_system_id_button,
662 self.system_id_widget.start_button,
663 self.system_id_widget.samplesPerFrameSpinBox,
664 self.system_id_widget.averagingTypeComboBox,
665 self.system_id_widget.noiseAveragesSpinBox,
666 self.system_id_widget.systemIDAveragesSpinBox,
667 self.system_id_widget.averagingCoefficientDoubleSpinBox,
668 self.system_id_widget.estimatorComboBox,
669 self.system_id_widget.levelDoubleSpinBox,
670 self.system_id_widget.signalTypeComboBox,
671 self.system_id_widget.windowComboBox,
672 self.system_id_widget.overlapDoubleSpinBox,
673 self.system_id_widget.onFractionDoubleSpinBox,
674 self.system_id_widget.pretriggerDoubleSpinBox,
675 self.system_id_widget.rampFractionDoubleSpinBox,
676 self.system_id_widget.stream_transfer_function_data_checkbox,
677 self.system_id_widget.select_transfer_function_stream_file_button,
678 self.system_id_widget.transfer_function_stream_file_display,
679 self.system_id_widget.levelRampTimeDoubleSpinBox,
680 self.system_id_widget.save_system_id_matrices_button,
681 self.system_id_widget.load_system_id_matrices_button,
682 self.system_id_widget.lowFreqCutoffSpinBox,
683 self.system_id_widget.highFreqCutoffSpinBox,
684 ]:
685 widget.setEnabled(False)
686 for widget in [self.system_id_widget.stop_button]:
687 widget.setEnabled(True)
688 self.environment_command_queue.put(
689 self.log_name,
690 (SystemIdCommands.PREVIEW_TRANSFER_FUNCTION, (self.environment_parameters)),
691 )
692
693 def acquire_transfer_function(self):
694 """Starts the acquisition phase of the controller"""
695 self.log("Starting System ID")
696 self.update_sysid_metadata(self.environment_parameters)
697 for widget in [
698 self.system_id_widget.preview_noise_button,
699 self.system_id_widget.preview_system_id_button,
700 self.system_id_widget.start_button,
701 self.system_id_widget.samplesPerFrameSpinBox,
702 self.system_id_widget.averagingTypeComboBox,
703 self.system_id_widget.noiseAveragesSpinBox,
704 self.system_id_widget.systemIDAveragesSpinBox,
705 self.system_id_widget.averagingCoefficientDoubleSpinBox,
706 self.system_id_widget.estimatorComboBox,
707 self.system_id_widget.levelDoubleSpinBox,
708 self.system_id_widget.signalTypeComboBox,
709 self.system_id_widget.windowComboBox,
710 self.system_id_widget.overlapDoubleSpinBox,
711 self.system_id_widget.onFractionDoubleSpinBox,
712 self.system_id_widget.pretriggerDoubleSpinBox,
713 self.system_id_widget.rampFractionDoubleSpinBox,
714 self.system_id_widget.stream_transfer_function_data_checkbox,
715 self.system_id_widget.select_transfer_function_stream_file_button,
716 self.system_id_widget.transfer_function_stream_file_display,
717 self.system_id_widget.levelRampTimeDoubleSpinBox,
718 self.system_id_widget.save_system_id_matrices_button,
719 self.system_id_widget.load_system_id_matrices_button,
720 self.system_id_widget.lowFreqCutoffSpinBox,
721 self.system_id_widget.highFreqCutoffSpinBox,
722 ]:
723 widget.setEnabled(False)
724 for widget in [self.system_id_widget.stop_button]:
725 widget.setEnabled(True)
726 if self.system_id_widget.stream_transfer_function_data_checkbox.isChecked():
727 stream_name = self.system_id_widget.transfer_function_stream_file_display.text()
728 else:
729 stream_name = None
730 self.environment_command_queue.put(
731 self.log_name,
732 (
733 SystemIdCommands.START_SYSTEM_ID,
734 (self.environment_parameters, stream_name),
735 ),
736 )
737
738 def stop_system_id(self):
739 """Stops the system identification"""
740 self.log("Stopping System ID")
741 self.system_id_widget.stop_button.setEnabled(False)
742 self.environment_command_queue.put(
743 self.log_name, (SystemIdCommands.STOP_SYSTEM_ID, (True, True))
744 )
745
746 def select_transfer_function_stream_file(self):
747 """Select a file to save transfer function data to"""
748 filename, _ = QtWidgets.QFileDialog.getSaveFileName(
749 self.system_id_widget,
750 "Select NetCDF File to Save Transfer Function Data",
751 filter="NetCDF File (*.nc4)",
752 )
753 if filename == "":
754 return
755 self.system_id_widget.transfer_function_stream_file_display.setText(filename)
756 self.system_id_widget.stream_transfer_function_data_checkbox.setChecked(True)
757
758 def update_sysid_plots(
759 self,
760 update_time=True,
761 update_transfer_function=True,
762 update_noise=True,
763 update_kurtosis=True,
764 ):
765 """Updates the plots on the system identification window
766
767 Parameters
768 ----------
769 update_time : bool, optional
770 If True, updates the time hitory plots, by default True
771 update_transfer_function : bool, optional
772 If True, updates the transfer function plots, by default True
773 update_noise : bool, optional
774 If True, updates the noise plots, by default True
775 update_kurtosis : bool, optional
776 If True, updates the kurtosis bar graph, by default True
777 """
778 # Figure out the selected entries
779 response_indices = [
780 i
781 for i in range(self.system_id_widget.response_selector.count())
782 if self.system_id_widget.response_selector.item(i).isSelected()
783 ]
784 reference_indices = [
785 i
786 for i in range(self.system_id_widget.reference_selector.count())
787 if self.system_id_widget.reference_selector.item(i).isSelected()
788 ]
789 # print(response_indices)
790 # print(reference_indices)
791 if update_time:
792 self.time_response_plot.clear()
793 self.time_reference_plot.clear()
794 if self.last_time_response is not None:
795 response_frame_indices = np.array(
796 self.environment_parameters.response_channel_indices
797 )[response_indices]
798 reference_frame_indices = np.array(
799 self.environment_parameters.reference_channel_indices
800 )[reference_indices]
801 response_time_data = self.last_time_response[response_frame_indices]
802 reference_time_data = self.last_time_response[reference_frame_indices]
803 times = (
804 np.arange(response_time_data.shape[-1])
805 / self.data_acquisition_parameters.sample_rate
806 )
807 for i, time_data in enumerate(response_time_data):
808 self.time_response_plot.plot(times, time_data, pen=i)
809 for i, time_data in enumerate(reference_time_data):
810 self.time_reference_plot.plot(times, time_data, pen=i)
811 if update_transfer_function:
812 self.transfer_function_phase_plot.clear()
813 self.transfer_function_magnitude_plot.clear()
814 self.condition_plot.clear()
815 self.coherence_plot.clear()
816 self.impulse_response_plot.clear()
817 if (
818 self.last_transfer_function is not None
819 and len(response_indices) > 0
820 and len(reference_indices) > 0
821 ):
822 # print(self.last_transfer_function)
823 # print(np.array(response_indices)[:,np.newaxis])
824 # print(np.array(reference_indices))
825 frf_section = np.reshape(
826 self.last_transfer_function[
827 ...,
828 np.array(response_indices)[:, np.newaxis],
829 np.array(reference_indices),
830 ],
831 (self.frequencies.size, -1),
832 ).T
833 impulse_response = np.fft.irfft(frf_section, axis=-1)
834 for i, (frf, imp) in enumerate(zip(frf_section, impulse_response)):
835 self.transfer_function_phase_plot.plot(
836 self.frequencies, np.angle(frf) * 180 / np.pi, pen=i
837 )
838 self.transfer_function_magnitude_plot.plot(self.frequencies, np.abs(frf), pen=i)
839 self.impulse_response_plot.plot(
840 np.arange(imp.size) / self.environment_parameters.sample_rate,
841 imp,
842 pen=i,
843 )
844 for i, coherence in enumerate(self.last_coherence[..., response_indices].T):
845 self.coherence_plot.plot(self.frequencies, coherence, pen=i)
846 if self.last_condition is not None:
847 self.condition_plot.plot(self.frequencies, self.last_condition, pen=0)
848 if update_noise:
849 reference_noise = (
850 None
851 if self.last_reference_noise is None or len(reference_indices) == 0
852 else self.last_reference_noise[..., reference_indices, reference_indices].real
853 )
854 response_noise = (
855 None
856 if self.last_response_noise is None or len(response_indices) == 0
857 else self.last_response_noise[..., response_indices, response_indices].real
858 )
859 reference_level = (
860 None
861 if self.last_reference_cpsd is None or len(reference_indices) == 0
862 else self.last_reference_cpsd[..., reference_indices, reference_indices].real
863 )
864 response_level = (
865 None
866 if self.last_response_cpsd is None or len(response_indices) == 0
867 else self.last_response_cpsd[..., response_indices, response_indices].real
868 )
869 self.level_reference_plot.clear()
870 self.level_response_plot.clear()
871 for i in range(len(reference_indices)):
872 if reference_noise is not None:
873 self.level_reference_plot.plot(self.frequencies, reference_noise[:, i], pen=i)
874 if reference_level is not None:
875 try:
876 self.level_reference_plot.plot(
877 self.frequencies, reference_level[:, i], pen=i
878 )
879 except Exception:
880 pass
881 for i in range(len(response_indices)):
882 if response_noise is not None:
883 self.level_response_plot.plot(self.frequencies, response_noise[:, i], pen=i)
884 if response_level is not None:
885 try:
886 self.level_response_plot.plot(self.frequencies, response_level[:, i], pen=i)
887 except Exception:
888 pass
889
890 if update_kurtosis:
891 self.kurtosis_response_plot.clear()
892 self.kurtosis_reference_plot.clear()
893 if self.last_kurtosis is not None:
894 response_kurtosis = self.last_kurtosis[self.all_response_indices]
895 reference_kurtosis = self.last_kurtosis[self.all_reference_indices]
896 response_bar = pg.BarGraphItem(
897 x=range(len(self.response_nodes)),
898 height=response_kurtosis,
899 width=0.5,
900 pen="r",
901 brush="r",
902 )
903 reference_bar = pg.BarGraphItem(
904 x=range(len(self.reference_nodes)),
905 height=reference_kurtosis,
906 width=0.5,
907 pen="r",
908 brush="r",
909 )
910 self.kurtosis_response_plot.addItem(response_bar)
911 self.kurtosis_reference_plot.addItem(reference_bar)
912
913 def show_hide_coherence(self):
914 """Sets the visibility of the coherence plots"""
915 if self.system_id_widget.coherence_checkbox.isChecked():
916 self.system_id_widget.coherence_groupbox.show()
917 else:
918 self.system_id_widget.coherence_groupbox.hide()
919
920 def show_hide_levels(self):
921 """Sets the visibility of the level plots"""
922 if self.system_id_widget.levels_checkbox.isChecked():
923 self.system_id_widget.levels_groupbox.show()
924 else:
925 self.system_id_widget.levels_groupbox.hide()
926
927 def show_hide_time_data(self):
928 """Sets the visibility of the time data plots"""
929 if self.system_id_widget.time_data_checkbox.isChecked():
930 self.system_id_widget.time_data_groupbox.show()
931 else:
932 self.system_id_widget.time_data_groupbox.hide()
933
934 def show_hide_transfer_function(self):
935 """Sets the visibility of the transfer function plots"""
936 if self.system_id_widget.transfer_function_checkbox.isChecked():
937 self.system_id_widget.transfer_function_groupbox.show()
938 else:
939 self.system_id_widget.transfer_function_groupbox.hide()
940
941 def show_hide_impulse(self):
942 """Sets the visibility of the impulse response plots"""
943 if self.system_id_widget.impulse_checkbox.isChecked():
944 self.system_id_widget.impulse_groupbox.show()
945 else:
946 self.system_id_widget.impulse_groupbox.hide()
947
948 def show_hide_kurtosis(self):
949 """Sets the visibility of the kurtosis plots"""
950 if self.system_id_widget.kurtosis_checkbox.isChecked():
951 self.system_id_widget.kurtosis_groupbox.show()
952 else:
953 self.system_id_widget.kurtosis_groupbox.hide()
954
955 def update_signal_type(self):
956 """Updates the UI widgets based on the type of signal that has been selected"""
957 if self.system_id_widget.signalTypeComboBox.currentIndex() == 0: # Random
958 self.system_id_widget.windowComboBox.setCurrentIndex(0)
959 self.system_id_widget.overlapDoubleSpinBox.show()
960 self.system_id_widget.overlapLabel.show()
961 self.system_id_widget.onFractionLabel.hide()
962 self.system_id_widget.onFractionDoubleSpinBox.hide()
963 self.system_id_widget.pretriggerLabel.hide()
964 self.system_id_widget.pretriggerDoubleSpinBox.hide()
965 self.system_id_widget.rampFractionLabel.hide()
966 self.system_id_widget.rampFractionDoubleSpinBox.hide()
967 self.system_id_widget.bandwidthLabel.show()
968 self.system_id_widget.lowFreqCutoffSpinBox.show()
969 self.system_id_widget.highFreqCutoffSpinBox.show()
970 elif self.system_id_widget.signalTypeComboBox.currentIndex() == 1: # Pseudorandom
971 self.system_id_widget.windowComboBox.setCurrentIndex(1)
972 self.system_id_widget.overlapDoubleSpinBox.hide()
973 self.system_id_widget.overlapLabel.hide()
974 self.system_id_widget.onFractionLabel.hide()
975 self.system_id_widget.onFractionDoubleSpinBox.hide()
976 self.system_id_widget.pretriggerLabel.hide()
977 self.system_id_widget.pretriggerDoubleSpinBox.hide()
978 self.system_id_widget.rampFractionLabel.hide()
979 self.system_id_widget.rampFractionDoubleSpinBox.hide()
980 self.system_id_widget.bandwidthLabel.show()
981 self.system_id_widget.lowFreqCutoffSpinBox.show()
982 self.system_id_widget.highFreqCutoffSpinBox.show()
983 elif self.system_id_widget.signalTypeComboBox.currentIndex() == 2: # Burst
984 self.system_id_widget.windowComboBox.setCurrentIndex(1)
985 self.system_id_widget.overlapDoubleSpinBox.hide()
986 self.system_id_widget.overlapLabel.hide()
987 self.system_id_widget.onFractionLabel.show()
988 self.system_id_widget.onFractionDoubleSpinBox.show()
989 self.system_id_widget.pretriggerLabel.show()
990 self.system_id_widget.pretriggerDoubleSpinBox.show()
991 self.system_id_widget.rampFractionLabel.show()
992 self.system_id_widget.rampFractionDoubleSpinBox.show()
993 self.system_id_widget.bandwidthLabel.show()
994 self.system_id_widget.lowFreqCutoffSpinBox.show()
995 self.system_id_widget.highFreqCutoffSpinBox.show()
996 elif self.system_id_widget.signalTypeComboBox.currentIndex() == 3: # Chirp
997 self.system_id_widget.windowComboBox.setCurrentIndex(1)
998 self.system_id_widget.overlapDoubleSpinBox.hide()
999 self.system_id_widget.overlapLabel.hide()
1000 self.system_id_widget.onFractionLabel.hide()
1001 self.system_id_widget.onFractionDoubleSpinBox.hide()
1002 self.system_id_widget.pretriggerLabel.hide()
1003 self.system_id_widget.pretriggerDoubleSpinBox.hide()
1004 self.system_id_widget.rampFractionLabel.hide()
1005 self.system_id_widget.rampFractionDoubleSpinBox.hide()
1006 self.system_id_widget.bandwidthLabel.hide()
1007 self.system_id_widget.lowFreqCutoffSpinBox.hide()
1008 self.system_id_widget.highFreqCutoffSpinBox.hide()
1009
1010 @abstractmethod
1011 def retrieve_metadata(
1012 self,
1013 netcdf_handle: nc4._netCDF4.Dataset, # pylint: disable=c-extension-no-member
1014 environment_name: str = None,
1015 ) -> nc4._netCDF4.Group: # pylint: disable=c-extension-no-member
1016 """Collects environment parameters from a netCDF dataset.
1017
1018 This function retrieves parameters from a netCDF dataset that was written
1019 by the controller during streaming. It must populate the widgets
1020 in the user interface with the proper information.
1021
1022 This function is the "read" counterpart to the store_to_netcdf
1023 function in the AbstractMetadata class, which will write parameters to
1024 the netCDF file to document the metadata.
1025
1026 Note that the entire dataset is passed to this function, so the function
1027 should collect parameters pertaining to the environment from a Group
1028 in the dataset sharing the environment's name, e.g.
1029
1030 Parameters
1031 ----------
1032 netcdf_handle : nc4._netCDF4.Dataset :
1033 The netCDF dataset from which the data will be read. It should have
1034 a group name with the enviroment's name.
1035
1036 environment_name : str : (optional)
1037 The netCDF group name from which the data will be read. This will override
1038 the current environment's name if given.
1039
1040 Returns
1041 -------
1042 group : nc4._netCDF4.Group
1043 The netCDF group that was used to set the system ID parameters
1044 """
1045 # Get the group
1046 group = netcdf_handle.groups[
1047 self.environment_name if environment_name is None else environment_name
1048 ]
1049 self.system_id_widget.samplesPerFrameSpinBox.setValue(group.sysid_frame_size)
1050 self.system_id_widget.averagingTypeComboBox.setCurrentIndex(
1051 self.system_id_widget.averagingTypeComboBox.findText(group.sysid_averaging_type)
1052 )
1053 self.system_id_widget.noiseAveragesSpinBox.setValue(group.sysid_noise_averages)
1054 self.system_id_widget.systemIDAveragesSpinBox.setValue(group.sysid_averages)
1055 self.system_id_widget.averagingCoefficientDoubleSpinBox.setValue(
1056 group.sysid_exponential_averaging_coefficient
1057 )
1058 self.system_id_widget.estimatorComboBox.setCurrentIndex(
1059 self.system_id_widget.estimatorComboBox.findText(group.sysid_estimator)
1060 )
1061 self.system_id_widget.levelDoubleSpinBox.setValue(group.sysid_level)
1062 self.system_id_widget.levelRampTimeDoubleSpinBox.setValue(group.sysid_level_ramp_time)
1063 self.system_id_widget.signalTypeComboBox.setCurrentIndex(
1064 self.system_id_widget.signalTypeComboBox.findText(group.sysid_signal_type)
1065 )
1066 self.system_id_widget.windowComboBox.setCurrentIndex(
1067 self.system_id_widget.windowComboBox.findText(group.sysid_window)
1068 )
1069 self.system_id_widget.overlapDoubleSpinBox.setValue(group.sysid_overlap * 100)
1070 self.system_id_widget.onFractionDoubleSpinBox.setValue(group.sysid_burst_on * 100)
1071 self.system_id_widget.pretriggerDoubleSpinBox.setValue(group.sysid_pretrigger * 100)
1072 self.system_id_widget.rampFractionDoubleSpinBox.setValue(
1073 group.sysid_burst_ramp_fraction * 100
1074 )
1075 if hasattr(group, "sysid_low_frequency_cutoff"):
1076 self.system_id_widget.lowFreqCutoffSpinBox.setValue(group.sysid_low_frequency_cutoff)
1077 if hasattr(group, "sysid_high_frequency_cutoff"):
1078 self.system_id_widget.highFreqCutoffSpinBox.setValue(group.sysid_high_frequency_cutoff)
1079 return group
1080
1081 @abstractmethod
1082 def update_gui(self, queue_data: tuple):
1083 """Update the environment's graphical user interface
1084
1085 This function will receive data from the gui_update_queue that
1086 specifies how the user interface should be updated. Data will usually
1087 be received as ``(instruction,data)`` pairs, where the ``instruction`` notes
1088 what operation should be taken or which widget should be modified, and
1089 the ``data`` notes what data should be used in the update.
1090
1091 Parameters
1092 ----------
1093 queue_data : tuple
1094 A tuple containing ``(instruction,data)`` pairs where ``instruction``
1095 defines and operation or widget to be modified and ``data`` contains
1096 the data used to perform the operation.
1097 """
1098 message, data = queue_data
1099 self.log(f"Got GUI Message {message}")
1100 # print('Update GUI Got {:}'.format(message))
1101 if message == "time_frame":
1102 self.last_time_response, accept = data
1103 self.update_sysid_plots(
1104 update_time=True,
1105 update_transfer_function=False,
1106 update_noise=False,
1107 update_kurtosis=False,
1108 )
1109 elif message == "kurtosis":
1110 self.last_kurtosis = data
1111 self.update_sysid_plots(
1112 update_time=False,
1113 update_transfer_function=False,
1114 update_noise=False,
1115 update_kurtosis=True,
1116 )
1117 elif message == "noise_update":
1118 (
1119 frames,
1120 total_frames,
1121 self.frequencies,
1122 self.last_response_noise,
1123 self.last_reference_noise,
1124 ) = data
1125 self.update_sysid_plots(
1126 update_time=False,
1127 update_transfer_function=False,
1128 update_noise=True,
1129 update_kurtosis=False,
1130 )
1131 self.system_id_widget.current_frames_spinbox.setValue(frames)
1132 self.system_id_widget.total_frames_spinbox.setValue(total_frames)
1133 self.system_id_widget.progressBar.setValue(int(frames / total_frames * 100))
1134 elif message == "sysid_update":
1135 (
1136 frames,
1137 total_frames,
1138 self.frequencies,
1139 self.last_transfer_function,
1140 self.last_coherence,
1141 self.last_response_cpsd,
1142 self.last_reference_cpsd,
1143 self.last_condition,
1144 ) = data
1145 # print(self.last_transfer_function.shape)
1146 # print(self.last_coherence.shape)
1147 # print(self.last_response_cpsd.shape)
1148 # print(self.last_reference_cpsd.shape)
1149 self.update_sysid_plots(
1150 update_time=False,
1151 update_transfer_function=True,
1152 update_noise=True,
1153 update_kurtosis=False,
1154 )
1155 self.system_id_widget.current_frames_spinbox.setValue(frames)
1156 self.system_id_widget.total_frames_spinbox.setValue(total_frames)
1157 self.system_id_widget.progressBar.setValue(int(frames / total_frames * 100))
1158 elif message == "enable_system_id":
1159 for widget in [
1160 self.system_id_widget.preview_noise_button,
1161 self.system_id_widget.preview_system_id_button,
1162 self.system_id_widget.start_button,
1163 self.system_id_widget.samplesPerFrameSpinBox,
1164 self.system_id_widget.averagingTypeComboBox,
1165 self.system_id_widget.noiseAveragesSpinBox,
1166 self.system_id_widget.systemIDAveragesSpinBox,
1167 self.system_id_widget.averagingCoefficientDoubleSpinBox,
1168 self.system_id_widget.estimatorComboBox,
1169 self.system_id_widget.levelDoubleSpinBox,
1170 self.system_id_widget.signalTypeComboBox,
1171 self.system_id_widget.windowComboBox,
1172 self.system_id_widget.overlapDoubleSpinBox,
1173 self.system_id_widget.onFractionDoubleSpinBox,
1174 self.system_id_widget.pretriggerDoubleSpinBox,
1175 self.system_id_widget.rampFractionDoubleSpinBox,
1176 self.system_id_widget.stream_transfer_function_data_checkbox,
1177 self.system_id_widget.select_transfer_function_stream_file_button,
1178 self.system_id_widget.transfer_function_stream_file_display,
1179 self.system_id_widget.levelRampTimeDoubleSpinBox,
1180 self.system_id_widget.save_system_id_matrices_button,
1181 self.system_id_widget.load_system_id_matrices_button,
1182 self.system_id_widget.lowFreqCutoffSpinBox,
1183 self.system_id_widget.highFreqCutoffSpinBox,
1184 ]:
1185 widget.setEnabled(True)
1186 for widget in [self.system_id_widget.stop_button]:
1187 widget.setEnabled(False)
1188 elif message == "disable_system_id":
1189 for widget in [
1190 self.system_id_widget.preview_noise_button,
1191 self.system_id_widget.preview_system_id_button,
1192 self.system_id_widget.start_button,
1193 self.system_id_widget.samplesPerFrameSpinBox,
1194 self.system_id_widget.averagingTypeComboBox,
1195 self.system_id_widget.noiseAveragesSpinBox,
1196 self.system_id_widget.systemIDAveragesSpinBox,
1197 self.system_id_widget.averagingCoefficientDoubleSpinBox,
1198 self.system_id_widget.estimatorComboBox,
1199 self.system_id_widget.levelDoubleSpinBox,
1200 self.system_id_widget.signalTypeComboBox,
1201 self.system_id_widget.windowComboBox,
1202 self.system_id_widget.overlapDoubleSpinBox,
1203 self.system_id_widget.onFractionDoubleSpinBox,
1204 self.system_id_widget.pretriggerDoubleSpinBox,
1205 self.system_id_widget.rampFractionDoubleSpinBox,
1206 self.system_id_widget.stream_transfer_function_data_checkbox,
1207 self.system_id_widget.select_transfer_function_stream_file_button,
1208 self.system_id_widget.transfer_function_stream_file_display,
1209 self.system_id_widget.levelRampTimeDoubleSpinBox,
1210 self.system_id_widget.save_system_id_matrices_button,
1211 self.system_id_widget.load_system_id_matrices_button,
1212 self.system_id_widget.lowFreqCutoffSpinBox,
1213 self.system_id_widget.highFreqCutoffSpinBox,
1214 ]:
1215 widget.setEnabled(False)
1216 for widget in [self.system_id_widget.stop_button]:
1217 widget.setEnabled(True)
1218 else:
1219 return False
1220 return True
1221
1222 @staticmethod
1223 @abstractmethod
1224 def create_environment_template(
1225 environment_name: str, workbook: openpyxl.workbook.workbook.Workbook
1226 ):
1227 """Creates a template worksheet in an Excel workbook defining the
1228 environment.
1229
1230 This function creates a template worksheet in an Excel workbook that
1231 when filled out could be read by the controller to re-create the
1232 environment.
1233
1234 This function is the "write" counterpart to the
1235 ``set_parameters_from_template`` function in the ``AbstractUI`` class,
1236 which reads the values from the template file to populate the user
1237 interface.
1238
1239 Parameters
1240 ----------
1241 environment_name : str :
1242 The name of the environment that will specify the worksheet's name
1243 workbook : openpyxl.workbook.workbook.Workbook :
1244 A reference to an ``openpyxl`` workbook.
1245
1246 """
1247
1248 @abstractmethod
1249 def set_parameters_from_template(self, worksheet: openpyxl.worksheet.worksheet.Worksheet):
1250 """
1251 Collects parameters for the user interface from the Excel template file
1252
1253 This function reads a filled out template worksheet to create an
1254 environment. Cells on this worksheet contain parameters needed to
1255 specify the environment, so this function should read those cells and
1256 update the UI widgets with those parameters.
1257
1258 This function is the "read" counterpart to the
1259 ``create_environment_template`` function in the ``AbstractUI`` class,
1260 which writes a template file that can be filled out by a user.
1261
1262
1263 Parameters
1264 ----------
1265 worksheet : openpyxl.worksheet.worksheet.Worksheet
1266 An openpyxl worksheet that contains the environment template.
1267 Cells on this worksheet should contain the parameters needed for the
1268 user interface.
1269
1270 """
1271
1272 def save_sysid_matrix_file(self):
1273 """Saves out system identification data to a file"""
1274 if self.last_transfer_function is None or self.last_response_noise is None:
1275 error_message_qt(
1276 "Run System Identification First!",
1277 "System Identification Matrices not yet created.\n\n"
1278 "Run System Identification First!",
1279 )
1280 return
1281 filename, file_filter = QtWidgets.QFileDialog.getSaveFileName(
1282 self.system_id_widget,
1283 "Select File to Save Transfer Function Matrices",
1284 filter="NetCDF File (*.nc4);;MatLab File (*.mat);;Numpy File (*.npz)",
1285 )
1286 labels = [
1287 ["node_number", str],
1288 ["node_direction", str],
1289 ["comment", str],
1290 ["serial_number", str],
1291 ["triax_dof", str],
1292 ["sensitivity", str],
1293 ["unit", str],
1294 ["make", str],
1295 ["model", str],
1296 ["expiration", str],
1297 ["physical_device", str],
1298 ["physical_channel", str],
1299 ["channel_type", str],
1300 ["minimum_value", str],
1301 ["maximum_value", str],
1302 ["coupling", str],
1303 ["excitation_source", str],
1304 ["excitation", str],
1305 ["feedback_device", str],
1306 ["feedback_channel", str],
1307 ["warning_level", str],
1308 ["abort_level", str],
1309 ]
1310 if file_filter == "NetCDF File (*.nc4)":
1311 netcdf_handle = nc4.Dataset( # pylint: disable=no-member
1312 filename, "w", format="NETCDF4", clobber=True
1313 )
1314 # Create dimensions
1315 netcdf_handle.createDimension(
1316 "response_channels", len(self.data_acquisition_parameters.channel_list)
1317 )
1318
1319 netcdf_handle.createDimension(
1320 "num_environments",
1321 len(self.data_acquisition_parameters.environment_names),
1322 )
1323 # Create attributes
1324 netcdf_handle.file_version = "3.0.0"
1325 netcdf_handle.sample_rate = self.data_acquisition_parameters.sample_rate
1326 netcdf_handle.time_per_write = (
1327 self.data_acquisition_parameters.samples_per_write
1328 / self.data_acquisition_parameters.output_sample_rate
1329 )
1330 netcdf_handle.time_per_read = (
1331 self.data_acquisition_parameters.samples_per_read
1332 / self.data_acquisition_parameters.sample_rate
1333 )
1334 netcdf_handle.hardware = self.data_acquisition_parameters.hardware
1335 netcdf_handle.hardware_file = (
1336 "None"
1337 if self.data_acquisition_parameters.hardware_file is None
1338 else self.data_acquisition_parameters.hardware_file
1339 )
1340 netcdf_handle.output_oversample = self.data_acquisition_parameters.output_oversample
1341 for (
1342 name,
1343 value,
1344 ) in self.data_acquisition_parameters.extra_parameters.items():
1345 setattr(netcdf_handle, name, value)
1346 # Create Variables
1347 var = netcdf_handle.createVariable("environment_names", str, ("num_environments",))
1348 this_environment_index = None
1349 for i, name in enumerate(self.data_acquisition_parameters.environment_names):
1350 var[i] = name
1351 if name == self.environment_name:
1352 this_environment_index = i
1353 var = netcdf_handle.createVariable(
1354 "environment_active_channels",
1355 "i1",
1356 ("response_channels", "num_environments"),
1357 )
1358 var[...] = self.data_acquisition_parameters.environment_active_channels.astype("int8")[
1359 self.data_acquisition_parameters.environment_active_channels[
1360 :, this_environment_index
1361 ],
1362 :,
1363 ]
1364 # Create channel table variables
1365 for label, netcdf_datatype in labels:
1366 var = netcdf_handle.createVariable(
1367 "/channels/" + label, netcdf_datatype, ("response_channels",)
1368 )
1369 channel_data = [
1370 getattr(channel, label)
1371 for channel in self.data_acquisition_parameters.channel_list
1372 ]
1373 if netcdf_datatype == "i1":
1374 channel_data = np.array([1 if val else 0 for val in channel_data])
1375 else:
1376 channel_data = ["" if val is None else val for val in channel_data]
1377 for i, cd in enumerate(channel_data):
1378 var[i] = cd
1379 group_handle = netcdf_handle.createGroup(self.environment_name)
1380 self.environment_parameters.store_to_netcdf(group_handle)
1381 try:
1382 group_handle.createDimension(
1383 "sysid_control_channels", self.last_transfer_function.shape[1]
1384 )
1385 except RuntimeError:
1386 pass
1387 try:
1388 group_handle.createDimension(
1389 "sysid_output_channels", self.last_transfer_function.shape[2]
1390 )
1391 except RuntimeError:
1392 pass
1393 try:
1394 group_handle.createDimension(
1395 "sysid_fft_lines", self.last_transfer_function.shape[0]
1396 )
1397 except RuntimeError:
1398 pass
1399 var = group_handle.createVariable(
1400 "frf_data_real",
1401 "f8",
1402 ("sysid_fft_lines", "sysid_control_channels", "sysid_output_channels"),
1403 )
1404 var[...] = self.last_transfer_function.real
1405 var = group_handle.createVariable(
1406 "frf_data_imag",
1407 "f8",
1408 ("sysid_fft_lines", "sysid_control_channels", "sysid_output_channels"),
1409 )
1410 var[...] = self.last_transfer_function.imag
1411 var = group_handle.createVariable(
1412 "frf_coherence", "f8", ("sysid_fft_lines", "sysid_control_channels")
1413 )
1414 var[...] = self.last_coherence.real
1415 var = group_handle.createVariable(
1416 "response_cpsd_real",
1417 "f8",
1418 ("sysid_fft_lines", "sysid_control_channels", "sysid_control_channels"),
1419 )
1420 var[...] = self.last_response_cpsd.real
1421 var = group_handle.createVariable(
1422 "response_cpsd_imag",
1423 "f8",
1424 ("sysid_fft_lines", "sysid_control_channels", "sysid_control_channels"),
1425 )
1426 var[...] = self.last_response_cpsd.imag
1427 var = group_handle.createVariable(
1428 "reference_cpsd_real",
1429 "f8",
1430 ("sysid_fft_lines", "sysid_output_channels", "sysid_output_channels"),
1431 )
1432 var[...] = self.last_reference_cpsd.real
1433 var = group_handle.createVariable(
1434 "reference_cpsd_imag",
1435 "f8",
1436 ("sysid_fft_lines", "sysid_output_channels", "sysid_output_channels"),
1437 )
1438 var[...] = self.last_reference_cpsd.imag
1439 var = group_handle.createVariable(
1440 "response_noise_cpsd_real",
1441 "f8",
1442 ("sysid_fft_lines", "sysid_control_channels", "sysid_control_channels"),
1443 )
1444 var[...] = self.last_response_noise.real
1445 var = group_handle.createVariable(
1446 "response_noise_cpsd_imag",
1447 "f8",
1448 ("sysid_fft_lines", "sysid_control_channels", "sysid_control_channels"),
1449 )
1450 var[...] = self.last_response_noise.imag
1451 var = group_handle.createVariable(
1452 "reference_noise_cpsd_real",
1453 "f8",
1454 ("sysid_fft_lines", "sysid_output_channels", "sysid_output_channels"),
1455 )
1456 var[...] = self.last_reference_noise.real
1457 var = group_handle.createVariable(
1458 "reference_noise_cpsd_imag",
1459 "f8",
1460 ("sysid_fft_lines", "sysid_output_channels", "sysid_output_channels"),
1461 )
1462 var[...] = self.last_reference_noise.imag
1463 else:
1464 field_dict = {}
1465 field_dict["version"] = "3.0.0"
1466 field_dict["sample_rate"] = self.data_acquisition_parameters.sample_rate
1467 field_dict["time_per_write"] = (
1468 self.data_acquisition_parameters.samples_per_write
1469 / self.data_acquisition_parameters.output_sample_rate
1470 )
1471 field_dict["time_per_read"] = (
1472 self.data_acquisition_parameters.samples_per_read
1473 / self.data_acquisition_parameters.sample_rate
1474 )
1475 field_dict["hardware"] = self.data_acquisition_parameters.hardware
1476 field_dict["hardware_file"] = (
1477 "None"
1478 if self.data_acquisition_parameters.hardware_file is None
1479 else self.data_acquisition_parameters.hardware_file
1480 )
1481 field_dict["output_oversample"] = self.data_acquisition_parameters.output_oversample
1482 field_dict["frf_data"] = self.last_transfer_function
1483 field_dict["response_cpsd"] = self.last_response_cpsd
1484 field_dict["reference_cpsd"] = self.last_reference_cpsd
1485 field_dict["coherence"] = self.last_coherence
1486 field_dict["response_noise_cpsd"] = self.last_response_noise
1487 field_dict["reference_noise_cpsd"] = self.last_reference_noise
1488 field_dict["response_indices"] = self.environment_parameters.response_channel_indices
1489 field_dict["reference_indices"] = self.environment_parameters.reference_channel_indices
1490 field_dict["response_transformation_matrix"] = (
1491 np.nan
1492 if self.environment_parameters.response_transformation_matrix is None
1493 else self.environment_parameters.response_transformation_matrix
1494 )
1495 field_dict["reference_transformation_matrix"] = (
1496 np.nan
1497 if self.environment_parameters.reference_transformation_matrix is None
1498 else self.environment_parameters.reference_transformation_matrix
1499 )
1500 field_dict["sysid_frequency_spacing"] = (
1501 self.environment_parameters.sysid_frequency_spacing
1502 )
1503 field_dict.update(self.data_acquisition_parameters.extra_parameters)
1504 for key, value in self.environment_parameters.__dict__.items():
1505 try:
1506 if "sysid_" in key:
1507 field_dict[key] = np.array(value)
1508 except TypeError:
1509 continue
1510 for label, _ in labels:
1511 field_dict["channel_" + label] = np.array(
1512 [
1513 ("" if getattr(channel, label) is None else getattr(channel, label))
1514 for channel in self.data_acquisition_parameters.channel_list
1515 ]
1516 )
1517 # print(field_dict)
1518 if file_filter == "MatLab File (*.mat)":
1519 for field in [
1520 "frf_data",
1521 "response_cpsd",
1522 "reference_cpsd",
1523 "coherence",
1524 "response_noise_cpsd",
1525 "reference_noise_cpsd",
1526 ]:
1527 field_dict[field] = np.moveaxis(field_dict[field], 0, -1)
1528 savemat(filename, field_dict)
1529 elif file_filter == "Numpy File (*.npz)":
1530 np.savez(filename, **field_dict)
1531
1532 def load_sysid_matrix_file(self, filename, popup=True):
1533 """Loads a system identification dataset from previous analysis or testing
1534
1535 Parameters
1536 ----------
1537 filename : str
1538 The filename of the system identification file to load
1539 popup : bool, optional
1540 If True, bring up a file selection dialog box instead of using filename, by default True
1541
1542 Raises
1543 ------
1544 ValueError
1545 If the wrong type of file is loaded
1546 """
1547 if popup:
1548 filename, file_filter = QtWidgets.QFileDialog.getOpenFileName(
1549 self.system_id_widget,
1550 "Select File to Load Transfer Function Matrices",
1551 filter="NetCDF File (*.nc4);;MatLab File (*.mat);;Numpy File (*.npz);;"
1552 "SDynPy FRF (*.npz);;Forcefinder SPR (*.npz)",
1553 )
1554 else:
1555 file_filter = None
1556 if filename is None or filename == "":
1557 return
1558 elif file_filter == "NetCDF File (*.nc4)" or (
1559 file_filter is None and filename.endswith(".nc4")
1560 ):
1561 netcdf_handle = nc4.Dataset( # pylint: disable=no-member
1562 filename, "r", format="NETCDF4"
1563 )
1564 # TODO: error checking to make sure relevant info matches current controller state
1565 group_handle = netcdf_handle[self.environment_name]
1566 sample_rate = netcdf_handle.sample_rate
1567 frame_size = group_handle.sysid_frame_size
1568 fft_lines = group_handle.dimensions["fft_lines"].size
1569 variables = group_handle.variables
1570 combine = np.vectorize(complex)
1571 try:
1572 self.last_transfer_function = np.array(
1573 combine(variables["frf_data_real"][:], variables["frf_data_imag"][:])
1574 )
1575 self.last_coherence = np.array(variables["frf_coherence"][:])
1576 self.last_response_cpsd = np.array(
1577 combine(
1578 variables["response_cpsd_real"][:],
1579 variables["response_cpsd_imag"][:],
1580 )
1581 )
1582 self.last_reference_cpsd = np.array(
1583 combine(
1584 variables["reference_cpsd_real"][:],
1585 variables["reference_cpsd_imag"][:],
1586 )
1587 )
1588 self.last_response_noise = np.array(
1589 combine(
1590 variables["response_noise_cpsd_real"][:],
1591 variables["response_noise_cpsd_imag"][:],
1592 )
1593 )
1594 self.last_reference_noise = np.array(
1595 combine(
1596 variables["reference_noise_cpsd_real"][:],
1597 variables["reference_noise_cpsd_imag"][:],
1598 )
1599 )
1600 self.last_condition = np.linalg.cond(self.last_transfer_function)
1601 self.frequencies = np.arange(fft_lines) * sample_rate / frame_size
1602 except KeyError:
1603 # TODO: in the case that a time history file was chosen, should FRF be
1604 # auto-computed? could work on environment run or sysid (environment run just
1605 # may have poor FRF)
1606 # could we use the data analysis process to do the computation? so we don't
1607 # lock up the UI
1608 # could we also pass the FRF to any virtual hardware?
1609 return
1610 elif file_filter == "SDynPy FRF (*.npz)":
1611 sdynpy_dict = np.load(filename)
1612 if sdynpy_dict["function_type"].item() != 4:
1613 raise ValueError("File must contain a Sdynpy FrequencyResponseFunctionArray")
1614 self.last_transfer_function = np.moveaxis(
1615 np.array(sdynpy_dict["data"]["ordinate"]), -1, 0
1616 )
1617 self.last_condition = np.linalg.cond(self.last_transfer_function)
1618 self.frequencies = np.array(sdynpy_dict["data"]["abscissa"][0][0])
1619 self.last_coherence = np.zeros((0, self.last_transfer_function.shape[1]))
1620 # TODO: pull coordinate out to verify matching info
1621 elif file_filter == "Forcefinder SPR (*.npz)":
1622 forcefinder_dict = np.load(filename)
1623 self.last_transfer_function = np.array(
1624 forcefinder_dict["training_frf"]
1625 ) # training frf will generally be the one used for testing
1626 self.last_condition = np.linalg.cond(self.last_transfer_function)
1627 self.frequencies = np.array(forcefinder_dict["abscissa"])
1628 self.last_coherence = np.zeros((0, self.last_transfer_function.shape[1]))
1629 if "buzz_cpsd" in forcefinder_dict:
1630 self.last_response_cpsd = np.array(forcefinder_dict["buzz_cpsd"])
1631 else:
1632 if file_filter == "MatLab File (*.mat)":
1633 field_dict = loadmat(filename)
1634 for field in [
1635 "frf_data",
1636 "response_cpsd",
1637 "reference_cpsd",
1638 "coherence",
1639 "response_noise_cpsd",
1640 "reference_noise_cpsd",
1641 ]:
1642 field_dict[field] = np.moveaxis(field_dict[field], -1, 0)
1643 elif file_filter == "Numpy File (*.npz)":
1644 field_dict = np.load(filename)
1645 self.last_transfer_function = np.array(field_dict["frf_data"])
1646 self.last_response_cpsd = np.array(field_dict["response_cpsd"])
1647 self.last_reference_cpsd = np.array(field_dict["reference_cpsd"])
1648 self.last_coherence = np.array(field_dict["coherence"])
1649 self.last_response_noise = np.array(field_dict["response_noise_cpsd"])
1650 self.last_reference_noise = np.array(field_dict["reference_noise_cpsd"])
1651 self.last_condition = np.linalg.cond(self.last_transfer_function)
1652 self.frequencies = (
1653 np.arange(self.last_transfer_function.shape[0])
1654 * field_dict["sysid_frequency_spacing"].squeeze()
1655 )
1656 # Send values to data analysis process (through the
1657 # environment queue, environment then passes to data analysis)
1658 self.environment_command_queue.put(
1659 self.log_name,
1660 (
1661 SysIDDataAnalysisCommands.LOAD_NOISE,
1662 (
1663 0,
1664 self.frequencies,
1665 None,
1666 None,
1667 self.last_response_noise,
1668 self.last_reference_noise,
1669 None,
1670 ),
1671 ),
1672 )
1673 self.environment_command_queue.put(
1674 self.log_name,
1675 (
1676 SysIDDataAnalysisCommands.LOAD_TRANSFER_FUNCTION,
1677 (
1678 0,
1679 self.frequencies,
1680 self.last_transfer_function,
1681 self.last_coherence,
1682 self.last_response_cpsd,
1683 self.last_reference_cpsd,
1684 self.last_condition,
1685 ),
1686 ),
1687 )
1688 self.update_sysid_plots(
1689 update_time=False,
1690 update_transfer_function=True,
1691 update_noise=True,
1692 update_kurtosis=False,
1693 )
1694 self.system_id_widget.current_frames_spinbox.setValue(0)
1695 self.system_id_widget.total_frames_spinbox.setValue(0)
1696 self.system_id_widget.progressBar.setValue(100)
1697
1698 def disable_system_id_daq_armed(self):
1699 """Disables widget on the UI due to the data acquisition being in use"""
1700 for widget in [
1701 self.system_id_widget.preview_noise_button,
1702 self.system_id_widget.preview_system_id_button,
1703 self.system_id_widget.start_button,
1704 self.system_id_widget.samplesPerFrameSpinBox,
1705 self.system_id_widget.averagingTypeComboBox,
1706 self.system_id_widget.noiseAveragesSpinBox,
1707 self.system_id_widget.systemIDAveragesSpinBox,
1708 self.system_id_widget.averagingCoefficientDoubleSpinBox,
1709 self.system_id_widget.estimatorComboBox,
1710 self.system_id_widget.levelDoubleSpinBox,
1711 self.system_id_widget.signalTypeComboBox,
1712 self.system_id_widget.windowComboBox,
1713 self.system_id_widget.overlapDoubleSpinBox,
1714 self.system_id_widget.onFractionDoubleSpinBox,
1715 self.system_id_widget.pretriggerDoubleSpinBox,
1716 self.system_id_widget.rampFractionDoubleSpinBox,
1717 self.system_id_widget.stream_transfer_function_data_checkbox,
1718 self.system_id_widget.select_transfer_function_stream_file_button,
1719 self.system_id_widget.transfer_function_stream_file_display,
1720 self.system_id_widget.levelRampTimeDoubleSpinBox,
1721 self.system_id_widget.save_system_id_matrices_button,
1722 self.system_id_widget.load_system_id_matrices_button,
1723 self.system_id_widget.lowFreqCutoffSpinBox,
1724 self.system_id_widget.highFreqCutoffSpinBox,
1725 ]:
1726 widget.setEnabled(False)
1727 for widget in [self.system_id_widget.stop_button]:
1728 widget.setEnabled(False)
1729
1730 def enable_system_id_daq_disarmed(self):
1731 """Enables widgets on the UI due to the data acquisition being no longer in use"""
1732 for widget in [
1733 self.system_id_widget.preview_noise_button,
1734 self.system_id_widget.preview_system_id_button,
1735 self.system_id_widget.start_button,
1736 self.system_id_widget.samplesPerFrameSpinBox,
1737 self.system_id_widget.averagingTypeComboBox,
1738 self.system_id_widget.noiseAveragesSpinBox,
1739 self.system_id_widget.systemIDAveragesSpinBox,
1740 self.system_id_widget.averagingCoefficientDoubleSpinBox,
1741 self.system_id_widget.estimatorComboBox,
1742 self.system_id_widget.levelDoubleSpinBox,
1743 self.system_id_widget.signalTypeComboBox,
1744 self.system_id_widget.windowComboBox,
1745 self.system_id_widget.overlapDoubleSpinBox,
1746 self.system_id_widget.onFractionDoubleSpinBox,
1747 self.system_id_widget.pretriggerDoubleSpinBox,
1748 self.system_id_widget.rampFractionDoubleSpinBox,
1749 self.system_id_widget.stream_transfer_function_data_checkbox,
1750 self.system_id_widget.select_transfer_function_stream_file_button,
1751 self.system_id_widget.transfer_function_stream_file_display,
1752 self.system_id_widget.levelRampTimeDoubleSpinBox,
1753 self.system_id_widget.save_system_id_matrices_button,
1754 self.system_id_widget.load_system_id_matrices_button,
1755 self.system_id_widget.lowFreqCutoffSpinBox,
1756 self.system_id_widget.highFreqCutoffSpinBox,
1757 ]:
1758 widget.setEnabled(True)
1759 for widget in [self.system_id_widget.stop_button]:
1760 widget.setEnabled(False)
1761
1762
1763class AbstractSysIdEnvironment(AbstractEnvironment):
1764 """Abstract Environment class defining the interface with the controller
1765
1766 This class is used to define the operation of an environment within the
1767 controller, which must be completed by subclasses inheriting from this
1768 class. Children of this class will sit in a While loop in the
1769 ``AbstractEnvironment.run()`` function. While in this loop, the
1770 Environment will pull instructions and data from the
1771 ``command_queue`` and then use the ``command_map`` to map those instructions
1772 to functions in the class.
1773
1774 All child classes inheriting from AbstractEnvironment will require functions
1775 to be defined for global operations of the controller, which are already
1776 mapped in the ``command_map``. Any additional operations must be defined
1777 by functions and then added to the command_map when initilizing the child
1778 class.
1779
1780 All functions called via the ``command_map`` must accept one input argument
1781 which is the data passed along with the command. For functions that do not
1782 require additional data, this argument can be ignored, but it must still be
1783 present in the function's calling signature.
1784
1785 The run function will continue until one of the functions called by
1786 ``command_map`` returns a truthy value, which signifies the controller to
1787 quit. Therefore, any functions mapped to ``command_map`` that should not
1788 instruct the program to quit should not return any value that could be
1789 interpreted as true."""
1790
1791 def __init__(
1792 self,
1793 environment_name: str,
1794 command_queue: VerboseMessageQueue,
1795 gui_update_queue: Queue,
1796 controller_communication_queue: VerboseMessageQueue,
1797 log_file_queue: Queue,
1798 collector_command_queue: VerboseMessageQueue,
1799 signal_generator_command_queue: VerboseMessageQueue,
1800 spectral_processing_command_queue: VerboseMessageQueue,
1801 data_analysis_command_queue: VerboseMessageQueue,
1802 data_in_queue: Queue,
1803 data_out_queue: Queue,
1804 acquisition_active: mp.sharedctypes.Synchronized,
1805 output_active: mp.sharedctypes.Synchronized,
1806 ):
1807 super().__init__(
1808 environment_name,
1809 command_queue,
1810 gui_update_queue,
1811 controller_communication_queue,
1812 log_file_queue,
1813 data_in_queue,
1814 data_out_queue,
1815 acquisition_active,
1816 output_active,
1817 )
1818 self.map_command(SystemIdCommands.PREVIEW_NOISE, self.preview_noise)
1819 self.map_command(SystemIdCommands.PREVIEW_TRANSFER_FUNCTION, self.preview_transfer_function)
1820 self.map_command(SystemIdCommands.START_SYSTEM_ID, self.start_noise)
1821 self.map_command(SystemIdCommands.STOP_SYSTEM_ID, self.stop_system_id)
1822 self.map_command(
1823 SignalGenerationCommands.SHUTDOWN_ACHIEVED, self.siggen_shutdown_achieved_fn
1824 )
1825 self.map_command(
1826 DataCollectorCommands.SHUTDOWN_ACHIEVED, self.collector_shutdown_achieved_fn
1827 )
1828 self.map_command(
1829 SpectralProcessingCommands.SHUTDOWN_ACHIEVED,
1830 self.spectral_shutdown_achieved_fn,
1831 )
1832 self.map_command(
1833 SysIDDataAnalysisCommands.SHUTDOWN_ACHIEVED,
1834 self.analysis_shutdown_achieved_fn,
1835 )
1836 self.map_command(SysIDDataAnalysisCommands.START_SHUTDOWN, self.stop_system_id)
1837 self.map_command(
1838 SysIDDataAnalysisCommands.START_SHUTDOWN_AND_RUN_SYSID,
1839 self.start_shutdown_and_run_sysid,
1840 )
1841 self.map_command(SysIDDataAnalysisCommands.SYSTEM_ID_COMPLETE, self.system_id_complete)
1842 self.map_command(SysIDDataAnalysisCommands.LOAD_NOISE, self.load_noise)
1843 self.map_command(
1844 SysIDDataAnalysisCommands.LOAD_TRANSFER_FUNCTION,
1845 self.load_transfer_function,
1846 )
1847 self.map_command(
1848 SystemIdCommands.CHECK_FOR_COMPLETE_SHUTDOWN, self.check_for_sysid_shutdown
1849 )
1850 self._waiting_to_start_transfer_function = False
1851 self.collector_command_queue = collector_command_queue
1852 self.signal_generator_command_queue = signal_generator_command_queue
1853 self.spectral_processing_command_queue = spectral_processing_command_queue
1854 self.data_analysis_command_queue = data_analysis_command_queue
1855 self.data_acquisition_parameters = None
1856 self.environment_parameters = None
1857 self.collector_shutdown_achieved = True
1858 self.spectral_shutdown_achieved = True
1859 self.siggen_shutdown_achieved = True
1860 self.analysis_shutdown_achieved = True
1861 self._sysid_stream_name = None
1862
1863 def initialize_data_acquisition_parameters(
1864 self, data_acquisition_parameters: DataAcquisitionParameters
1865 ):
1866 """Initialize the data acquisition parameters in the environment.
1867
1868 The environment will receive the global data acquisition parameters from
1869 the controller, and must set itself up accordingly.
1870
1871 Parameters
1872 ----------
1873 data_acquisition_parameters : DataAcquisitionParameters :
1874 A container containing data acquisition parameters, including
1875 channels active in the environment as well as sampling parameters.
1876 """
1877 self.data_acquisition_parameters = data_acquisition_parameters
1878
1879 def initialize_environment_test_parameters(self, environment_parameters: AbstractSysIdMetadata):
1880 """
1881 Initialize the environment parameters specific to this environment
1882
1883 The environment will recieve parameters defining itself from the
1884 user interface and must set itself up accordingly.
1885
1886 Parameters
1887 ----------
1888 environment_parameters : AbstractMetadata
1889 A container containing the parameters defining the environment
1890
1891 """
1892 self.environment_parameters = environment_parameters
1893
1894 def get_sysid_data_collector_metadata(self) -> CollectorMetadata:
1895 """Collects metadata to send to the data collector"""
1896 num_channels = self.environment_parameters.number_of_channels
1897 response_channel_indices = self.environment_parameters.response_channel_indices
1898 reference_channel_indices = self.environment_parameters.reference_channel_indices
1899 if self.environment_parameters.sysid_signal_type in [
1900 "Random",
1901 "Pseudorandom",
1902 "Chirp",
1903 ]:
1904 acquisition_type = AcquisitionType.FREE_RUN
1905 else:
1906 acquisition_type = AcquisitionType.TRIGGER_FIRST_FRAME
1907 acceptance = Acceptance.AUTOMATIC
1908 acceptance_function = None
1909 if self.environment_parameters.sysid_signal_type == "Random":
1910 overlap_fraction = self.environment_parameters.sysid_overlap
1911 else:
1912 overlap_fraction = 0
1913 if self.environment_parameters.sysid_signal_type == "Burst Random":
1914 trigger_channel_index = reference_channel_indices[0]
1915 else:
1916 trigger_channel_index = 0
1917 trigger_slope = TriggerSlope.POSITIVE
1918 trigger_level = self.environment_parameters.sysid_level / 100
1919 trigger_hysteresis = self.environment_parameters.sysid_level / 200
1920 trigger_hysteresis_samples = (
1921 (1 - self.environment_parameters.sysid_burst_on)
1922 * self.environment_parameters.sysid_frame_size
1923 ) // 2
1924 pretrigger_fraction = self.environment_parameters.sysid_pretrigger
1925 frame_size = self.environment_parameters.sysid_frame_size
1926 window = (
1927 Window.HANN if self.environment_parameters.sysid_window == "Hann" else Window.RECTANGLE
1928 )
1929 kurtosis_buffer_length = self.environment_parameters.sysid_averages
1930
1931 return CollectorMetadata(
1932 num_channels,
1933 response_channel_indices,
1934 reference_channel_indices,
1935 acquisition_type,
1936 acceptance,
1937 acceptance_function,
1938 overlap_fraction,
1939 trigger_channel_index,
1940 trigger_slope,
1941 trigger_level,
1942 trigger_hysteresis,
1943 trigger_hysteresis_samples,
1944 pretrigger_fraction,
1945 frame_size,
1946 window,
1947 kurtosis_buffer_length=kurtosis_buffer_length,
1948 response_transformation_matrix=self.environment_parameters.response_transformation_matrix,
1949 reference_transformation_matrix=self.environment_parameters.reference_transformation_matrix,
1950 )
1951
1952 def get_sysid_spectral_processing_metadata(self, is_noise=False) -> SpectralProcessingMetadata:
1953 """Collects metadata to send to the spectral processing process"""
1954 averaging_type = (
1955 AveragingTypes.LINEAR
1956 if self.environment_parameters.sysid_averaging_type == "Linear"
1957 else AveragingTypes.EXPONENTIAL
1958 )
1959 averages = (
1960 self.environment_parameters.sysid_noise_averages
1961 if is_noise
1962 else self.environment_parameters.sysid_averages
1963 )
1964 exponential_averaging_coefficient = (
1965 self.environment_parameters.sysid_exponential_averaging_coefficient
1966 )
1967 if self.environment_parameters.sysid_estimator == "H1":
1968 frf_estimator = Estimator.H1
1969 elif self.environment_parameters.sysid_estimator == "H2":
1970 frf_estimator = Estimator.H2
1971 elif self.environment_parameters.sysid_estimator == "H3":
1972 frf_estimator = Estimator.H3
1973 elif self.environment_parameters.sysid_estimator == "Hv":
1974 frf_estimator = Estimator.HV
1975 else:
1976 raise ValueError(f"Invalid FRF Estimator {self.environment_parameters.sysid_estimator}")
1977 num_response_channels = self.environment_parameters.num_response_channels
1978 num_reference_channels = self.environment_parameters.num_reference_channels
1979 frequency_spacing = self.environment_parameters.sysid_frequency_spacing
1980 sample_rate = self.environment_parameters.sample_rate
1981 num_frequency_lines = self.environment_parameters.sysid_fft_lines
1982 return SpectralProcessingMetadata(
1983 averaging_type,
1984 averages,
1985 exponential_averaging_coefficient,
1986 frf_estimator,
1987 num_response_channels,
1988 num_reference_channels,
1989 frequency_spacing,
1990 sample_rate,
1991 num_frequency_lines,
1992 )
1993
1994 def get_sysid_signal_generation_metadata(self) -> SignalGenerationMetadata:
1995 """Collects metadata to send to the signal generation process"""
1996 return SignalGenerationMetadata(
1997 samples_per_write=self.data_acquisition_parameters.samples_per_write,
1998 level_ramp_samples=self.environment_parameters.sysid_level_ramp_time
1999 * self.environment_parameters.sample_rate
2000 * self.data_acquisition_parameters.output_oversample,
2001 output_transformation_matrix=self.environment_parameters.reference_transformation_matrix,
2002 )
2003
2004 def get_sysid_signal_generator(self) -> SignalGenerator:
2005 """Creates a signal generator object that will generate the signals"""
2006 if self.environment_parameters.sysid_signal_type == "Random":
2007 return RandomSignalGenerator(
2008 rms=self.environment_parameters.sysid_level,
2009 sample_rate=self.environment_parameters.sample_rate,
2010 num_samples_per_frame=self.environment_parameters.sysid_frame_size,
2011 num_signals=self.environment_parameters.num_reference_channels,
2012 low_frequency_cutoff=self.environment_parameters.sysid_low_frequency_cutoff,
2013 high_frequency_cutoff=self.environment_parameters.sysid_high_frequency_cutoff,
2014 cola_overlap=0.5,
2015 cola_window="hann",
2016 cola_exponent=0.5,
2017 output_oversample=self.data_acquisition_parameters.output_oversample,
2018 )
2019 elif self.environment_parameters.sysid_signal_type == "Pseudorandom":
2020 return PseudorandomSignalGenerator(
2021 rms=self.environment_parameters.sysid_level,
2022 sample_rate=self.environment_parameters.sample_rate,
2023 num_samples_per_frame=self.environment_parameters.sysid_frame_size,
2024 num_signals=self.environment_parameters.num_reference_channels,
2025 low_frequency_cutoff=self.environment_parameters.sysid_low_frequency_cutoff,
2026 high_frequency_cutoff=self.environment_parameters.sysid_high_frequency_cutoff,
2027 output_oversample=self.data_acquisition_parameters.output_oversample,
2028 )
2029 elif self.environment_parameters.sysid_signal_type == "Burst Random":
2030 return BurstRandomSignalGenerator(
2031 rms=self.environment_parameters.sysid_level,
2032 sample_rate=self.environment_parameters.sample_rate,
2033 num_samples_per_frame=self.environment_parameters.sysid_frame_size,
2034 num_signals=self.environment_parameters.num_reference_channels,
2035 low_frequency_cutoff=self.environment_parameters.sysid_low_frequency_cutoff,
2036 high_frequency_cutoff=self.environment_parameters.sysid_high_frequency_cutoff,
2037 on_fraction=self.environment_parameters.sysid_burst_on,
2038 ramp_fraction=self.environment_parameters.sysid_burst_ramp_fraction,
2039 output_oversample=self.data_acquisition_parameters.output_oversample,
2040 )
2041 elif self.environment_parameters.sysid_signal_type == "Chirp":
2042 return ChirpSignalGenerator(
2043 level=self.environment_parameters.sysid_level,
2044 sample_rate=self.environment_parameters.sample_rate,
2045 num_samples_per_frame=self.environment_parameters.sysid_frame_size,
2046 num_signals=self.environment_parameters.num_reference_channels,
2047 low_frequency_cutoff=np.max(
2048 [
2049 self.environment_parameters.sysid_frequency_spacing,
2050 self.environment_parameters.sysid_low_frequency_cutoff,
2051 ]
2052 ),
2053 high_frequency_cutoff=np.min(
2054 [
2055 self.environment_parameters.sample_rate / 2,
2056 self.environment_parameters.sysid_high_frequency_cutoff,
2057 ]
2058 ),
2059 output_oversample=self.data_acquisition_parameters.output_oversample,
2060 )
2061
2062 def load_noise(self, data):
2063 """Sends noise data to the data analysis process"""
2064 self.data_analysis_command_queue.put(
2065 self.environment_name, (SysIDDataAnalysisCommands.LOAD_NOISE, data)
2066 )
2067
2068 def load_transfer_function(self, data):
2069 """Sends transfer function data to the data analysis process"""
2070 self.data_analysis_command_queue.put(
2071 self.environment_name,
2072 (SysIDDataAnalysisCommands.LOAD_TRANSFER_FUNCTION, data),
2073 )
2074
2075 def preview_noise(self, data):
2076 """Starts up the noise preview with the defined metadata"""
2077 self.log("Starting Noise Preview")
2078 self.siggen_shutdown_achieved = False
2079 self.collector_shutdown_achieved = False
2080 self.spectral_shutdown_achieved = False
2081 self.analysis_shutdown_achieved = False
2082 self.environment_parameters = data
2083 # Start up controller
2084 self.controller_communication_queue.put(
2085 self.environment_name, (GlobalCommands.RUN_HARDWARE, None)
2086 )
2087 self.controller_communication_queue.put(
2088 self.environment_name,
2089 (GlobalCommands.START_ENVIRONMENT, self.environment_name),
2090 )
2091
2092 # Set up the collector
2093 collector_metadata = deepcopy(self.get_sysid_data_collector_metadata())
2094 collector_metadata.acquisition_type = AcquisitionType.FREE_RUN
2095 self.collector_command_queue.put(
2096 self.environment_name,
2097 (DataCollectorCommands.FORCE_INITIALIZE_COLLECTOR, collector_metadata),
2098 )
2099
2100 self.collector_command_queue.put(
2101 self.environment_name,
2102 (
2103 DataCollectorCommands.SET_TEST_LEVEL,
2104 (self.environment_parameters.sysid_skip_frames, 1),
2105 ),
2106 )
2107 time.sleep(0.01)
2108
2109 # Set up the signal generation
2110 self.signal_generator_command_queue.put(
2111 self.environment_name,
2112 (
2113 SignalGenerationCommands.INITIALIZE_PARAMETERS,
2114 self.get_sysid_signal_generation_metadata(),
2115 ),
2116 )
2117
2118 self.signal_generator_command_queue.put(
2119 self.environment_name,
2120 (
2121 SignalGenerationCommands.INITIALIZE_SIGNAL_GENERATOR,
2122 self.get_sysid_signal_generator(),
2123 ),
2124 )
2125
2126 self.signal_generator_command_queue.put(
2127 self.environment_name, (SignalGenerationCommands.MUTE, None)
2128 )
2129
2130 # Tell the collector to start acquiring data
2131 self.collector_command_queue.put(
2132 self.environment_name, (DataCollectorCommands.ACQUIRE, None)
2133 )
2134
2135 # Tell the signal generation to start generating signals
2136 self.signal_generator_command_queue.put(
2137 self.environment_name, (SignalGenerationCommands.GENERATE_SIGNALS, None)
2138 )
2139
2140 # Set up the data analysis
2141 self.data_analysis_command_queue.put(
2142 self.environment_name,
2143 (
2144 SysIDDataAnalysisCommands.INITIALIZE_PARAMETERS,
2145 self.environment_parameters,
2146 ),
2147 )
2148
2149 # Start the data analysis running
2150 self.data_analysis_command_queue.put(
2151 self.environment_name, (SysIDDataAnalysisCommands.RUN_NOISE, False)
2152 )
2153
2154 # Set up the spectral processing
2155 self.spectral_processing_command_queue.put(
2156 self.environment_name,
2157 (
2158 SpectralProcessingCommands.INITIALIZE_PARAMETERS,
2159 self.get_sysid_spectral_processing_metadata(is_noise=True),
2160 ),
2161 )
2162
2163 # Tell the spectral analysis to clear and start acquiring
2164 self.spectral_processing_command_queue.put(
2165 self.environment_name,
2166 (SpectralProcessingCommands.CLEAR_SPECTRAL_PROCESSING, None),
2167 )
2168
2169 self.spectral_processing_command_queue.put(
2170 self.environment_name,
2171 (SpectralProcessingCommands.RUN_SPECTRAL_PROCESSING, None),
2172 )
2173
2174 # Tell data collector to clear the kurtosis buffer
2175 self.collector_command_queue.put(
2176 self.environment_name, (DataCollectorCommands.CLEAR_KURTOSIS_BUFFER, None)
2177 )
2178
2179 def preview_transfer_function(self, data):
2180 """Starts up a transfer function preview with the provided environment metadata"""
2181 self.log("Starting System ID Preview")
2182 self.siggen_shutdown_achieved = False
2183 self.collector_shutdown_achieved = False
2184 self.spectral_shutdown_achieved = False
2185 self.analysis_shutdown_achieved = False
2186 self.environment_parameters = data
2187 # Start up controller
2188 self.controller_communication_queue.put(
2189 self.environment_name, (GlobalCommands.RUN_HARDWARE, None)
2190 )
2191 # Wait for the environment to start up
2192 while not (self.acquisition_active and self.output_active):
2193 # print('Waiting for Acquisition and Output to Start up')
2194 time.sleep(0.1)
2195 self.controller_communication_queue.put(
2196 self.environment_name,
2197 (GlobalCommands.START_ENVIRONMENT, self.environment_name),
2198 )
2199
2200 # Set up the collector
2201 self.collector_command_queue.put(
2202 self.environment_name,
2203 (
2204 DataCollectorCommands.FORCE_INITIALIZE_COLLECTOR,
2205 self.get_sysid_data_collector_metadata(),
2206 ),
2207 )
2208
2209 self.collector_command_queue.put(
2210 self.environment_name,
2211 (
2212 DataCollectorCommands.SET_TEST_LEVEL,
2213 (self.environment_parameters.sysid_skip_frames, 1),
2214 ),
2215 )
2216 time.sleep(0.01)
2217
2218 # Set up the signal generation
2219 self.signal_generator_command_queue.put(
2220 self.environment_name,
2221 (
2222 SignalGenerationCommands.INITIALIZE_PARAMETERS,
2223 self.get_sysid_signal_generation_metadata(),
2224 ),
2225 )
2226
2227 self.signal_generator_command_queue.put(
2228 self.environment_name,
2229 (
2230 SignalGenerationCommands.INITIALIZE_SIGNAL_GENERATOR,
2231 self.get_sysid_signal_generator(),
2232 ),
2233 )
2234
2235 self.signal_generator_command_queue.put(
2236 self.environment_name, (SignalGenerationCommands.MUTE, None)
2237 )
2238
2239 self.signal_generator_command_queue.put(
2240 self.environment_name, (SignalGenerationCommands.ADJUST_TEST_LEVEL, 1.0)
2241 )
2242
2243 # Tell the collector to start acquiring data
2244 self.collector_command_queue.put(
2245 self.environment_name, (DataCollectorCommands.ACQUIRE, None)
2246 )
2247
2248 # Tell the signal generation to start generating signals
2249 self.signal_generator_command_queue.put(
2250 self.environment_name, (SignalGenerationCommands.GENERATE_SIGNALS, None)
2251 )
2252
2253 # Set up the data analysis
2254 self.data_analysis_command_queue.put(
2255 self.environment_name,
2256 (
2257 SysIDDataAnalysisCommands.INITIALIZE_PARAMETERS,
2258 self.environment_parameters,
2259 ),
2260 )
2261
2262 # Start the data analysis running
2263 self.data_analysis_command_queue.put(
2264 self.environment_name,
2265 (SysIDDataAnalysisCommands.RUN_TRANSFER_FUNCTION, False),
2266 )
2267
2268 # Set up the spectral processing
2269 self.spectral_processing_command_queue.put(
2270 self.environment_name,
2271 (
2272 SpectralProcessingCommands.INITIALIZE_PARAMETERS,
2273 self.get_sysid_spectral_processing_metadata(is_noise=False),
2274 ),
2275 )
2276
2277 # Tell the spectral analysis to clear and start acquiring
2278 self.spectral_processing_command_queue.put(
2279 self.environment_name,
2280 (SpectralProcessingCommands.CLEAR_SPECTRAL_PROCESSING, None),
2281 )
2282
2283 self.spectral_processing_command_queue.put(
2284 self.environment_name,
2285 (SpectralProcessingCommands.RUN_SPECTRAL_PROCESSING, None),
2286 )
2287
2288 # Tell data collector to clear the kurtosis buffer
2289 self.collector_command_queue.put(
2290 self.environment_name, (DataCollectorCommands.CLEAR_KURTOSIS_BUFFER, None)
2291 )
2292
2293 def start_noise(self, data):
2294 """Starts the noise measurement with the provided metadata"""
2295 self.log("Starting Noise Measurement for System ID")
2296 self.siggen_shutdown_achieved = False
2297 self.collector_shutdown_achieved = False
2298 self.spectral_shutdown_achieved = False
2299 self.analysis_shutdown_achieved = False
2300 self.environment_parameters, self._sysid_stream_name = data
2301 self.controller_communication_queue.put(
2302 self.environment_name,
2303 (
2304 GlobalCommands.UPDATE_METADATA,
2305 (self.environment_name, self.environment_parameters),
2306 ),
2307 )
2308 # Start up controller
2309 if self._sysid_stream_name is not None:
2310 self.controller_communication_queue.put(
2311 self.environment_name,
2312 (GlobalCommands.INITIALIZE_STREAMING, self._sysid_stream_name),
2313 )
2314 self.controller_communication_queue.put(
2315 self.environment_name, (GlobalCommands.START_STREAMING, None)
2316 )
2317 self.controller_communication_queue.put(
2318 self.environment_name, (GlobalCommands.RUN_HARDWARE, None)
2319 )
2320 self.controller_communication_queue.put(
2321 self.environment_name,
2322 (GlobalCommands.START_ENVIRONMENT, self.environment_name),
2323 )
2324
2325 # Set up the collector
2326 collector_metadata = deepcopy(self.get_sysid_data_collector_metadata())
2327 collector_metadata.acquisition_type = AcquisitionType.FREE_RUN
2328 self.collector_command_queue.put(
2329 self.environment_name,
2330 (DataCollectorCommands.FORCE_INITIALIZE_COLLECTOR, collector_metadata),
2331 )
2332
2333 self.collector_command_queue.put(
2334 self.environment_name,
2335 (
2336 DataCollectorCommands.SET_TEST_LEVEL,
2337 (self.environment_parameters.sysid_skip_frames, 1),
2338 ),
2339 )
2340 time.sleep(0.01)
2341
2342 # Set up the signal generation
2343 self.signal_generator_command_queue.put(
2344 self.environment_name,
2345 (
2346 SignalGenerationCommands.INITIALIZE_PARAMETERS,
2347 self.get_sysid_signal_generation_metadata(),
2348 ),
2349 )
2350
2351 self.signal_generator_command_queue.put(
2352 self.environment_name,
2353 (
2354 SignalGenerationCommands.INITIALIZE_SIGNAL_GENERATOR,
2355 self.get_sysid_signal_generator(),
2356 ),
2357 )
2358
2359 self.signal_generator_command_queue.put(
2360 self.environment_name, (SignalGenerationCommands.MUTE, None)
2361 )
2362
2363 # Tell the collector to start acquiring data
2364 self.collector_command_queue.put(
2365 self.environment_name, (DataCollectorCommands.ACQUIRE, None)
2366 )
2367
2368 # Tell the signal generation to start generating signals
2369 self.signal_generator_command_queue.put(
2370 self.environment_name, (SignalGenerationCommands.GENERATE_SIGNALS, None)
2371 )
2372
2373 # Set up the data analysis
2374 self.data_analysis_command_queue.put(
2375 self.environment_name,
2376 (
2377 SysIDDataAnalysisCommands.INITIALIZE_PARAMETERS,
2378 self.environment_parameters,
2379 ),
2380 )
2381
2382 # Start the data analysis running
2383 self.data_analysis_command_queue.put(
2384 self.environment_name, (SysIDDataAnalysisCommands.RUN_NOISE, True)
2385 )
2386
2387 # Set up the spectral processing
2388 self.spectral_processing_command_queue.put(
2389 self.environment_name,
2390 (
2391 SpectralProcessingCommands.INITIALIZE_PARAMETERS,
2392 self.get_sysid_spectral_processing_metadata(is_noise=True),
2393 ),
2394 )
2395
2396 # Tell the spectral analysis to clear and start acquiring
2397 self.spectral_processing_command_queue.put(
2398 self.environment_name,
2399 (SpectralProcessingCommands.CLEAR_SPECTRAL_PROCESSING, None),
2400 )
2401
2402 self.spectral_processing_command_queue.put(
2403 self.environment_name,
2404 (SpectralProcessingCommands.RUN_SPECTRAL_PROCESSING, None),
2405 )
2406
2407 # Tell data collector to clear the kurtosis buffer
2408 self.collector_command_queue.put(
2409 self.environment_name, (DataCollectorCommands.CLEAR_KURTOSIS_BUFFER, None)
2410 )
2411
2412 def start_transfer_function(self, data):
2413 """Starts the transfer function measurement with the provided metadata"""
2414 self.log("Starting Transfer Function for System ID")
2415 self.siggen_shutdown_achieved = False
2416 self.collector_shutdown_achieved = False
2417 self.spectral_shutdown_achieved = False
2418 self.analysis_shutdown_achieved = False
2419 self.environment_parameters = data
2420 # Start up controller
2421 if self._sysid_stream_name is not None:
2422 self.controller_communication_queue.put(
2423 self.environment_name, (GlobalCommands.START_STREAMING, None)
2424 )
2425
2426 self.controller_communication_queue.put(
2427 self.environment_name,
2428 (GlobalCommands.START_ENVIRONMENT, self.environment_name),
2429 )
2430
2431 # Set up the collector
2432 self.collector_command_queue.put(
2433 self.environment_name,
2434 (
2435 DataCollectorCommands.FORCE_INITIALIZE_COLLECTOR,
2436 self.get_sysid_data_collector_metadata(),
2437 ),
2438 )
2439
2440 self.collector_command_queue.put(
2441 self.environment_name,
2442 (
2443 DataCollectorCommands.SET_TEST_LEVEL,
2444 (self.environment_parameters.sysid_skip_frames, 1),
2445 ),
2446 )
2447 time.sleep(0.01)
2448
2449 # Set up the signal generation
2450 self.signal_generator_command_queue.put(
2451 self.environment_name,
2452 (
2453 SignalGenerationCommands.INITIALIZE_PARAMETERS,
2454 self.get_sysid_signal_generation_metadata(),
2455 ),
2456 )
2457
2458 self.signal_generator_command_queue.put(
2459 self.environment_name,
2460 (
2461 SignalGenerationCommands.INITIALIZE_SIGNAL_GENERATOR,
2462 self.get_sysid_signal_generator(),
2463 ),
2464 )
2465
2466 self.signal_generator_command_queue.put(
2467 self.environment_name, (SignalGenerationCommands.MUTE, None)
2468 )
2469
2470 self.signal_generator_command_queue.put(
2471 self.environment_name, (SignalGenerationCommands.ADJUST_TEST_LEVEL, 1.0)
2472 )
2473
2474 # Tell the collector to start acquiring data
2475 self.collector_command_queue.put(
2476 self.environment_name, (DataCollectorCommands.ACQUIRE, None)
2477 )
2478
2479 # Tell the signal generation to start generating signals
2480 self.signal_generator_command_queue.put(
2481 self.environment_name, (SignalGenerationCommands.GENERATE_SIGNALS, None)
2482 )
2483
2484 # Set up the data analysis
2485 self.data_analysis_command_queue.put(
2486 self.environment_name,
2487 (
2488 SysIDDataAnalysisCommands.INITIALIZE_PARAMETERS,
2489 self.environment_parameters,
2490 ),
2491 )
2492
2493 # Start the data analysis running
2494 self.data_analysis_command_queue.put(
2495 self.environment_name,
2496 (SysIDDataAnalysisCommands.RUN_TRANSFER_FUNCTION, True),
2497 )
2498
2499 # Set up the spectral processing
2500 self.spectral_processing_command_queue.put(
2501 self.environment_name,
2502 (
2503 SpectralProcessingCommands.INITIALIZE_PARAMETERS,
2504 self.get_sysid_spectral_processing_metadata(is_noise=False),
2505 ),
2506 )
2507
2508 # Tell the spectral analysis to clear and start acquiring
2509 self.spectral_processing_command_queue.put(
2510 self.environment_name,
2511 (SpectralProcessingCommands.CLEAR_SPECTRAL_PROCESSING, None),
2512 )
2513
2514 self.spectral_processing_command_queue.put(
2515 self.environment_name,
2516 (SpectralProcessingCommands.RUN_SPECTRAL_PROCESSING, None),
2517 )
2518
2519 # Tell data collector to clear the kurtosis buffer
2520 self.collector_command_queue.put(
2521 self.environment_name, (DataCollectorCommands.CLEAR_KURTOSIS_BUFFER, None)
2522 )
2523
2524 def stop_system_id(self, stop_tasks):
2525 """Starts the shutdown process for the system identification"""
2526 stop_data_analysis, stop_hardware = stop_tasks
2527 self.log("Stop Transfer Function")
2528 if stop_hardware:
2529 self.controller_communication_queue.put(
2530 self.environment_name, (GlobalCommands.STOP_HARDWARE, None)
2531 )
2532 elif self._sysid_stream_name is not None:
2533 self.controller_communication_queue.put(
2534 self.environment_name, (GlobalCommands.STOP_STREAMING, None)
2535 )
2536 self.collector_command_queue.put(
2537 self.environment_name,
2538 (
2539 DataCollectorCommands.SET_TEST_LEVEL,
2540 (self.environment_parameters.sysid_skip_frames * 10, 1),
2541 ),
2542 )
2543 self.signal_generator_command_queue.put(
2544 self.environment_name, (SignalGenerationCommands.START_SHUTDOWN, None)
2545 )
2546 self.spectral_processing_command_queue.put(
2547 self.environment_name,
2548 (SpectralProcessingCommands.STOP_SPECTRAL_PROCESSING, None),
2549 )
2550 if stop_data_analysis:
2551 self.data_analysis_command_queue.put(
2552 self.environment_name, (SysIDDataAnalysisCommands.STOP_SYSTEM_ID, None)
2553 )
2554 self.environment_command_queue.put(
2555 self.environment_name, (SystemIdCommands.CHECK_FOR_COMPLETE_SHUTDOWN, None)
2556 )
2557
2558 def siggen_shutdown_achieved_fn(self, data): # pylint: disable=unused-argument
2559 """Sets the sshutdown flag to denote the signal generation has shut down successfully"""
2560 self.siggen_shutdown_achieved = True
2561
2562 def collector_shutdown_achieved_fn(self, data): # pylint: disable=unused-argument
2563 """Sets the shutdown flag to denote the data collector has shut down successfully"""
2564 self.collector_shutdown_achieved = True
2565
2566 def spectral_shutdown_achieved_fn(self, data): # pylint: disable=unused-argument
2567 """Sets the shutdown flag to denote the spectral computation has shut down successfully"""
2568 self.spectral_shutdown_achieved = True
2569
2570 def analysis_shutdown_achieved_fn(self, data): # pylint: disable=unused-argument
2571 """Sets the shutdown flag to denote the data analysis has shut down successfully"""
2572 self.analysis_shutdown_achieved = True
2573
2574 def check_for_sysid_shutdown(self, data): # pylint: disable=unused-argument
2575 """Checks that all of the relevant system identification processes have shut down"""
2576 if (
2577 self.siggen_shutdown_achieved
2578 and self.collector_shutdown_achieved
2579 and self.spectral_shutdown_achieved
2580 and self.analysis_shutdown_achieved
2581 and ((not self.acquisition_active) or self._waiting_to_start_transfer_function)
2582 and ((not self.output_active) or self._waiting_to_start_transfer_function)
2583 ):
2584 self.log("Shutdown Achieved")
2585 if self._waiting_to_start_transfer_function:
2586 self.start_transfer_function(self.environment_parameters)
2587 else:
2588 self.gui_update_queue.put((self.environment_name, ("enable_system_id", None)))
2589 self._sysid_stream_name = None
2590 self._waiting_to_start_transfer_function = False
2591 else:
2592 # Recheck some time later
2593 time.sleep(1)
2594 waiting_for = []
2595 if not self.siggen_shutdown_achieved:
2596 waiting_for.append("Signal Generation")
2597 if not self.collector_shutdown_achieved:
2598 waiting_for.append("Collector")
2599 if not self.spectral_shutdown_achieved:
2600 waiting_for.append("Spectral Processing")
2601 if not self.analysis_shutdown_achieved:
2602 waiting_for.append("Data Analysis")
2603 if self.output_active and (not self._waiting_to_start_transfer_function):
2604 waiting_for.append("Output Shutdown")
2605 if self.acquisition_active and (not self._waiting_to_start_transfer_function):
2606 waiting_for.append("Acquisition Shutdown")
2607 self.log(f"Waiting for {' and '.join(waiting_for)}")
2608 self.environment_command_queue.put(
2609 self.environment_name,
2610 (SystemIdCommands.CHECK_FOR_COMPLETE_SHUTDOWN, None),
2611 )
2612
2613 def start_shutdown_and_run_sysid(self, data): # pylint: disable=unused-argument
2614 """After successful noise run, shut down and start up system identification"""
2615 self.log("Shutting down and then Running System ID Afterwards")
2616 self._waiting_to_start_transfer_function = True
2617 self.stop_system_id((False, False))
2618
2619 def system_id_complete(self, data):
2620 """Sends a message to the controller that this environment has completed system id"""
2621 self.log("Finished System Identification")
2622 self.controller_communication_queue.put(
2623 self.environment_name,
2624 (GlobalCommands.COMPLETED_SYSTEM_ID, (self.environment_name, data)),
2625 )
2626
2627 @abstractmethod
2628 def stop_environment(self, data):
2629 """Stop the environment gracefully
2630
2631 This function defines the operations to shut down the environment
2632 gracefully so there is no hard stop that might damage test equipment
2633 or parts.
2634
2635 Parameters
2636 ----------
2637 data : Ignored
2638 This parameter is not used by the function but must be present
2639 due to the calling signature of functions called through the
2640 ``command_map``
2641
2642 """
2643
2644 def quit(self, data):
2645 """Closes down the environment when quitting the software"""
2646 for queue in [
2647 self.collector_command_queue,
2648 self.signal_generator_command_queue,
2649 self.spectral_processing_command_queue,
2650 self.data_analysis_command_queue,
2651 ]:
2652 queue.put(self.environment_name, (GlobalCommands.QUIT, None))
2653 # Return true to stop the task
2654 return True