1# -*- coding: utf-8 -*-
2"""
3This file defines a sine environment that utilizes system
4identification.
5
6Rattlesnake Vibration Control Software
7Copyright (C) 2021 National Technology & Engineering Solutions of Sandia, LLC
8(NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S.
9Government retains certain rights in this software.
10
11This program is free software: you can redistribute it and/or modify
12it under the terms of the GNU General Public License as published by
13the Free Software Foundation, either version 3 of the License, or
14(at your option) any later version.
15
16This program is distributed in the hope that it will be useful,
17but WITHOUT ANY WARRANTY; without even the implied warranty of
18MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19GNU General Public License for more details.
20
21You should have received a copy of the GNU General Public License
22along with this program. If not, see <https://www.gnu.org/licenses/>.
23"""
24
25import importlib
26import inspect
27import multiprocessing as mp
28import os
29import time
30import traceback
31from enum import Enum
32from multiprocessing.queues import Queue
33
34import netCDF4 as nc4
35import numpy as np
36import scipy.signal as sig
37from qtpy import QtWidgets, uic
38from qtpy.QtCore import Qt
39from qtpy.QtGui import QColor # pylint: disable=no-name-in-module
40
41from .abstract_sysid_environment import (
42 AbstractSysIdEnvironment,
43 AbstractSysIdMetadata,
44 AbstractSysIdUI,
45)
46from .environments import (
47 ControlTypes,
48 environment_definition_ui_paths,
49 environment_prediction_ui_paths,
50 environment_run_ui_paths,
51)
52from .sine_sys_id_utilities import (
53 DefaultSineControlLaw,
54 FilterExplorer,
55 PlotSineWindow,
56 SineSpecification,
57 SineSweepTable,
58 digital_tracking_filter_generator,
59 sine_sweep,
60 vold_kalman_filter_generator,
61)
62from .ui_utilities import (
63 TransformationMatrixWindow,
64 VaryingNumberOfLinePlot,
65 blended_scatter_plot,
66 multiline_plotter,
67)
68from .utilities import (
69 GlobalCommands,
70 VerboseMessageQueue,
71 db2scale,
72 flush_queue,
73 load_python_module,
74 scale2db,
75 wrap,
76)
77
78# %% Global Variables
79CONTROL_TYPE = ControlTypes.SINE
80MAXIMUM_NAME_LENGTH = 50
81MAXIMUM_SAMPLES_TO_PLOT = 1000000
82
83DEBUG = False
84
85if DEBUG:
86 from glob import glob
87
88 FILE_OUTPUT = "debug_data/sine_control_{:}.npz"
89
90
91# %% Commands
92class SineCommands(Enum):
93 """Enumeration containing sine commands"""
94
95 START_CONTROL = 0
96 STOP_CONTROL = 1
97 SAVE_CONTROL_DATA = 2
98 PERFORM_CONTROL_PREDICTION = 3
99 SEND_EXCITATION_PREDICTION = 4
100 SEND_RESPONSE_PREDICTION = 5
101
102
103# %% Queues
104class SineQueues:
105 """A container class for the queues that this environment will manage."""
106
107 def __init__(
108 self,
109 environment_name: str,
110 environment_command_queue: VerboseMessageQueue,
111 gui_update_queue: Queue,
112 controller_communication_queue: VerboseMessageQueue,
113 data_in_queue: Queue,
114 data_out_queue: Queue,
115 log_file_queue: VerboseMessageQueue,
116 ):
117 """A container class for the queues that sine vibration will manage.
118
119 The environment uses many queues to pass data between the various
120 pieces. This class organizes those queues into one common namespace.
121
122 Parameters
123 ----------
124 environment_name : str
125 Name of the environment
126 environment_command_queue : VerboseMessageQueue
127 Queue that is read by the environment for environment commands
128 gui_update_queue : mp.queues.Queue
129 Queue where various subtasks put instructions for updating the
130 widgets in the user interface
131 controller_communication_queue : VerboseMessageQueue
132 Queue that is read by the controller for global controller commands
133 data_in_queue : mp.queues.Queue
134 Multiprocessing queue that connects the acquisition subtask to the
135 environment subtask. Each environment will retrieve acquired data
136 from this queue.
137 data_out_queue : mp.queues.Queue
138 Multiprocessing queue that connects the output subtask to the
139 environment subtask. Each environment will put data that it wants
140 the controller to generate in this queue.
141 log_file_queue : VerboseMessageQueue
142 Queue for putting logging messages that will be read by the logging
143 subtask and written to a file.
144 """
145 self.environment_command_queue = environment_command_queue
146 self.gui_update_queue = gui_update_queue
147 self.data_analysis_command_queue = VerboseMessageQueue(
148 log_file_queue, environment_name + " Data Analysis Command Queue"
149 )
150 self.signal_generation_command_queue = VerboseMessageQueue(
151 log_file_queue, environment_name + " Signal Generation Command Queue"
152 )
153 self.spectral_command_queue = VerboseMessageQueue(
154 log_file_queue, environment_name + " Spectral Computation Command Queue"
155 )
156 self.collector_command_queue = VerboseMessageQueue(
157 log_file_queue, environment_name + " Data Collector Command Queue"
158 )
159 self.controller_communication_queue = controller_communication_queue
160 self.data_in_queue = data_in_queue
161 self.data_out_queue = data_out_queue
162 self.data_for_spectral_computation_queue = mp.Queue()
163 self.updated_spectral_quantities_queue = mp.Queue()
164 self.time_history_to_generate_queue = mp.Queue()
165 self.log_file_queue = log_file_queue
166
167
168# %% Metadata
169class SineMetadata(AbstractSysIdMetadata):
170 """Metadata describing the Sine environment"""
171
172 def __init__(
173 self,
174 *,
175 sample_rate,
176 samples_per_frame,
177 number_of_channels,
178 specifications,
179 ramp_time,
180 buffer_blocks,
181 control_convergence,
182 update_drives_after_environment,
183 phase_fit,
184 allow_automatic_aborts,
185 tracking_filter_type,
186 tracking_filter_cutoff,
187 tracking_filter_order,
188 vk_filter_order,
189 vk_filter_bandwidth,
190 vk_filter_blocksize,
191 vk_filter_overlap,
192 control_python_script,
193 control_python_class,
194 control_python_parameters,
195 control_channel_indices,
196 output_channel_indices,
197 response_transformation_matrix,
198 output_transformation_matrix,
199 ):
200 """Creates a Metadata object defining a Sine environment
201
202 Parameters
203 ----------
204 sample_rate : float
205 The sample rate in Hz
206 samples_per_frame : int
207 The number of samples per acquisition block
208 number_of_channels : int
209 The number of channels in the environment
210 specifications : array of SineSpecification
211 One SineSpecification object for each tone in the environment
212 ramp_time : float
213 The time to ramp to the initial level and ramp down from the final level
214 buffer_blocks : int
215 The number of blocks of data to use in various buffered activities,
216 such as filtering and overlapping and adding data
217 control_convergence : float
218 A value between 0 and 1 that controls the proportional correction
219 factor in the on-line control.
220 update_drives_after_environment : bool
221 If True, the control class will call the finalize_control method to
222 update the preshaped drives based on the results of the previous test
223 phase_fit : bool
224 If True, the achieved phases will be best-fit to the specification
225 phase. This should handle time delays between acquisition and output
226 allow_automatic_aborts : bool
227 If True, the test will shut down if an abort level is reached
228 tracking_filter_type : int
229 0 for digital tracking filter, 1 for vold kalman filter
230 tracking_filter_cutoff : float
231 Filter cutoff for the digital tracking filter.
232 tracking_filter_order : int
233 Filter order for the digital tracking filter.
234 vk_filter_order : int
235 Order of the Vold-Kalman filter.
236 vk_filter_bandwidth : float
237 Bandwidth of the Vold-Kalman filter
238 vk_filter_blocksize : int
239 Number of samples in each Vold-Kalman filter segment
240 vk_filter_overlap : float
241 Overlap fraction between 0 and 0.5 for the Vold-Kalman filter
242 control_python_script : str
243 Path to a Python script containing an alternative Sine control law class
244 control_python_class : str
245 Name of the sine control law class
246 control_python_parameters : str
247 Extra parameters passed to the sine control law
248 control_channel_indices : array of int
249 Indices into the channels specifying the channels used as control channels
250 output_channel_indices : array of int
251 Indices into the channels specifying the channels used as drive channels
252 response_transformation_matrix : ndarray
253 A 2D np.ndarray consisting of a transformation matrix applied to the
254 control channels
255 output_transformation_matrix : _type_
256 A 2D np.ndarray consisting of a transformation matrix applied to the
257 drive channels
258 """
259 super().__init__()
260 self.sample_rate = sample_rate
261 self.samples_per_frame = samples_per_frame
262 self.number_of_channels = number_of_channels
263 self.specifications = specifications
264 self.ramp_time = ramp_time
265 self.buffer_blocks = buffer_blocks
266 self.control_convergence = control_convergence
267 self.update_drives_after_environment = update_drives_after_environment
268 self.phase_fit = phase_fit
269 self.allow_automatic_aborts = allow_automatic_aborts
270 self.tracking_filter_type = tracking_filter_type
271 self.tracking_filter_cutoff = tracking_filter_cutoff
272 self.tracking_filter_order = tracking_filter_order
273 self.vk_filter_order = vk_filter_order
274 self.vk_filter_bandwidth = vk_filter_bandwidth
275 self.vk_filter_blocksize = vk_filter_blocksize
276 self.vk_filter_overlap = vk_filter_overlap
277 self.control_python_script = control_python_script
278 self.control_python_class = control_python_class
279 self.control_python_parameters = control_python_parameters
280 self.control_channel_indices = control_channel_indices
281 self.output_channel_indices = output_channel_indices
282 self.response_transformation_matrix = response_transformation_matrix
283 self.reference_transformation_matrix = output_transformation_matrix
284
285 @property
286 def sample_rate(self):
287 """Sample rate of the data acquisition system"""
288 return self._sample_rate
289
290 @sample_rate.setter
291 def sample_rate(self, value):
292 """Sets the stored sample rate parameter"""
293 self._sample_rate = value
294
295 @property
296 def ramp_samples(self):
297 """Number of samples in the ramp time"""
298 return int(self.ramp_time * self.sample_rate)
299
300 @property
301 def number_of_channels(self):
302 """Total number of channels in the environment"""
303 return self._number_of_channels
304
305 @number_of_channels.setter
306 def number_of_channels(self, value):
307 """Sets the stored value of the number of channels in the environment"""
308 self._number_of_channels = value
309
310 @property
311 def reference_channel_indices(self):
312 """Indices corresponding to the drive channels"""
313 return self.output_channel_indices
314
315 @property
316 def response_channel_indices(self):
317 """Indices corresponding to the control channels"""
318 return self.control_channel_indices
319
320 @property
321 def response_transformation_matrix(self):
322 """Transformation matrix applied to the control channels"""
323 return self._response_transformation_matrix
324
325 @response_transformation_matrix.setter
326 def response_transformation_matrix(self, value):
327 """Sets the transformation matrix applied to the control channels"""
328 self._response_transformation_matrix = value
329
330 @property
331 def reference_transformation_matrix(self):
332 """Transformation matrix applied to the drive channels"""
333 return self._reference_transformation_matrix
334
335 @reference_transformation_matrix.setter
336 def reference_transformation_matrix(self, value):
337 """Sets the transformation matrix applied to the drive channels"""
338 self._reference_transformation_matrix = value
339
340 def store_to_netcdf(
341 self,
342 netcdf_group_handle: nc4._netCDF4.Group, # pylint: disable=c-extension-no-member
343 ):
344 """Store parameters to a group in a netCDF streaming file.
345
346 This function stores parameters from the environment into the netCDF
347 file in a group with the environment's name as its name. The function
348 will receive a reference to the group within the dataset and should
349 store the environment's parameters into that group in the form of
350 attributes, dimensions, or variables.
351
352 This function is the "write" counterpart to the retrieve_metadata
353 function in the AbstractUI class, which will read parameters from
354 the netCDF file to populate the parameters in the user interface.
355
356 Parameters
357 ----------
358 netcdf_group_handle : nc4._netCDF4.Group
359 A reference to the Group within the netCDF dataset where the
360 environment's metadata is stored.
361
362 """
363 super().store_to_netcdf(netcdf_group_handle)
364
365 netcdf_group_handle.sample_rate = self.sample_rate
366 netcdf_group_handle.samples_per_frame = self.samples_per_frame
367 netcdf_group_handle.ramp_time = self.ramp_time
368
369 netcdf_group_handle.number_of_channels = self.number_of_channels
370 netcdf_group_handle.update_drives_after_environment = (
371 1 if self.update_drives_after_environment else 0
372 )
373 netcdf_group_handle.phase_fit = 1 if self.phase_fit else 0
374 netcdf_group_handle.control_convergence = self.control_convergence
375 netcdf_group_handle.allow_automatic_aborts = 1 if self.allow_automatic_aborts else 0
376 netcdf_group_handle.control_python_script = (
377 "" if self.control_python_script is None else self.control_python_script
378 )
379 netcdf_group_handle.control_python_class = (
380 "" if self.control_python_script is None else self.control_python_class
381 )
382 netcdf_group_handle.control_python_parameters = (
383 "" if self.control_python_script is None else self.control_python_parameters
384 )
385 netcdf_group_handle.tracking_filter_type = self.tracking_filter_type
386 netcdf_group_handle.tracking_filter_cutoff = self.tracking_filter_cutoff
387 netcdf_group_handle.tracking_filter_order = self.tracking_filter_order
388 netcdf_group_handle.vk_filter_order = self.vk_filter_order
389 netcdf_group_handle.vk_filter_bandwidth = self.vk_filter_bandwidth
390 netcdf_group_handle.vk_filter_blocksize = self.vk_filter_blocksize
391 netcdf_group_handle.vk_filter_overlap = self.vk_filter_overlap
392 netcdf_group_handle.buffer_blocks = self.buffer_blocks
393
394 # Control channels
395 netcdf_group_handle.createDimension("control_channels", len(self.control_channel_indices))
396 var = netcdf_group_handle.createVariable(
397 "control_channel_indices", "i4", ("control_channels")
398 )
399 var[...] = self.control_channel_indices
400 # Transformation matrices
401 if self.response_transformation_matrix is not None:
402 netcdf_group_handle.createDimension(
403 "response_transformation_rows",
404 self.response_transformation_matrix.shape[0],
405 )
406 netcdf_group_handle.createDimension(
407 "response_transformation_cols",
408 self.response_transformation_matrix.shape[1],
409 )
410 var = netcdf_group_handle.createVariable(
411 "response_transformation_matrix",
412 "f8",
413 ("response_transformation_rows", "response_transformation_cols"),
414 )
415 var[...] = self.response_transformation_matrix
416 if self.reference_transformation_matrix is not None:
417 netcdf_group_handle.createDimension(
418 "reference_transformation_rows",
419 self.reference_transformation_matrix.shape[0],
420 )
421 netcdf_group_handle.createDimension(
422 "reference_transformation_cols",
423 self.reference_transformation_matrix.shape[1],
424 )
425 var = netcdf_group_handle.createVariable(
426 "reference_transformation_matrix",
427 "f8",
428 ("reference_transformation_rows", "reference_transformation_cols"),
429 )
430 var[...] = self.reference_transformation_matrix
431 # Specification
432 spec_group = netcdf_group_handle.createGroup("specifications")
433 for specification in self.specifications:
434 specification: SineSpecification
435 grp = spec_group.createGroup(specification.name)
436 grp.start_time = specification.start_time
437 grp.createDimension("num_breakpoints", len(specification.breakpoint_table))
438 grp.createDimension(
439 "specification_channels",
440 specification.breakpoint_table["amplitude"].shape[-1],
441 )
442 grp.createDimension("two", 2)
443 var = grp.createVariable("spec_frequency", "f8", ("num_breakpoints"))
444 var[...] = specification.breakpoint_table["frequency"]
445 var = grp.createVariable(
446 "spec_amplitude", "f8", ("num_breakpoints", "specification_channels")
447 )
448 var[...] = specification.breakpoint_table["amplitude"]
449 var = grp.createVariable(
450 "spec_phase", "f8", ("num_breakpoints", "specification_channels")
451 )
452 var[...] = specification.breakpoint_table["phase"]
453 var = grp.createVariable("spec_sweep_type", "i1", ("num_breakpoints"))
454 var[...] = specification.breakpoint_table["sweep_type"]
455 var = grp.createVariable("spec_sweep_rate", "f8", ("num_breakpoints"))
456 var[...] = specification.breakpoint_table["sweep_rate"]
457 var = grp.createVariable(
458 "spec_warning",
459 "f8",
460 ("num_breakpoints", "two", "two", "specification_channels"),
461 )
462 var[...] = specification.breakpoint_table["warning"]
463 var = grp.createVariable(
464 "spec_abort",
465 "f8",
466 ("num_breakpoints", "two", "two", "specification_channels"),
467 )
468 var[...] = specification.breakpoint_table["abort"]
469
470
471# %% Additional Imports
472# These need to be here to avoid circular imports
473from .abstract_sysid_data_analysis import ( # pylint: disable=wrong-import-position # noqa: E402
474 sysid_data_analysis_process,
475)
476from .data_collector import ( # pylint: disable=wrong-import-position # noqa: E402
477 data_collector_process,
478)
479from .signal_generation import ( # pylint: disable=wrong-import-position # noqa: E402
480 ContinuousTransientSignalGenerator,
481)
482from .signal_generation_process import ( # pylint: disable=wrong-import-position # noqa: E402
483 SignalGenerationCommands,
484 SignalGenerationMetadata,
485 signal_generation_process,
486)
487from .spectral_processing import ( # pylint: disable=wrong-import-position # noqa: E402
488 spectral_processing_process,
489)
490
491
492# %% UI
493class SineUI(AbstractSysIdUI):
494 """Class to represent the user interface of the MIMO sine module"""
495
496 def __init__(
497 self,
498 environment_name: str,
499 definition_tabwidget: QtWidgets.QTabWidget,
500 system_id_tabwidget: QtWidgets.QTabWidget,
501 test_predictions_tabwidget: QtWidgets.QTabWidget,
502 run_tabwidget: QtWidgets.QTabWidget,
503 environment_command_queue: VerboseMessageQueue,
504 controller_communication_queue: VerboseMessageQueue,
505 log_file_queue: Queue,
506 ):
507 """Initializes a Sine Environment User Interface
508
509 Parameters
510 ----------
511 environment_name : str
512 The name of the environment
513 definition_tabwidget : QtWidgets.QTabWidget
514 The tab widget containing the environment definitions, into which the
515 definition widget will be placed.
516 system_id_tabwidget : QtWidgets.QTabWidget
517 The tab widget containing the system identification operations, into
518 which the system id widget is placed by the abstract parent class
519 test_predictions_tabwidget : QtWidgets.QTabWidget
520 The tab widget containing the test predictions, into which the
521 prediction widget will be placed
522 run_tabwidget : QtWidgets.QTabWidget
523 The tab widget containing the operations to run the environment,
524 into which the run widget will be placed.
525 environment_command_queue : VerboseMessageQueue
526 The queue to put commands for the environment process
527 controller_communication_queue : VerboseMessageQueue
528 The queue to put commands for the environment
529 log_file_queue : Queue
530 The queue to put logging information
531 """
532 super().__init__(
533 environment_name,
534 environment_command_queue,
535 controller_communication_queue,
536 log_file_queue,
537 system_id_tabwidget,
538 )
539 # Add the page to the control definition tabwidget
540 self.definition_widget = QtWidgets.QWidget()
541 uic.loadUi(environment_definition_ui_paths[CONTROL_TYPE], self.definition_widget)
542 definition_tabwidget.addTab(self.definition_widget, self.environment_name)
543 # Add the page to the control prediction tabwidget
544 self.prediction_widget = QtWidgets.QWidget()
545 uic.loadUi(environment_prediction_ui_paths[CONTROL_TYPE], self.prediction_widget)
546 test_predictions_tabwidget.addTab(self.prediction_widget, self.environment_name)
547 # Add the page to the run tabwidget
548 self.run_widget = QtWidgets.QWidget()
549 uic.loadUi(environment_run_ui_paths[CONTROL_TYPE], self.run_widget)
550 run_tabwidget.addTab(self.run_widget, self.environment_name)
551
552 self.run_widget.splitter.setSizes([200000, 100000])
553
554 self.physical_channel_names = None
555 self.physical_output_indices = None
556 self.physical_unit_names = None
557 self.response_transformation_matrix = None
558 self.output_transformation_matrix = None
559 self.python_control_module = None
560 self.plot_data_items = {}
561 self.achieved_response_signals_combined = None
562 self.achieved_response_signals = None
563 self.achieved_response_amplitudes = None
564 self.achieved_response_phases = None
565 self.complex_drive_modifications = None
566 self.achieved_excitation_signals_combined = None
567 self.achieved_excitation_signals = None
568 self.achieved_excitation_frequencies = None
569 self.achieved_excitation_arguments = None
570 self.achieved_excitation_amplitudes = None
571 self.achieved_excitation_phases = None
572 self.plot_downsample = None
573 self.specification_signals_combined = None
574 self.specification_signals = None
575 self.specification_frequencies = None
576 self.specification_arguments = None
577 self.specification_amplitudes = None
578 self.specification_phases = None
579 self.sine_tables = []
580 self.plot_windows = []
581 self.spec_time = None
582 self.shutdown_sent = False
583
584 self.control_selector_widgets = [
585 self.definition_widget.specification_row_selector,
586 self.prediction_widget.response_selector,
587 self.run_widget.control_channel_selector,
588 ]
589 self.output_selector_widgets = [
590 self.prediction_widget.excitation_selector,
591 ]
592
593 self.spec_display_plotwidgets = [
594 self.definition_widget.specification_all_frequencies_plot,
595 self.definition_widget.specification_all_amplitudes_plot,
596 self.definition_widget.specification_channel_amplitude_plot,
597 self.definition_widget.specification_channel_phase_plot,
598 ]
599
600 self.spec_display_viewboxes = []
601 self.spec_display_imgviews = []
602 for plotwidget in self.spec_display_plotwidgets:
603 plot_item = plotwidget.getPlotItem()
604 plot_item.showGrid(True, True, 0.25)
605 plot_item.enableAutoRange()
606 plot_item.getViewBox().enableAutoRange(enable=True)
607
608 for widget in [
609 self.run_widget.control_updates_signal_selector,
610 self.run_widget.signal_selector,
611 ]:
612 widget.setSelectionMode(QtWidgets.QTableWidget.SingleSelection)
613 for widget in [self.run_widget.partial_environment_tone_selector]:
614 widget.setSelectionMode(QtWidgets.QListWidget.MultiSelection)
615
616 plotitem = self.definition_widget.specification_all_frequencies_plot.getPlotItem()
617 plotitem.setLabel("bottom", "Time")
618 plotitem.setLabel("left", "Frequency")
619 plotitem = self.definition_widget.specification_all_amplitudes_plot.getPlotItem()
620 plotitem.setLabel("bottom", "Frequency")
621 plotitem.setLabel("left", "Amplitude")
622 plotitem = self.definition_widget.specification_channel_amplitude_plot.getPlotItem()
623 plotitem.setLabel("bottom", "Frequency")
624 plotitem.setLabel("left", "Amplitude")
625 plotitem = self.definition_widget.specification_channel_phase_plot.getPlotItem()
626 plotitem.setLabel("bottom", "Frequency")
627 plotitem.setLabel("left", "Phase (deg)")
628
629 self.change_filter_setting_visibility()
630
631 self.connect_callbacks()
632
633 # Complete the profile commands
634 self.command_map["Set Test Level"] = self.change_test_level_from_profile
635 self.command_map["Save Control Data"] = self.save_control_data_from_profile
636
637 def connect_callbacks(self):
638 """Connects UI callbacks to object methods"""
639 # Definition
640 self.definition_widget.transformation_matrices_button.clicked.connect(
641 self.define_transformation_matrices
642 )
643 self.definition_widget.control_channels_selector.itemChanged.connect(
644 self.update_control_channels
645 )
646 self.definition_widget.check_selected_button.clicked.connect(
647 self.check_selected_control_channels
648 )
649 self.definition_widget.uncheck_selected_button.clicked.connect(
650 self.uncheck_selected_control_channels
651 )
652 self.definition_widget.specification_row_selector.currentIndexChanged.connect(
653 self.update_specification
654 )
655 self.definition_widget.script_load_file_button.clicked.connect(self.select_python_module)
656 self.definition_widget.sine_table_tab_widget.currentChanged.connect(
657 self.sine_table_tab_changed
658 )
659 self.definition_widget.explore_filter_button.clicked.connect(self.explore_filter_settings)
660 self.definition_widget.filter_type_selector.currentIndexChanged.connect(
661 self.change_filter_setting_visibility
662 )
663 # Prediction
664 self.prediction_widget.excitation_selector.currentIndexChanged.connect(
665 self.send_excitation_prediction_plot_choices
666 )
667 self.prediction_widget.excitation_display_type.currentIndexChanged.connect(
668 self.update_excitation_prediction_type
669 )
670 self.prediction_widget.excitation_display_tone.currentIndexChanged.connect(
671 self.update_excitation_prediction_tone
672 )
673 self.prediction_widget.response_selector.currentIndexChanged.connect(
674 self.send_response_prediction_plot_choices
675 )
676 self.prediction_widget.response_display_type.currentIndexChanged.connect(
677 self.update_response_prediction_type
678 )
679 self.prediction_widget.response_display_tone.currentIndexChanged.connect(
680 self.update_response_prediction_tone
681 )
682 self.prediction_widget.excitation_voltage_list.itemDoubleClicked.connect(
683 self.update_excitation_prediction_from_table
684 )
685 self.prediction_widget.response_error_table.cellDoubleClicked.connect(
686 self.update_response_prediction_from_table
687 )
688 # Run Test
689 self.run_widget.start_test_button.clicked.connect(self.start_control)
690 self.run_widget.stop_test_button.clicked.connect(self.stop_control)
691 self.run_widget.create_window_button.clicked.connect(self.create_window)
692 self.run_widget.show_all_channels_button.clicked.connect(self.show_all_channels)
693 self.run_widget.tile_windows_button.clicked.connect(self.tile_windows)
694 self.run_widget.close_windows_button.clicked.connect(self.close_windows)
695 self.run_widget.control_updates_signal_selector.itemSelectionChanged.connect(
696 self.update_control_run_plot
697 )
698 self.run_widget.signal_selector.currentCellChanged.connect(self.update_run_plot)
699 self.run_widget.save_control_data_button.clicked.connect(self.save_control_data)
700 self.run_widget.partial_environment_selector.stateChanged.connect(
701 self.enable_disable_partial_environment
702 )
703
704 # %% Data Acquisition
705
706 def initialize_data_acquisition(self, data_acquisition_parameters):
707 super().initialize_data_acquisition(data_acquisition_parameters)
708 # Initialize Plots
709 for plotwidget in self.spec_display_plotwidgets:
710 plotwidget.clear()
711 self.plot_data_items["specification_all_frequencies"] = VaryingNumberOfLinePlot(
712 self.definition_widget.specification_all_frequencies_plot.getPlotItem()
713 )
714 self.plot_data_items["specification_all_amplitudes"] = VaryingNumberOfLinePlot(
715 self.definition_widget.specification_all_amplitudes_plot.getPlotItem()
716 )
717 self.plot_data_items[
718 "specification_channel_phase"
719 ] = self.definition_widget.specification_channel_phase_plot.getPlotItem().plot(
720 np.array([0, 1]), np.zeros(2), pen={"color": "b", "width": 1}, name="Phase"
721 )
722 self.plot_data_items[
723 "specification_channel_amplitude"
724 ] = self.definition_widget.specification_channel_amplitude_plot.getPlotItem().plot(
725 np.array([0, 1]),
726 np.zeros(2),
727 pen={"color": "b", "width": 1},
728 name="Amplitude",
729 )
730 self.plot_data_items[
731 "specification_channel_warning_upper"
732 ] = self.definition_widget.specification_channel_amplitude_plot.getPlotItem().plot(
733 np.array([0, 1]),
734 np.zeros(2),
735 pen={"color": (255, 204, 0), "width": 1, "style": Qt.DashLine},
736 name="Warning",
737 )
738 self.plot_data_items[
739 "specification_channel_warning_lower"
740 ] = self.definition_widget.specification_channel_amplitude_plot.getPlotItem().plot(
741 np.array([0, 1]),
742 np.zeros(2),
743 pen={"color": (255, 204, 0), "width": 1, "style": Qt.DashLine},
744 )
745 self.plot_data_items[
746 "specification_channel_abort_upper"
747 ] = self.definition_widget.specification_channel_amplitude_plot.getPlotItem().plot(
748 np.array([0, 1]),
749 np.zeros(2),
750 pen={"color": (153, 0, 0), "width": 1, "style": Qt.DashLine},
751 name="Abort",
752 )
753 self.plot_data_items[
754 "specification_channel_abort_lower"
755 ] = self.definition_widget.specification_channel_amplitude_plot.getPlotItem().plot(
756 np.array([0, 1]),
757 np.zeros(2),
758 pen={"color": (153, 0, 0), "width": 1, "style": Qt.DashLine},
759 )
760 self.definition_widget.specification_channel_amplitude_plot.getPlotItem().addLegend()
761
762 # Set up channel names
763 self.physical_channel_names = [
764 (
765 f"{'' if channel.channel_type is None else channel.channel_type} "
766 f"{channel.node_number} "
767 f"{'' if channel.node_direction is None else channel.node_direction}"
768 )[:MAXIMUM_NAME_LENGTH]
769 for channel in data_acquisition_parameters.channel_list
770 ]
771 self.physical_unit_names = [
772 f"{'-' if channel.unit is None else channel.unit}"
773 for channel in data_acquisition_parameters.channel_list
774 ]
775 self.physical_output_indices = [
776 i
777 for i, channel in enumerate(data_acquisition_parameters.channel_list)
778 if channel.feedback_device
779 ]
780 # Set up widgets
781 self.definition_widget.sample_rate_display.setValue(data_acquisition_parameters.sample_rate)
782 self.system_id_widget.samplesPerFrameSpinBox.setValue(
783 data_acquisition_parameters.sample_rate
784 )
785 self.definition_widget.samples_per_acquire_display.setValue(
786 data_acquisition_parameters.samples_per_read
787 )
788 self.definition_widget.samples_per_write_display.setValue(
789 data_acquisition_parameters.samples_per_write
790 )
791 self.definition_widget.frame_time_display.setValue(
792 data_acquisition_parameters.samples_per_read / data_acquisition_parameters.sample_rate
793 )
794 self.definition_widget.nyquist_frequency_display.setValue(
795 data_acquisition_parameters.sample_rate / 2
796 )
797 self.definition_widget.control_channels_selector.clear()
798 for channel_name in self.physical_channel_names:
799 item = QtWidgets.QListWidgetItem()
800 item.setText(channel_name)
801 item.setFlags(
802 item.flags() | Qt.ItemIsUserCheckable
803 ) # | Qt.ItemIsUserTristate) # We will add this when we implement limits
804 item.setCheckState(Qt.Unchecked)
805 self.definition_widget.control_channels_selector.addItem(item)
806 self.response_transformation_matrix = None
807 self.output_transformation_matrix = None
808 self.define_transformation_matrices(None, False)
809 self.definition_widget.input_channels_display.setValue(len(self.physical_channel_names))
810 self.definition_widget.output_channels_display.setValue(len(self.physical_output_indices))
811 self.definition_widget.control_channels_display.setValue(0)
812 if self.definition_widget.sine_table_tab_widget.count() == 1:
813 self.sine_tables.append(
814 SineSweepTable(
815 self.definition_widget.sine_table_tab_widget,
816 self.update_specification,
817 self.remove_sine_table_entry,
818 (
819 self.physical_control_names
820 if self.response_transformation_matrix is None
821 else [
822 f"Transformed Response {i}"
823 for i in range(self.response_transformation_matrix.shape[0])
824 ]
825 ),
826 self.data_acquisition_parameters,
827 )
828 )
829 self.clear_and_update_specification_table()
830
831 @property
832 def physical_output_names(self):
833 """Defines names of the physical drive channels"""
834 return [self.physical_channel_names[i] for i in self.physical_output_indices]
835
836 # %% Environment
837
838 @property
839 def physical_control_indices(self):
840 """Gets the physical control indices currently checked"""
841 return [
842 i
843 for i in range(self.definition_widget.control_channels_selector.count())
844 if self.definition_widget.control_channels_selector.item(i).checkState() == Qt.Checked
845 ]
846
847 @property
848 def physical_control_names(self):
849 """Gets the names for the physical control channels currently checked"""
850 return [self.physical_channel_names[i] for i in self.physical_control_indices]
851
852 @property
853 def physical_control_units(self):
854 """Gets the unit for the control channels currently checked"""
855 return [self.physical_unit_names[i] for i in self.physical_control_indices]
856
857 @property
858 def initialized_control_names(self):
859 """Gets the names of the control channels that have been initialized"""
860 if self.environment_parameters.response_transformation_matrix is None:
861 return [
862 self.physical_channel_names[i]
863 for i in self.environment_parameters.control_channel_indices
864 ]
865 return [
866 f"Transformed Response {i + 1}"
867 for i in range(self.environment_parameters.response_transformation_matrix.shape[0])
868 ]
869
870 @property
871 def initialized_output_names(self):
872 """Gets the names of the drive channels that have been initialized"""
873 if self.environment_parameters.reference_transformation_matrix is None:
874 return self.physical_output_names
875 else:
876 return [
877 f"Transformed Drive {i + 1}"
878 for i in range(self.environment_parameters.reference_transformation_matrix.shape[0])
879 ]
880
881 def update_control_channels(self):
882 """Updates the control channels due to selection changes"""
883 self.response_transformation_matrix = None
884 self.output_transformation_matrix = None
885 self.definition_widget.control_channels_display.setValue(len(self.physical_control_indices))
886 self.define_transformation_matrices(None, False)
887 self.clear_and_update_specification_table()
888
889 def check_selected_control_channels(self):
890 """Checks the selected control channels on the UI"""
891 for item in self.definition_widget.control_channels_selector.selectedItems():
892 item.setCheckState(Qt.Checked)
893
894 def uncheck_selected_control_channels(self):
895 """Unchecks the selected control channels on the UI"""
896 for item in self.definition_widget.control_channels_selector.selectedItems():
897 item.setCheckState(Qt.Unchecked)
898
899 def clear_and_update_specification_table(self):
900 """Clears the specification table of all information"""
901 control_names = (
902 self.physical_control_names
903 if self.response_transformation_matrix is None
904 else [
905 f"Transformed Response {i+1}"
906 for i in range(self.response_transformation_matrix.shape[0])
907 ]
908 )
909 for sine_table in self.sine_tables:
910 sine_table.clear_and_update_specification_table(control_names=control_names)
911
912 def add_sine_table_tab(self):
913 """Adds a new sine tone to the sine specification table"""
914 self.definition_widget.sine_table_tab_widget.blockSignals(True)
915 self.sine_tables.append(
916 SineSweepTable(
917 self.definition_widget.sine_table_tab_widget,
918 self.update_specification,
919 self.remove_sine_table_entry,
920 (
921 self.physical_control_names
922 if self.response_transformation_matrix is None
923 else [
924 f"Transformed Response {i}"
925 for i in range(self.response_transformation_matrix.shape[0])
926 ]
927 ),
928 self.data_acquisition_parameters,
929 )
930 )
931 self.definition_widget.sine_table_tab_widget.blockSignals(False)
932
933 def sine_table_tab_changed(self, index):
934 """Updates the displayed sine table and adds a new index if necessary."""
935 if index == self.definition_widget.sine_table_tab_widget.count() - 1:
936 self.add_sine_table_tab()
937 else:
938 self.update_specification()
939
940 def remove_sine_table_entry(self, index):
941 """Removes a tone from the sine table"""
942 self.definition_widget.sine_table_tab_widget.setCurrentIndex(0)
943 self.definition_widget.sine_table_tab_widget.removeTab(index)
944 self.sine_tables.pop(index)
945 for i, table in enumerate(self.sine_tables):
946 table.index = i
947 self.update_specification()
948
949 def change_filter_setting_visibility(self):
950 """Updates which settings are available depending on the selected filter type"""
951 isdtf = self.definition_widget.filter_type_selector.currentIndex() == 0
952 for widget in [
953 self.definition_widget.vk_filter_order_label,
954 self.definition_widget.vk_filter_order_selector,
955 self.definition_widget.vk_filter_block_overlap_label,
956 self.definition_widget.vk_filter_block_overlap_selector,
957 self.definition_widget.vk_filter_bandwidth_label,
958 self.definition_widget.vk_filter_bandwidth_selector,
959 self.definition_widget.vk_filter_block_size_label,
960 self.definition_widget.vk_filter_block_size_selector,
961 ]:
962 widget.setVisible(not isdtf)
963 for widget in [
964 self.definition_widget.tracking_filter_cutoff_label,
965 self.definition_widget.tracking_filter_cutoff_selector,
966 self.definition_widget.tracking_filter_order_label,
967 self.definition_widget.tracking_filter_order_selector,
968 ]:
969 widget.setVisible(isdtf)
970
971 def collect_specification(self):
972 """Collects the specifications defined in the sine table"""
973 specs = []
974 for sine_table in self.sine_tables:
975 spec = sine_table.get_specification()
976 specs.append(spec)
977 return specs
978
979 def update_specification(self):
980 """Updates the specification in the table and plots based on the selection"""
981 # print('Updating Specification Plot')
982 # Go through each of the sine signals
983 for sine_table in self.sine_tables:
984 # Go through and update the prefixes
985 for row in range(sine_table.widget.breakpoint_table.rowCount() - 1):
986 combobox = sine_table.widget.breakpoint_table.cellWidget(row, 1)
987 spinbox = sine_table.widget.breakpoint_table.cellWidget(row, 2)
988 if combobox.currentIndex() == 0:
989 spinbox.setSuffix(" Hz/s")
990 else:
991 spinbox.setSuffix(" oct/min")
992 # Generate representative time signals
993 specs = self.collect_specification()
994 if len(specs) == 0:
995 return
996 table_index = self.definition_widget.sine_table_tab_widget.currentIndex()
997 control_index = self.definition_widget.specification_row_selector.currentIndex()
998 all_ordinate = []
999 all_abscissa = []
1000 all_frequency = []
1001 all_amplitude = []
1002 all_phase = []
1003 for spec in specs:
1004 (
1005 ordinate,
1006 frequency,
1007 _,
1008 amplitude,
1009 phase,
1010 abscissa,
1011 _,
1012 _,
1013 ) = spec.create_signal(
1014 self.data_acquisition_parameters.sample_rate,
1015 control_index=control_index,
1016 ignore_start_time=True,
1017 only_breakpoints=True,
1018 )
1019 # print(f'Shapes: {ordinate.shape=}, {frequency.shape=}, '
1020 # '{amplitude.shape=}, {phase.shape=}')
1021 all_ordinate.append(ordinate)
1022 all_abscissa.append(abscissa + spec.start_time)
1023 all_frequency.append(frequency)
1024 all_amplitude.append(amplitude)
1025 all_phase.append(phase)
1026 self.plot_data_items["specification_all_frequencies"].set_data(all_abscissa, all_frequency)
1027 self.plot_data_items["specification_all_amplitudes"].set_data(all_frequency, all_amplitude)
1028
1029 self.plot_data_items["specification_channel_amplitude"].setData(
1030 all_frequency[table_index], all_amplitude[table_index]
1031 )
1032 self.plot_data_items["specification_channel_phase"].setData(
1033 all_frequency[table_index], all_phase[table_index] * 180 / np.pi
1034 )
1035 self.plot_data_items["specification_channel_warning_lower"].setData(
1036 np.repeat(specs[table_index].breakpoint_table["frequency"], 2),
1037 specs[table_index].breakpoint_table["warning"][:, 0, :, control_index].flatten(),
1038 )
1039 self.plot_data_items["specification_channel_warning_upper"].setData(
1040 np.repeat(specs[table_index].breakpoint_table["frequency"], 2),
1041 specs[table_index].breakpoint_table["warning"][:, 1, :, control_index].flatten(),
1042 )
1043 self.plot_data_items["specification_channel_abort_lower"].setData(
1044 np.repeat(specs[table_index].breakpoint_table["frequency"], 2),
1045 specs[table_index].breakpoint_table["abort"][:, 0, :, control_index].flatten(),
1046 )
1047 self.plot_data_items["specification_channel_abort_upper"].setData(
1048 np.repeat(specs[table_index].breakpoint_table["frequency"], 2),
1049 specs[table_index].breakpoint_table["abort"][:, 1, :, control_index].flatten(),
1050 )
1051 # Return the length of the specification
1052 return max(max(abscissa) for abscissa in all_abscissa)
1053
1054 def explore_filter_settings(self):
1055 """Brings up a dialog box to explore filter settings"""
1056 control_names = (
1057 self.physical_control_names
1058 if self.response_transformation_matrix is None
1059 else [
1060 f"Transformed Response {i+1}"
1061 for i in range(self.response_transformation_matrix.shape[0])
1062 ]
1063 )
1064 order_names = [
1065 self.definition_widget.sine_table_tab_widget.tabText(i)
1066 for i in range(self.definition_widget.sine_table_tab_widget.count() - 1)
1067 ]
1068 specs = self.collect_specification()
1069 (
1070 result,
1071 filter_type,
1072 dtf_cutoff,
1073 dtf_order,
1074 vk_order,
1075 vk_bandwidth,
1076 vk_blocksize,
1077 vk_overlap,
1078 ) = FilterExplorer.explore_filter_settings(
1079 control_names,
1080 order_names,
1081 specs,
1082 self.definition_widget.filter_type_selector.currentIndex(),
1083 self.definition_widget.tracking_filter_cutoff_selector.value(),
1084 self.definition_widget.tracking_filter_order_selector.value(),
1085 self.definition_widget.vk_filter_order_selector.currentIndex() + 1,
1086 self.definition_widget.vk_filter_bandwidth_selector.value(),
1087 self.definition_widget.vk_filter_block_size_selector.value(),
1088 self.definition_widget.vk_filter_block_overlap_selector.value(),
1089 self.data_acquisition_parameters.sample_rate,
1090 self.definition_widget.ramp_time_spinbox.value(),
1091 self.data_acquisition_parameters.samples_per_read,
1092 self.definition_widget,
1093 )
1094 if result:
1095 self.definition_widget.filter_type_selector.setCurrentIndex(filter_type)
1096 self.definition_widget.tracking_filter_cutoff_selector.setValue(dtf_cutoff)
1097 self.definition_widget.tracking_filter_order_selector.setValue(dtf_order)
1098 self.definition_widget.vk_filter_order_selector.setCurrentIndex(vk_order - 1)
1099 self.definition_widget.vk_filter_bandwidth_selector.setValue(vk_bandwidth)
1100 self.definition_widget.vk_filter_block_size_selector.setValue(vk_blocksize)
1101 self.definition_widget.vk_filter_block_overlap_selector.setValue(vk_overlap)
1102
1103 def select_python_module(self, clicked, filename=None): # pylint: disable=unused-argument
1104 """Loads a Python module using a dialog or the specified filename
1105
1106 Parameters
1107 ----------
1108 clicked :
1109 The clicked event that triggered the callback.
1110 filename :
1111 File name defining the Python module for bypassing the callback when
1112 loading from a file (Default value = None).
1113
1114 """
1115 if filename is None:
1116 filename, _ = QtWidgets.QFileDialog.getOpenFileName(
1117 self.definition_widget,
1118 "Select Python Module",
1119 filter="Python Modules (*.py)",
1120 )
1121 if filename == "":
1122 return
1123 self.python_control_module = load_python_module(filename)
1124 classes = [
1125 function
1126 for function in inspect.getmembers(self.python_control_module)
1127 if (
1128 inspect.isclass(function[1])
1129 and all(
1130 [method in function[1].__dict__ for method in ["system_id_update", "control"]]
1131 )
1132 )
1133 ]
1134 self.log(
1135 f"Loaded module {self.python_control_module.__name__} with classes "
1136 f"{[control_class[0] for control_class in classes]}"
1137 )
1138 self.definition_widget.python_class_input.clear()
1139 self.definition_widget.script_file_path_input.setText(filename)
1140 for control_class in classes:
1141 self.definition_widget.python_class_input.addItem(control_class[0])
1142
1143 def collect_environment_definition_parameters(self):
1144 if self.python_control_module is None:
1145 control_module = None
1146 control_class = None
1147 control_class_parameters = (
1148 self.definition_widget.control_parameters_text_input.toPlainText()
1149 )
1150 else:
1151 control_module = self.definition_widget.script_file_path_input.text()
1152 control_class = self.definition_widget.python_class_input.itemText(
1153 self.definition_widget.python_class_input.currentIndex()
1154 )
1155 control_class_parameters = (
1156 self.definition_widget.control_parameters_text_input.toPlainText()
1157 )
1158 return SineMetadata(
1159 sample_rate=self.definition_widget.sample_rate_display.value(),
1160 samples_per_frame=self.definition_widget.samples_per_acquire_display.value(),
1161 number_of_channels=len(self.data_acquisition_parameters.channel_list),
1162 specifications=self.collect_specification(),
1163 ramp_time=self.definition_widget.ramp_time_spinbox.value(),
1164 buffer_blocks=self.definition_widget.buffer_blocks_selector.value(),
1165 control_convergence=self.definition_widget.control_convergence_selector.value(),
1166 update_drives_after_environment=self.definition_widget.update_drives_after_environment_selector.isChecked(),
1167 phase_fit=self.definition_widget.best_fit_phase_checkbox.isChecked(),
1168 allow_automatic_aborts=self.definition_widget.auto_abort_checkbox.isChecked(),
1169 tracking_filter_type=self.definition_widget.filter_type_selector.currentIndex(),
1170 tracking_filter_cutoff=self.definition_widget.tracking_filter_cutoff_selector.value()
1171 / 100,
1172 tracking_filter_order=self.definition_widget.tracking_filter_order_selector.value(),
1173 vk_filter_order=self.definition_widget.vk_filter_order_selector.currentIndex() + 1,
1174 vk_filter_bandwidth=self.definition_widget.vk_filter_bandwidth_selector.value(),
1175 vk_filter_blocksize=self.definition_widget.vk_filter_block_size_selector.value(),
1176 vk_filter_overlap=self.definition_widget.vk_filter_block_overlap_selector.value(),
1177 control_python_script=control_module,
1178 control_python_class=control_class,
1179 control_python_parameters=control_class_parameters,
1180 control_channel_indices=self.physical_control_indices,
1181 output_channel_indices=self.physical_output_indices,
1182 response_transformation_matrix=self.response_transformation_matrix,
1183 output_transformation_matrix=self.output_transformation_matrix,
1184 )
1185
1186 def initialize_environment(self):
1187 super().initialize_environment()
1188 # Set up channel names in selectors
1189 for widget in [
1190 self.prediction_widget.response_selector,
1191 self.run_widget.control_channel_selector,
1192 ]:
1193 widget.blockSignals(True)
1194 widget.clear()
1195 for i, control_name in enumerate(self.initialized_control_names):
1196 widget.addItem(f"{i + 1}: {control_name}")
1197 if isinstance(widget, QtWidgets.QListWidget):
1198 widget.setCurrentRow(0)
1199 widget.blockSignals(False)
1200 for widget in [self.prediction_widget.excitation_selector]:
1201 widget.blockSignals(True)
1202 widget.clear()
1203 for i, drive_name in enumerate(self.initialized_output_names):
1204 widget.addItem(f"{i + 1}: {drive_name}")
1205 if isinstance(widget, QtWidgets.QListWidget):
1206 widget.setCurrentRow(0)
1207 widget.blockSignals(False)
1208 # Set up tone names in selectors
1209 for widget in [
1210 self.prediction_widget.response_display_tone,
1211 self.prediction_widget.excitation_display_tone,
1212 self.run_widget.partial_environment_tone_selector,
1213 self.run_widget.control_tone_selector,
1214 ]:
1215 widget.blockSignals(True)
1216 widget.clear()
1217 if widget not in [
1218 self.run_widget.partial_environment_tone_selector,
1219 self.run_widget.control_tone_selector,
1220 ]:
1221 widget.addItem("All Tones")
1222 for table in self.sine_tables:
1223 widget.addItem(table.widget.name_editor.text())
1224 if isinstance(widget, QtWidgets.QListWidget):
1225 widget.setCurrentRow(0)
1226 widget.blockSignals(False)
1227 # Set up the run widget tables
1228 for widget, channel_names in zip(
1229 [
1230 self.run_widget.signal_selector,
1231 self.run_widget.control_updates_signal_selector,
1232 ],
1233 [self.initialized_control_names, self.initialized_output_names],
1234 ):
1235 widget.blockSignals(True)
1236 widget.clear()
1237 widget.setRowCount(len(self.sine_tables))
1238 widget.setColumnCount(len(channel_names))
1239 for i in range(widget.rowCount()):
1240 for j in range(widget.columnCount()):
1241 item = QtWidgets.QTableWidgetItem("0.000")
1242 widget.setItem(i, j, item)
1243 item.setFlags(item.flags() & ~Qt.ItemIsEditable)
1244 widget.blockSignals(False)
1245
1246 # Set up the prediction and run plots
1247 self.prediction_widget.excitation_display_plot.getPlotItem().clear()
1248 self.prediction_widget.response_display_plot.getPlotItem().clear()
1249 self.run_widget.control_updates_plot.getPlotItem().clear()
1250 self.run_widget.amplitude_plot.getPlotItem().clear()
1251 self.run_widget.phase_plot.getPlotItem().clear()
1252 self.run_widget.amplitude_plot.getPlotItem().addLegend()
1253 self.run_widget.phase_plot.getPlotItem().addLegend()
1254 self.prediction_widget.excitation_display_plot.getPlotItem().addLegend()
1255 self.prediction_widget.response_display_plot.getPlotItem().addLegend()
1256 self.plot_data_items["response_prediction"] = multiline_plotter(
1257 np.arange(2),
1258 np.zeros((2, 2)),
1259 widget=self.prediction_widget.response_display_plot,
1260 other_pen_options={"width": 1},
1261 names=["Prediction", "Spec"],
1262 )
1263 self.plot_data_items["excitation_prediction"] = multiline_plotter(
1264 np.arange(2),
1265 np.zeros((1, 2)),
1266 widget=self.prediction_widget.excitation_display_plot,
1267 other_pen_options={"width": 1},
1268 names=["Prediction"],
1269 )
1270 self.plot_data_items["control_amplitude"] = multiline_plotter(
1271 np.arange(2),
1272 np.zeros((2, 2)),
1273 widget=self.run_widget.amplitude_plot,
1274 other_pen_options={"width": 1},
1275 names=["Achieved", "Spec"],
1276 )
1277 self.plot_data_items["control_phase"] = multiline_plotter(
1278 np.arange(2),
1279 np.zeros((2, 2)),
1280 widget=self.run_widget.phase_plot,
1281 other_pen_options={"width": 1},
1282 names=["Achieved", "Spec"],
1283 )
1284 self.plot_data_items[
1285 "control_warning_upper"
1286 ] = self.run_widget.amplitude_plot.getPlotItem().plot(
1287 np.array([0, 0]),
1288 np.zeros(2),
1289 pen={"color": (255, 204, 0), "width": 1, "style": Qt.DashLine},
1290 name="Warning",
1291 )
1292 self.plot_data_items[
1293 "control_warning_lower"
1294 ] = self.run_widget.amplitude_plot.getPlotItem().plot(
1295 np.array([0, 0]),
1296 np.zeros(2),
1297 pen={"color": (255, 204, 0), "width": 1, "style": Qt.DashLine},
1298 )
1299 self.plot_data_items[
1300 "control_abort_upper"
1301 ] = self.run_widget.amplitude_plot.getPlotItem().plot(
1302 np.array([0, 0]),
1303 np.zeros(2),
1304 pen={"color": (153, 0, 0), "width": 1, "style": Qt.DashLine},
1305 name="Abort",
1306 )
1307 self.plot_data_items[
1308 "control_abort_lower"
1309 ] = self.run_widget.amplitude_plot.getPlotItem().plot(
1310 np.array([0, 0]),
1311 np.zeros(2),
1312 pen={"color": (153, 0, 0), "width": 1, "style": Qt.DashLine},
1313 )
1314 self.plot_data_items[
1315 "prediction_warning_upper"
1316 ] = self.prediction_widget.response_display_plot.getPlotItem().plot(
1317 np.array([0, 0]),
1318 np.zeros(2),
1319 pen={"color": (255, 204, 0), "width": 1, "style": Qt.DashLine},
1320 name="Warning",
1321 )
1322 self.plot_data_items[
1323 "prediction_warning_lower"
1324 ] = self.prediction_widget.response_display_plot.getPlotItem().plot(
1325 np.array([0, 0]),
1326 np.zeros(2),
1327 pen={"color": (255, 204, 0), "width": 1, "style": Qt.DashLine},
1328 )
1329 self.plot_data_items[
1330 "prediction_abort_upper"
1331 ] = self.prediction_widget.response_display_plot.getPlotItem().plot(
1332 np.array([0, 0]),
1333 np.zeros(2),
1334 pen={"color": (153, 0, 0), "width": 1, "style": Qt.DashLine},
1335 name="Abort",
1336 )
1337 self.plot_data_items[
1338 "prediction_abort_lower"
1339 ] = self.prediction_widget.response_display_plot.getPlotItem().plot(
1340 np.array([0, 0]),
1341 np.zeros(2),
1342 pen={"color": (153, 0, 0), "width": 1, "style": Qt.DashLine},
1343 )
1344 self.plot_data_items["control_updates"] = blended_scatter_plot(
1345 np.zeros((10, 2)), widget=self.run_widget.control_updates_plot
1346 )
1347
1348 # Make sure the specification starts at 0
1349 min_time = min(spec.start_time for spec in self.collect_specification())
1350 for sine_table in self.sine_tables:
1351 sine_table.widget.start_time_selector.setValue(
1352 sine_table.widget.start_time_selector.value() - min_time
1353 )
1354
1355 self.spec_time = self.update_specification()
1356
1357 for widget in [
1358 self.run_widget.start_time_selector,
1359 self.run_widget.stop_time_selector,
1360 ]:
1361 widget.setMinimum(0)
1362 widget.setMaximum(self.spec_time)
1363 self.run_widget.stop_time_selector.setValue(self.spec_time)
1364
1365 return self.environment_parameters
1366
1367 def define_transformation_matrices(
1368 self, clicked, dialog=True
1369 ): # pylint: disable=unused-argument
1370 """Defines the transformation matrices using the dialog box"""
1371 if dialog:
1372 (response_transformation, output_transformation, result) = (
1373 TransformationMatrixWindow.define_transformation_matrices(
1374 self.response_transformation_matrix,
1375 self.definition_widget.control_channels_display.value(),
1376 self.output_transformation_matrix,
1377 self.definition_widget.output_channels_display.value(),
1378 self.definition_widget,
1379 )
1380 )
1381 else:
1382 response_transformation = self.response_transformation_matrix
1383 output_transformation = self.output_transformation_matrix
1384 result = True
1385 if result:
1386 # Update the control names
1387 for widget in self.control_selector_widgets:
1388 widget.blockSignals(True)
1389 widget.clear()
1390 if response_transformation is None:
1391 for i, control_name in enumerate(self.physical_control_names):
1392 for widget in self.control_selector_widgets:
1393 widget.addItem(f"{i + 1}: {control_name}")
1394 self.definition_widget.transform_channels_display.setValue(
1395 len(self.physical_control_names)
1396 )
1397 else:
1398 for i in range(response_transformation.shape[0]):
1399 for widget in self.control_selector_widgets:
1400 widget.addItem(f"{i + 1}: Virtual Response")
1401 self.definition_widget.transform_channels_display.setValue(
1402 response_transformation.shape[0]
1403 )
1404 for widget in self.control_selector_widgets:
1405 widget.blockSignals(False)
1406 # Update the output names
1407 for widget in self.output_selector_widgets:
1408 widget.blockSignals(True)
1409 widget.clear()
1410 if output_transformation is None:
1411 for i, drive_name in enumerate(self.physical_output_names):
1412 for widget in self.output_selector_widgets:
1413 widget.addItem(f"{i + 1}: {drive_name}")
1414 self.definition_widget.transform_outputs_display.setValue(
1415 len(self.physical_output_names)
1416 )
1417 else:
1418 for i in range(output_transformation.shape[0]):
1419 for widget in self.output_selector_widgets:
1420 widget.addItem(f"{i + 1}: Virtual Drive")
1421 self.definition_widget.transform_outputs_display.setValue(
1422 output_transformation.shape[0]
1423 )
1424 for widget in self.output_selector_widgets:
1425 widget.blockSignals(False)
1426 self.response_transformation_matrix = response_transformation
1427 self.output_transformation_matrix = output_transformation
1428 self.clear_and_update_specification_table()
1429
1430 # %% Predictions
1431
1432 def update_response_prediction_tone(self):
1433 """Called when the tone is changed, sends selection to environment"""
1434 type_index = self.prediction_widget.response_display_type.currentIndex()
1435 tone_index = (
1436 self.prediction_widget.response_display_tone.currentIndex() - 1
1437 ) # All tones is first
1438 if tone_index < 0 and type_index != 0: # For all tones we can only show time histories
1439 self.prediction_widget.response_display_type.blockSignals(True)
1440 self.prediction_widget.response_display_type.setCurrentIndex(0)
1441 self.prediction_widget.response_display_type.blockSignals(False)
1442 self.send_response_prediction_plot_choices()
1443
1444 def update_excitation_prediction_tone(self):
1445 """Called when the tone is changed, sends selection to the environment"""
1446 # Excitation
1447 type_index = self.prediction_widget.excitation_display_type.currentIndex()
1448 tone_index = (
1449 self.prediction_widget.excitation_display_tone.currentIndex() - 1
1450 ) # All tones is first
1451 if tone_index < 0 and type_index != 0: # For all tones we can only show time histories
1452 self.prediction_widget.excitation_display_type.blockSignals(True)
1453 self.prediction_widget.excitation_display_type.setCurrentIndex(0)
1454 self.prediction_widget.excitation_display_type.blockSignals(False)
1455 self.send_excitation_prediction_plot_choices()
1456
1457 def update_response_prediction_type(self):
1458 """Called when the response type is changed, sends selection to environment"""
1459 type_index = self.prediction_widget.response_display_type.currentIndex()
1460 tone_index = (
1461 self.prediction_widget.response_display_tone.currentIndex() - 1
1462 ) # All tones is first
1463 if tone_index < 0 and type_index != 0: # For all tones we can only show time histories
1464 self.prediction_widget.response_display_tone.blockSignals(True)
1465 self.prediction_widget.response_display_tone.setCurrentIndex(1)
1466 self.prediction_widget.response_display_tone.blockSignals(False)
1467 self.send_response_prediction_plot_choices()
1468
1469 def update_excitation_prediction_type(self):
1470 """Called when the drive type is changed, sends selection to environment"""
1471 # Excitation
1472 type_index = self.prediction_widget.excitation_display_type.currentIndex()
1473 tone_index = (
1474 self.prediction_widget.excitation_display_tone.currentIndex() - 1
1475 ) # All tones is first
1476 if tone_index < 0 and type_index != 0: # For all tones we can only show time histories
1477 self.prediction_widget.excitation_display_tone.blockSignals(True)
1478 self.prediction_widget.excitation_display_tone.setCurrentIndex(1)
1479 self.prediction_widget.excitation_display_tone.blockSignals(False)
1480 self.send_excitation_prediction_plot_choices()
1481
1482 def send_response_prediction_plot_choices(self):
1483 """Sends the response prediction plot choices to the environment"""
1484 channel_index = self.prediction_widget.response_selector.currentIndex()
1485 type_index = self.prediction_widget.response_display_type.currentIndex()
1486 tone_index = (
1487 self.prediction_widget.response_display_tone.currentIndex() - 1
1488 ) # All tones is first
1489 self.environment_command_queue.put(
1490 self.log_name,
1491 (
1492 SineCommands.SEND_RESPONSE_PREDICTION,
1493 (channel_index, type_index, tone_index),
1494 ),
1495 )
1496 self.plot_prediction_warnings_and_aborts() # Update the plots for the warning/abort limits
1497
1498 def send_excitation_prediction_plot_choices(self):
1499 """Sends the drive prediction plot choices to the environment"""
1500 channel_index = self.prediction_widget.excitation_selector.currentIndex()
1501 type_index = self.prediction_widget.excitation_display_type.currentIndex()
1502 tone_index = (
1503 self.prediction_widget.excitation_display_tone.currentIndex() - 1
1504 ) # All tones is first
1505 self.environment_command_queue.put(
1506 self.log_name,
1507 (
1508 SineCommands.SEND_EXCITATION_PREDICTION,
1509 (channel_index, type_index, tone_index),
1510 ),
1511 )
1512
1513 def plot_prediction_warnings_and_aborts(self):
1514 """Adds warning and aborts to the prediction tab"""
1515 if self.prediction_widget.response_display_type.currentIndex() == 3:
1516 # Plot the response
1517 specs = self.environment_parameters.specifications
1518 table_index = self.prediction_widget.response_display_tone.currentIndex() - 1
1519 control_index = self.prediction_widget.response_selector.currentIndex()
1520 self.plot_data_items["prediction_warning_lower"].setData(
1521 np.repeat(specs[table_index].breakpoint_table["frequency"], 2),
1522 specs[table_index].breakpoint_table["warning"][:, 0, :, control_index].flatten(),
1523 )
1524 self.plot_data_items["prediction_warning_upper"].setData(
1525 np.repeat(specs[table_index].breakpoint_table["frequency"], 2),
1526 specs[table_index].breakpoint_table["warning"][:, 1, :, control_index].flatten(),
1527 )
1528 self.plot_data_items["prediction_abort_lower"].setData(
1529 np.repeat(specs[table_index].breakpoint_table["frequency"], 2),
1530 specs[table_index].breakpoint_table["abort"][:, 0, :, control_index].flatten(),
1531 )
1532 self.plot_data_items["prediction_abort_upper"].setData(
1533 np.repeat(specs[table_index].breakpoint_table["frequency"], 2),
1534 specs[table_index].breakpoint_table["abort"][:, 1, :, control_index].flatten(),
1535 )
1536 else:
1537 for item in [
1538 "prediction_warning_lower",
1539 "prediction_warning_upper",
1540 "prediction_abort_lower",
1541 "prediction_abort_upper",
1542 ]:
1543 self.plot_data_items[item].setData(np.zeros(2), np.nan * np.ones(2))
1544
1545 def plot_excitation_prediction(self, abscissa, ordinate):
1546 """Plots the recieved drive prediction"""
1547 self.plot_data_items["excitation_prediction"][0].setData(abscissa, ordinate)
1548
1549 def plot_response_prediction(self, abscissa, ordinate):
1550 """Plots the recieved control prediction"""
1551 for index, this_ordinate in enumerate(ordinate[::-1]):
1552 plot_length = min(abscissa.shape[-1], this_ordinate.shape[-1])
1553 self.plot_data_items["response_prediction"][index].setData(
1554 abscissa[:plot_length], this_ordinate[:plot_length]
1555 )
1556
1557 def update_voltage_list(self, voltages):
1558 """Updates the voltage list with predicted values"""
1559 self.prediction_widget.excitation_voltage_list.clear()
1560 for value in voltages:
1561 self.prediction_widget.excitation_voltage_list.addItem(f"{value:.3f}")
1562
1563 def update_response_matrix(self, amplitude_error, warning_matrix, abort_matrix):
1564 """Updates the response error predictions in the table"""
1565 self.prediction_widget.response_error_table.clear()
1566 self.prediction_widget.response_error_table.setRowCount(amplitude_error.shape[0])
1567 self.prediction_widget.response_error_table.setColumnCount(amplitude_error.shape[1])
1568 for i in range(amplitude_error.shape[0]):
1569 for j in range(amplitude_error.shape[1]):
1570 error_value = amplitude_error[i, j]
1571 item = QtWidgets.QTableWidgetItem(f"{error_value:0.3f}")
1572 self.prediction_widget.response_error_table.setItem(i, j, item)
1573 if abort_matrix[i, j]:
1574 item.setBackground(QColor(255, 125, 125))
1575 elif warning_matrix[i, j]:
1576 item.setBackground(QColor(255, 255, 125))
1577 else:
1578 item.setBackground(QColor(255, 255, 255))
1579 item.setFlags(item.flags() & ~Qt.ItemIsEditable)
1580
1581 def update_response_prediction_from_table(self, row, column):
1582 """Selects the specified tone and channel from a double-click on the prediction table"""
1583 widgets = [
1584 self.prediction_widget.response_display_type,
1585 self.prediction_widget.response_display_tone,
1586 self.prediction_widget.response_selector,
1587 ]
1588 for widget in widgets:
1589 widget.blockSignals(True)
1590 self.prediction_widget.response_display_type.setCurrentIndex(3)
1591 self.prediction_widget.response_display_tone.setCurrentIndex(row + 1) # All tones is first
1592 self.prediction_widget.response_selector.setCurrentIndex(column)
1593 for widget in widgets:
1594 widget.blockSignals(False)
1595 self.send_response_prediction_plot_choices()
1596
1597 def update_excitation_prediction_from_table(self, item):
1598 """Updates the specified tone and channel from a double-click on the predicted voltage"""
1599 index = self.prediction_widget.excitation_voltage_list.row(item)
1600 widgets = [
1601 self.prediction_widget.excitation_display_type,
1602 self.prediction_widget.excitation_display_tone,
1603 self.prediction_widget.excitation_selector,
1604 ]
1605 for widget in widgets:
1606 widget.blockSignals(True)
1607 self.prediction_widget.excitation_display_type.setCurrentIndex(0)
1608 self.prediction_widget.excitation_display_tone.setCurrentIndex(0) # All tones is first
1609 self.prediction_widget.excitation_selector.setCurrentIndex(index)
1610 for widget in widgets:
1611 widget.blockSignals(False)
1612 self.send_excitation_prediction_plot_choices()
1613
1614 # %% Control
1615
1616 def start_control(self):
1617 """Sets itself up to start controlling and sends a signal to the environment to start"""
1618 self.achieved_response_signals_combined = []
1619 self.achieved_response_signals = []
1620 self.achieved_response_amplitudes = []
1621 self.achieved_response_phases = []
1622 self.complex_drive_modifications = []
1623 self.achieved_excitation_signals_combined = []
1624 self.achieved_excitation_signals = []
1625 self.achieved_excitation_frequencies = []
1626 self.achieved_excitation_arguments = []
1627 self.achieved_excitation_amplitudes = []
1628 self.achieved_excitation_phases = []
1629 self.enable_control(False)
1630 self.shutdown_sent = False
1631 self.controller_communication_queue.put(
1632 self.log_name, (GlobalCommands.START_ENVIRONMENT, self.environment_name)
1633 )
1634 self.environment_command_queue.put(
1635 self.log_name,
1636 (
1637 SineCommands.START_CONTROL,
1638 (
1639 db2scale(self.run_widget.test_level_selector.value()),
1640 (
1641 [
1642 self.run_widget.partial_environment_tone_selector.row(item)
1643 for item in self.run_widget.partial_environment_tone_selector.selectedItems()
1644 ]
1645 if self.run_widget.partial_environment_selector.isChecked()
1646 else None
1647 ),
1648 (
1649 self.run_widget.start_time_selector.value()
1650 if self.run_widget.partial_environment_selector.isChecked()
1651 else None
1652 ),
1653 (
1654 self.run_widget.stop_time_selector.value()
1655 if self.run_widget.partial_environment_selector.isChecked()
1656 else None
1657 ),
1658 ),
1659 ),
1660 )
1661 if self.run_widget.test_level_selector.value() >= 0:
1662 self.controller_communication_queue.put(
1663 self.log_name, (GlobalCommands.AT_TARGET_LEVEL, self.environment_name)
1664 )
1665
1666 def enable_control(self, enabled):
1667 """Enables or disables the widgets to start or modify the control
1668
1669 Parameters
1670 ----------
1671 enabled : bool
1672 If True, enables the widgets. Otherwise, it disables the widgets
1673 """
1674 for widget in [
1675 self.run_widget.test_level_selector,
1676 self.run_widget.partial_environment_selector,
1677 self.run_widget.partial_environment_tone_selector,
1678 self.run_widget.start_time_selector,
1679 self.run_widget.stop_time_selector,
1680 self.run_widget.start_test_button,
1681 ]:
1682 widget.setEnabled(enabled)
1683 for widget in [self.run_widget.stop_test_button]:
1684 widget.setEnabled(not enabled)
1685 if enabled:
1686 self.enable_disable_partial_environment()
1687
1688 def stop_control(self):
1689 """Sends a signal to shut down the control"""
1690 self.shutdown_sent = True
1691 self.environment_command_queue.put(self.log_name, (SineCommands.STOP_CONTROL, None))
1692
1693 def change_test_level_from_profile(self, test_level):
1694 """Changes the value of the test level from a profile.
1695
1696 Parameters
1697 ----------
1698 test_level : int
1699 The value in decibels to set the test level to
1700 """
1701 self.run_widget.test_level_selector.setValue(int(test_level))
1702
1703 def create_window(
1704 self, event, tone_index=None, channel_index=None
1705 ): # pylint: disable=unused-argument
1706 """Creates a window with the specified tone and channel index.
1707
1708 Parameters
1709 ----------
1710 event : event
1711 The button clicked event that triggered this callback. Not used.
1712 tone_index : int, optional
1713 The tone index to visualize. By default, it will be the one
1714 currently selected in the UI
1715 channel_index : int, optional
1716 The channel index to visualize. By default, it will be the one
1717 currently selected in the UI
1718 """
1719 if tone_index is None:
1720 tone_index = self.run_widget.control_tone_selector.currentIndex()
1721 if channel_index is None:
1722 channel_index = self.run_widget.control_channel_selector.currentIndex()
1723 self.plot_windows.append(PlotSineWindow(None, self, tone_index, channel_index))
1724
1725 def show_all_channels(self):
1726 """Creates a window for all pairs of tone and channel"""
1727 for i in range(self.run_widget.control_tone_selector.count()):
1728 for j in range(self.run_widget.control_channel_selector.count()):
1729 self.create_window(None, i, j)
1730 self.tile_windows()
1731
1732 def tile_windows(self):
1733 """Tiles the plot windows across the monitor"""
1734 screen_rect = QtWidgets.QApplication.desktop().screenGeometry()
1735 # Go through and remove any closed windows
1736 self.plot_windows = [window for window in self.plot_windows if window.isVisible()]
1737 num_windows = len(self.plot_windows)
1738 if num_windows == 0:
1739 return
1740 ncols = int(np.ceil(np.sqrt(num_windows)))
1741 nrows = int(np.ceil(num_windows / ncols))
1742 window_width = int(screen_rect.width() / ncols)
1743 window_height = int(screen_rect.height() / nrows)
1744 for index, window in enumerate(self.plot_windows):
1745 window.resize(window_width, window_height)
1746 row_ind = index // ncols
1747 col_ind = index % ncols
1748 window.move(col_ind * window_width, row_ind * window_height)
1749
1750 def close_windows(self):
1751 """Closes all the plot windows"""
1752 for window in self.plot_windows:
1753 window.close()
1754
1755 def save_control_data_from_profile(self, filename):
1756 """Saves the current control data to the specified file name"""
1757 self.save_control_data(None, filename)
1758
1759 def save_control_data(self, clicked, filename=None): # pylint: disable=unused-argument
1760 """Saves the control data to the specified filename, or via a dialog box"""
1761 if filename is None:
1762 filename, _ = QtWidgets.QFileDialog.getSaveFileName(
1763 self.definition_widget,
1764 "Select File to Save Spectral Data",
1765 filter="NumPy File (*.npz)",
1766 )
1767 if filename == "":
1768 return
1769 self.environment_command_queue.put(
1770 self.log_name, (SineCommands.SAVE_CONTROL_DATA, filename)
1771 )
1772
1773 def update_control_run_plot(self, tone_index=None, channel_index=None):
1774 """Updates the drive plot showing the modifications to the control
1775
1776 Parameters
1777 ----------
1778 tone_index : int, optional
1779 The tone index to display. By default, the currently selected tone is displayed.
1780 channel_index : int, optional
1781 The channel index to display. By default, the currently selected channel is displayed.
1782 """
1783 if self.complex_drive_modifications is None:
1784 return
1785 if channel_index is None:
1786 channel_index = self.run_widget.control_updates_signal_selector.currentColumn()
1787 if tone_index is None:
1788 tone_index = self.run_widget.control_updates_signal_selector.currentRow()
1789 response_over_time = [
1790 cuh[tone_index, channel_index] for cuh in self.complex_drive_modifications[::-1]
1791 ]
1792 # print(response_over_time)
1793 for rot, marker in zip(response_over_time, self.plot_data_items["control_updates"][::-1]):
1794 marker.setData([np.real(rot)], [np.imag(rot)])
1795 for tone_index in range(self.run_widget.control_updates_signal_selector.rowCount()):
1796 for channel_index in range(
1797 self.run_widget.control_updates_signal_selector.columnCount()
1798 ):
1799 item = self.run_widget.control_updates_signal_selector.item(
1800 tone_index, channel_index
1801 )
1802 item.setText(
1803 f"{np.abs(self.complex_drive_modifications[-1][tone_index, channel_index]):0.3g}"
1804 )
1805
1806 def update_run_plot(
1807 self,
1808 tone_index=None,
1809 channel_index=None,
1810 previous_tone=None, # pylint: disable=unused-argument
1811 previous_channel=None, # pylint: disable=unused-argument
1812 update_spec=True,
1813 ):
1814 """Updates the control plots showing amplitude and phase
1815
1816 Parameters
1817 ----------
1818 tone_index : int, optional
1819 The tone index to display. By default, the currently selected tone is displayed.
1820 channel_index : int, optional
1821 The channel index to display. By default, the currently selected channel is displayed.
1822 previous_tone : int, optional
1823 Not used but required by callback signature
1824 previous_channel : int, optional
1825 Not used but required by callback signature
1826 update_spec : bool, optional
1827 If True, the specification will also be updated, by default True.
1828 """
1829 if channel_index is None:
1830 channel_index = self.run_widget.signal_selector.currentColumn()
1831 if tone_index is None:
1832 tone_index = self.run_widget.signal_selector.currentRow()
1833 if update_spec:
1834 spec_frequency = self.specification_frequencies[tone_index]
1835 spec_amplitude = self.specification_amplitudes[tone_index, channel_index]
1836 self.plot_data_items["control_amplitude"][1].setData(spec_frequency, spec_amplitude)
1837 spec_phase = self.specification_phases[tone_index, channel_index]
1838 self.plot_data_items["control_phase"][1].setData(spec_frequency, spec_phase)
1839 # Get the warning and abort limits
1840 spec = self.environment_parameters.specifications[tone_index]
1841 self.plot_data_items["control_warning_lower"].setData(
1842 np.repeat(spec.breakpoint_table["frequency"], 2),
1843 spec.breakpoint_table["warning"][:, 0, :, channel_index].flatten(),
1844 )
1845 self.plot_data_items["control_warning_upper"].setData(
1846 np.repeat(spec.breakpoint_table["frequency"], 2),
1847 spec.breakpoint_table["warning"][:, 1, :, channel_index].flatten(),
1848 )
1849 self.plot_data_items["control_abort_lower"].setData(
1850 np.repeat(spec.breakpoint_table["frequency"], 2),
1851 spec.breakpoint_table["abort"][:, 0, :, channel_index].flatten(),
1852 )
1853 self.plot_data_items["control_abort_upper"].setData(
1854 np.repeat(spec.breakpoint_table["frequency"], 2),
1855 spec.breakpoint_table["abort"][:, 1, :, channel_index].flatten(),
1856 )
1857 if self.achieved_excitation_frequencies is not None:
1858 achieved_frequency = np.concatenate(
1859 [fh[tone_index] for fh in self.achieved_excitation_frequencies]
1860 )
1861 if self.achieved_response_amplitudes is not None:
1862 achieved_amplitude = np.concatenate(
1863 [ah[tone_index, channel_index] for ah in self.achieved_response_amplitudes]
1864 )
1865 self.plot_data_items["control_amplitude"][0].setData(
1866 achieved_frequency, achieved_amplitude
1867 )
1868 if self.achieved_response_phases is not None:
1869 achieved_phase = np.concatenate(
1870 [ph[tone_index, channel_index] for ph in self.achieved_response_phases]
1871 )
1872 self.plot_data_items["control_phase"][0].setData(achieved_frequency, achieved_phase)
1873 # Go through and remove any closed windows
1874 self.plot_windows = [window for window in self.plot_windows if window.isVisible()]
1875 for window in self.plot_windows:
1876 window.update_plot()
1877
1878 def update_control_error_table(self, last_errors, last_warning_flags, last_abort_flags):
1879 """Updates the values in the control table, including color changes
1880
1881 Parameters
1882 ----------
1883 last_errors : ndarray
1884 An array of shape (tone,channel) containing the amplitude errors
1885 last_warning_flags : ndarray
1886 An array of shape (tone,channel) booleans denoting if a warning has been hit
1887 last_abort_flags : ndarray
1888 An array of shape (tone,channel) booleans denoting if an abort has been hit
1889 """
1890 for i in range(last_errors.shape[0]):
1891 for j in range(last_errors.shape[1]):
1892 item = self.run_widget.signal_selector.item(i, j)
1893 if last_abort_flags[i, j]:
1894 item.setBackground(QColor(255, 125, 125))
1895 elif last_warning_flags[i, j]:
1896 item.setBackground(QColor(255, 255, 125))
1897 else:
1898 item.setBackground(QColor(255, 255, 255))
1899 item.setText(f"{last_errors[i, j]:0.3f}")
1900 if (
1901 np.any(last_abort_flags)
1902 and self.environment_parameters.allow_automatic_aborts
1903 and not self.shutdown_sent
1904 ):
1905 self.log("Sending Abort Signal!")
1906 self.stop_control()
1907
1908 def enable_disable_partial_environment(self):
1909 """Enables or disables the partial environment widgets"""
1910 for widget in [
1911 self.run_widget.start_time_selector,
1912 self.run_widget.stop_time_selector,
1913 self.run_widget.partial_environment_tone_selector,
1914 ]:
1915 widget.setEnabled(self.run_widget.partial_environment_selector.isChecked())
1916
1917 # %% Misc
1918
1919 def retrieve_metadata(
1920 self,
1921 netcdf_handle: nc4._netCDF4.Dataset, # pylint: disable=c-extension-no-member
1922 environment_name: str = None,
1923 ) -> nc4._netCDF4.Group: # pylint: disable=c-extension-no-member
1924 """Retrieves metadata from a netcdf file and sets the UI appropriately."""
1925 # Get all the system identification information
1926 super().retrieve_metadata(netcdf_handle, environment_name)
1927 # Get the group
1928 group = netcdf_handle.groups[self.environment_name]
1929 self.definition_widget.ramp_time_spinbox.setValue(group.ramp_time)
1930 self.definition_widget.buffer_blocks_selector.setValue(group.buffer_blocks)
1931 self.definition_widget.control_convergence_selector.setValue(group.control_convergence)
1932 self.definition_widget.update_drives_after_environment_selector.setChecked(
1933 bool(group.update_drives_after_environment)
1934 )
1935 self.definition_widget.best_fit_phase_checkbox.setChecked(bool(group.phase_fit))
1936 self.definition_widget.auto_abort_checkbox.setChecked(bool(group.allow_automatic_aborts))
1937 self.definition_widget.filter_type_selector.setCurrentIndex(group.tracking_filter_type)
1938 self.definition_widget.tracking_filter_cutoff_selector.setValue(
1939 group.tracking_filter_cutoff * 100
1940 )
1941 self.definition_widget.tracking_filter_order_selector.setValue(group.tracking_filter_order)
1942 self.definition_widget.vk_filter_order_selector.setCurrentIndex(group.vk_filter_order - 1)
1943 self.definition_widget.vk_filter_bandwidth_selector.setValue(group.vk_filter_bandwidth)
1944 self.definition_widget.vk_filter_block_size_selector.setValue(group.vk_filter_blocksize)
1945 self.definition_widget.vk_filter_block_overlap_selector.setValue(group.vk_filter_overlap)
1946 if group.control_python_script != "":
1947 self.select_python_module(None, group.control_python_script)
1948 self.definition_widget.control_function_input.setCurrentIndex(
1949 self.definition_widget.control_function_input.findText(group.control_python_class)
1950 )
1951 self.definition_widget.control_parameters_text_input.setText(
1952 group.control_python_function_parameters
1953 )
1954 # Control channels
1955 for i in group.variables["control_channel_indices"][...]:
1956 item = self.definition_widget.control_channels_selector.item(i)
1957 item.setCheckState(Qt.Checked)
1958 # Transformation matrices
1959 try:
1960 self.response_transformation_matrix = group.variables["response_transformation_matrix"][
1961 ...
1962 ].data
1963 except KeyError:
1964 self.response_transformation_matrix = None
1965 try:
1966 self.output_transformation_matrix = group.variables["output_transformation_matrix"][
1967 ...
1968 ].data
1969 except KeyError:
1970 self.output_transformation_matrix = None
1971 self.define_transformation_matrices(None, dialog=False)
1972 # Specifications
1973 self.clear_and_update_specification_table()
1974 for index, (spec_name, spec_group) in enumerate(group["specifications"].groups.items()):
1975 if index > 0:
1976 self.add_sine_table_tab()
1977 frequency = spec_group["spec_frequency"][...]
1978 amplitude = spec_group["spec_amplitude"][...].transpose(1, 0)
1979 phase = spec_group["spec_phase"][...].transpose(1, 0)
1980 sweep_type = spec_group["spec_sweep_type"][...]
1981 sweep_rate = spec_group["spec_sweep_rate"][...].copy()
1982 sweep_rate[sweep_type == 1] = sweep_rate[sweep_type == 1] / 60
1983 sweep_type = ["lin" if val == 0 else "log" for val in sweep_type]
1984 warning = spec_group["spec_warning"][...].transpose(1, 2, 3, 0)
1985 abort = spec_group["spec_abort"][...].transpose(1, 2, 3, 0)
1986 start_time = spec_group.start_time
1987 self.sine_tables[-1].clear_and_update_specification_table(
1988 frequency,
1989 amplitude,
1990 phase,
1991 sweep_type,
1992 sweep_rate,
1993 warning,
1994 abort,
1995 start_time,
1996 spec_name,
1997 )
1998
1999 def update_gui(self, queue_data):
2000 if super().update_gui(queue_data):
2001 return
2002 message, data = queue_data
2003 if message == "request_prediction_plot_choices":
2004 self.log("Sending Prediction Plot Choices...")
2005 self.send_response_prediction_plot_choices()
2006 self.send_excitation_prediction_plot_choices()
2007 elif message == "excitation_prediction":
2008 self.plot_excitation_prediction(*data)
2009 elif message == "response_prediction":
2010 self.plot_response_prediction(*data)
2011 elif message == "response_error_matrix":
2012 self.update_response_matrix(*data)
2013 elif message == "excitation_voltage_list":
2014 self.update_voltage_list(data)
2015 elif message == "specification_for_plotting":
2016 (
2017 self.specification_signals_combined,
2018 self.specification_signals,
2019 self.specification_frequencies,
2020 self.specification_arguments,
2021 self.specification_amplitudes,
2022 self.specification_phases,
2023 self.plot_downsample,
2024 ) = data
2025 self.log(f"Plot Downsample: {self.plot_downsample}")
2026 self.update_run_plot(update_spec=True)
2027 elif message == "time_data":
2028 (last_excitation, last_control) = data
2029 self.achieved_excitation_signals_combined.append(last_excitation)
2030 self.achieved_response_signals_combined.append(last_control)
2031 elif message == "control_data":
2032 (
2033 last_signals,
2034 last_amplitudes,
2035 last_phases,
2036 last_frequencies,
2037 last_correction,
2038 last_errors,
2039 last_warning_flags,
2040 last_abort_flags,
2041 ) = data
2042 self.achieved_response_amplitudes.append(last_amplitudes)
2043 self.achieved_response_phases.append(last_phases)
2044 self.complex_drive_modifications.append(last_correction)
2045 self.achieved_excitation_frequencies.append(last_frequencies)
2046 self.achieved_excitation_signals.append(last_signals)
2047 self.update_control_run_plot()
2048 self.update_run_plot(update_spec=False)
2049 self.update_control_error_table(last_errors, last_warning_flags, last_abort_flags)
2050 elif message == "enable_control":
2051 self.enable_control(True)
2052 else:
2053 widget = None
2054 for parent in [
2055 self.definition_widget,
2056 self.run_widget,
2057 self.system_id_widget,
2058 self.prediction_widget,
2059 ]:
2060 try:
2061 widget = getattr(parent, message)
2062 break
2063 except AttributeError:
2064 continue
2065 if widget is None:
2066 raise ValueError(f"Cannot Update Widget {message}: not found in UI")
2067 if isinstance(widget, QtWidgets.QDoubleSpinBox):
2068 widget.setValue(data)
2069 elif isinstance(widget, QtWidgets.QSpinBox):
2070 widget.setValue(data)
2071 elif isinstance(widget, QtWidgets.QLineEdit):
2072 widget.setText(data)
2073 elif isinstance(widget, QtWidgets.QListWidget):
2074 widget.clear()
2075 widget.addItems([f"{d:.3f}" for d in data])
2076
2077 def set_parameters_from_template(self, worksheet):
2078 self.definition_widget.ramp_time_spinbox.setValue(float(worksheet.cell(2, 2).value))
2079 self.definition_widget.control_convergence_selector.setValue(
2080 float(worksheet.cell(3, 2).value)
2081 )
2082 self.definition_widget.update_drives_after_environment_selector.setChecked(
2083 worksheet.cell(4, 2).value.upper() == "Y"
2084 )
2085 self.definition_widget.best_fit_phase_checkbox.setChecked(
2086 worksheet.cell(5, 2).value.upper() == "Y"
2087 )
2088 self.definition_widget.auto_abort_checkbox.setChecked(
2089 worksheet.cell(6, 2).value.upper() == "Y"
2090 )
2091 self.definition_widget.buffer_blocks_selector.setValue(int(worksheet.cell(7, 2).value))
2092 self.definition_widget.filter_type_selector.setCurrentIndex(
2093 1 if worksheet.cell(8, 2).value.upper() == "VK" else 0
2094 )
2095 self.definition_widget.tracking_filter_cutoff_selector.setValue(
2096 float(worksheet.cell(9, 2).value)
2097 )
2098 self.definition_widget.tracking_filter_order_selector.setValue(
2099 int(worksheet.cell(10, 2).value)
2100 )
2101 self.definition_widget.vk_filter_order_selector.setCurrentIndex(
2102 int(worksheet.cell(11, 2).value) - 1
2103 )
2104 self.definition_widget.vk_filter_bandwidth_selector.setValue(
2105 float(worksheet.cell(12, 2).value)
2106 )
2107 self.definition_widget.vk_filter_block_size_selector.setValue(
2108 int(worksheet.cell(13, 2).value)
2109 )
2110 self.definition_widget.vk_filter_block_overlap_selector.setValue(
2111 float(worksheet.cell(14, 2).value)
2112 )
2113 if worksheet.cell(15, 2).value is not None and worksheet.cell(15, 2).value != "":
2114 self.select_python_module(None, worksheet.cell(15, 2).value)
2115 self.definition_widget.python_class_input.setCurrentIndex(
2116 self.definition_widget.python_class_input.findText(worksheet.cell(16, 2).value)
2117 )
2118 self.definition_widget.control_parameters_text_input.setText(
2119 "" if worksheet.cell(17, 2).value is None else str(worksheet.cell(17, 2).value)
2120 )
2121 column_index = 2
2122 while True:
2123 value = worksheet.cell(18, column_index).value
2124 if value is None or (isinstance(value, str) and value.strip() == ""):
2125 break
2126 item = self.definition_widget.control_channels_selector.item(int(value) - 1)
2127 item.setCheckState(Qt.Checked)
2128 column_index += 1
2129 self.system_id_widget.samplesPerFrameSpinBox.setValue(int(worksheet.cell(19, 2).value))
2130 self.system_id_widget.averagingTypeComboBox.setCurrentIndex(
2131 self.system_id_widget.averagingTypeComboBox.findText(worksheet.cell(20, 2).value)
2132 )
2133 self.system_id_widget.noiseAveragesSpinBox.setValue(int(worksheet.cell(21, 2).value))
2134 self.system_id_widget.systemIDAveragesSpinBox.setValue(int(worksheet.cell(22, 2).value))
2135 self.system_id_widget.averagingCoefficientDoubleSpinBox.setValue(
2136 float(worksheet.cell(23, 2).value)
2137 )
2138 self.system_id_widget.estimatorComboBox.setCurrentIndex(
2139 self.system_id_widget.estimatorComboBox.findText(worksheet.cell(24, 2).value)
2140 )
2141 self.system_id_widget.levelDoubleSpinBox.setValue(float(worksheet.cell(25, 2).value))
2142 self.system_id_widget.levelRampTimeDoubleSpinBox.setValue(
2143 float(worksheet.cell(26, 2).value)
2144 )
2145 self.system_id_widget.signalTypeComboBox.setCurrentIndex(
2146 self.system_id_widget.signalTypeComboBox.findText(worksheet.cell(27, 2).value)
2147 )
2148 self.system_id_widget.windowComboBox.setCurrentIndex(
2149 self.system_id_widget.windowComboBox.findText(worksheet.cell(28, 2).value)
2150 )
2151 self.system_id_widget.overlapDoubleSpinBox.setValue(float(worksheet.cell(29, 2).value))
2152 self.system_id_widget.onFractionDoubleSpinBox.setValue(float(worksheet.cell(30, 2).value))
2153 self.system_id_widget.pretriggerDoubleSpinBox.setValue(float(worksheet.cell(31, 2).value))
2154 self.system_id_widget.rampFractionDoubleSpinBox.setValue(float(worksheet.cell(32, 2).value))
2155
2156 # Now we need to find the transformation matrices' sizes
2157 response_channels = self.definition_widget.control_channels_display.value()
2158 output_channels = self.definition_widget.output_channels_display.value()
2159 output_transform_row = 35
2160 if (
2161 isinstance(worksheet.cell(34, 2).value, str)
2162 and worksheet.cell(34, 2).value.lower() == "none"
2163 ):
2164 self.response_transformation_matrix = None
2165 else:
2166 while True:
2167 if worksheet.cell(output_transform_row, 1).value == "Output Transformation Matrix:":
2168 break
2169 output_transform_row += 1
2170 response_size = output_transform_row - 34
2171 response_transformation = []
2172 for i in range(response_size):
2173 response_transformation.append([])
2174 for j in range(response_channels):
2175 response_transformation[-1].append(float(worksheet.cell(34 + i, 2 + j).value))
2176 self.response_transformation_matrix = np.array(response_transformation)
2177 if (
2178 isinstance(worksheet.cell(output_transform_row, 2).value, str)
2179 and worksheet.cell(output_transform_row, 2).value.lower() == "none"
2180 ):
2181 self.output_transformation_matrix = None
2182 else:
2183 output_transformation = []
2184 i = 0
2185 while True:
2186 if worksheet.cell(output_transform_row + i, 2).value is None or (
2187 isinstance(worksheet.cell(output_transform_row + i, 2).value, str)
2188 and worksheet.cell(output_transform_row + i, 2).value.strip() == ""
2189 ):
2190 break
2191 output_transformation.append([])
2192 for j in range(output_channels):
2193 output_transformation[-1].append(
2194 float(worksheet.cell(output_transform_row + i, 2 + j).value)
2195 )
2196 i += 1
2197 self.output_transformation_matrix = np.array(output_transformation)
2198 self.define_transformation_matrices(None, dialog=False)
2199
2200 # Load in the specification
2201 if worksheet.cell(33, 2).value:
2202 self.sine_tables[0].load_specification(None, worksheet.cell(33, 2).value)
2203 column_index = 3
2204 while True:
2205 if worksheet.cell(33, column_index).value:
2206 self.add_sine_table_tab()
2207 self.sine_tables[-1].load_specification(
2208 None, worksheet.cell(33, column_index).value
2209 )
2210 column_index += 1
2211 else:
2212 break
2213
2214 @staticmethod
2215 def create_environment_template(environment_name, workbook):
2216 worksheet = workbook.create_sheet(environment_name)
2217 worksheet.cell(1, 1, "Control Type")
2218 worksheet.cell(1, 2, "Sine")
2219 worksheet.cell(
2220 1,
2221 4,
2222 "Note: Replace cells with hash marks (#) to provide the requested parameters.",
2223 )
2224 worksheet.cell(2, 1, "Test Ramp Time")
2225 worksheet.cell(2, 2, "# Time for the test to ramp up or down when starting or stopping")
2226 worksheet.cell(3, 1, "Control Convergence")
2227 worksheet.cell(
2228 3,
2229 2,
2230 "# A scale factor on the closed-loop update to "
2231 "balance stability with speed of convergence",
2232 )
2233 worksheet.cell(4, 1, "Update Drives after Environment:")
2234 worksheet.cell(
2235 4,
2236 2,
2237 "# If Y, then a control calculation will be performed after the "
2238 "environment finishes to update the next drive signal (Y/N)",
2239 )
2240 worksheet.cell(5, 1, "Fit Phases")
2241 worksheet.cell(
2242 5,
2243 2,
2244 "# If Y, perform a best fit to phase quantities to accommodate time delays (Y/N)",
2245 )
2246 worksheet.cell(6, 1, "Allow Automatic Aborts")
2247 worksheet.cell(
2248 6,
2249 2,
2250 "# Shut down the test automatically if an abort level is reached (Y/N)",
2251 )
2252 worksheet.cell(7, 1, "Buffer Blocks")
2253 worksheet.cell(
2254 7,
2255 2,
2256 "# Number of write blocks to keep in the buffer to "
2257 "guard against running out of samples to generate",
2258 )
2259 worksheet.cell(8, 1, "Tracking Filter Type")
2260 worksheet.cell(
2261 8,
2262 2,
2263 "# Select the tracking filter type to use "
2264 "(VK - Vold-Kalman / DFT - Digital Tracking Filter)",
2265 )
2266 worksheet.cell(9, 1, "Digital Tracking Filter Cutoff Percent:")
2267 worksheet.cell(
2268 9,
2269 2,
2270 "# Tracking filter cutoff frequency compared to the instantaneous frequency",
2271 )
2272 worksheet.cell(10, 1, "Digital Tracking Filter Order")
2273 worksheet.cell(10, 2, "# Order of the Butterworth filter used in the tracking filter")
2274 worksheet.cell(11, 1, "VK Filter Order")
2275 worksheet.cell(11, 2, "# Order of the Vold-Kalman Filter (1, 2, or 3)")
2276 worksheet.cell(12, 1, "VK Filter Bandwidth")
2277 worksheet.cell(12, 2, "# Bandwidth of the Vold-Kalman Filter")
2278 worksheet.cell(13, 1, "VK Filter Block Size")
2279 worksheet.cell(13, 2, "# Number of samples in the filter blocks for the Vold-Kalman Filter")
2280 worksheet.cell(14, 1, "VK Filter Overlap")
2281 worksheet.cell(14, 2, "Overlap between frames in the VK filter as a fraction (0.5, not 50)")
2282 worksheet.cell(15, 1, "Custom Control Python Script:")
2283 worksheet.cell(15, 2, "# Path to the Python script containing the control law")
2284 worksheet.cell(16, 1, "Custom Control Python Class:")
2285 worksheet.cell(
2286 16,
2287 2,
2288 "# Class name within the Python Script that will serve as the control law",
2289 )
2290 worksheet.cell(17, 1, "Control Parameters:")
2291 worksheet.cell(17, 2, "# Extra parameters used in the control law")
2292 worksheet.cell(18, 1, "Control Channels (1-based):")
2293 worksheet.cell(18, 2, "# List of channels, one per cell on this row")
2294 worksheet.cell(19, 1, "System ID Samples per Frame")
2295 worksheet.cell(
2296 19,
2297 2,
2298 "# Number of Samples per Measurement Frame in the System Identification",
2299 )
2300 worksheet.cell(20, 1, "System ID Averaging:")
2301 worksheet.cell(20, 2, "# Averaging Type, should be Linear or Exponential")
2302 worksheet.cell(21, 1, "Noise Averages:")
2303 worksheet.cell(21, 2, "# Number of Averages used when characterizing noise")
2304 worksheet.cell(22, 1, "System ID Averages:")
2305 worksheet.cell(22, 2, "# Number of Averages used when computing the FRF")
2306 worksheet.cell(23, 1, "Exponential Averaging Coefficient:")
2307 worksheet.cell(23, 2, "# Averaging Coefficient for Exponential Averaging (if used)")
2308 worksheet.cell(24, 1, "System ID Estimator:")
2309 worksheet.cell(
2310 24,
2311 2,
2312 "# Technique used to compute system ID. Should be one of H1, H2, H3, or Hv.",
2313 )
2314 worksheet.cell(25, 1, "System ID Level (V RMS):")
2315 worksheet.cell(
2316 25,
2317 2,
2318 "# RMS Value of Flat Voltage Spectrum used for System Identification.",
2319 )
2320 worksheet.cell(26, 1, "System ID Ramp Time")
2321 worksheet.cell(
2322 26,
2323 2,
2324 "# Time for the system identification to ramp between levels or from start or to stop.",
2325 )
2326 worksheet.cell(27, 1, "System ID Signal Type:")
2327 worksheet.cell(27, 2, "# Signal to use for the system identification")
2328 worksheet.cell(28, 1, "System ID Window:")
2329 worksheet.cell(
2330 28,
2331 2,
2332 "# Window used to compute FRFs during system ID. Should be one of Hann or None",
2333 )
2334 worksheet.cell(29, 1, "System ID Overlap %:")
2335 worksheet.cell(29, 2, "# Overlap to use in the system identification")
2336 worksheet.cell(30, 1, "System ID Burst On %:")
2337 worksheet.cell(30, 2, "# Percentage of a frame that the burst random is on for")
2338 worksheet.cell(31, 1, "System ID Burst Pretrigger %:")
2339 worksheet.cell(
2340 31,
2341 2,
2342 "# Percentage of a frame that occurs before the burst starts in a burst random signal",
2343 )
2344 worksheet.cell(32, 1, "System ID Ramp Fraction %:")
2345 worksheet.cell(
2346 32,
2347 2,
2348 '# Percentage of the "System ID Burst On %" that will be used to ramp up to full level',
2349 )
2350 worksheet.cell(33, 1, "Specification File:")
2351 worksheet.cell(33, 2, "# Path to the file containing the Specification")
2352 worksheet.cell(34, 1, "Response Transformation Matrix:")
2353 worksheet.cell(
2354 34,
2355 2,
2356 (
2357 "# Transformation matrix to apply to the response channels. Type None if there is "
2358 "none. Otherwise, make this a 2D array in the spreadsheet and move the Output "
2359 "Transformation Matrix line down so it will fit. The number of columns should be "
2360 "the number of physical control channels."
2361 ),
2362 )
2363 worksheet.cell(35, 1, "Output Transformation Matrix:")
2364 worksheet.cell(
2365 35,
2366 2,
2367 "# Transformation matrix to apply to the outputs. Type None if there is none. "
2368 "Otherwise, make this a 2D array in the spreadsheet. The number of columns should be "
2369 "the number of physical output channels in the environment.",
2370 )
2371
2372
2373# %% Environment
2374
2375
2376class SineEnvironment(AbstractSysIdEnvironment):
2377 """Class representing the environment computations on a separate process from the main UI"""
2378
2379 def __init__(
2380 self,
2381 environment_name: str,
2382 queue_container: SineQueues,
2383 acquisition_active,
2384 output_active,
2385 ):
2386 """Initializes the sine environment computation class
2387
2388 Parameters
2389 ----------
2390 environment_name : str
2391 Name of the environment
2392 queue_container : SineQueues
2393 A container containing all the queues that the Sine environment needs to
2394 pass information between its parts
2395 acquisition_active : int
2396 A multiprocessing shared value that is used to tell all processes whether or not
2397 the acquisition is currently running
2398 output_active : int
2399 A multiprocessing shared value that is used to tell all processes whether or not
2400 the output is currently running
2401 """
2402 super().__init__(
2403 environment_name,
2404 queue_container.environment_command_queue,
2405 queue_container.gui_update_queue,
2406 queue_container.controller_communication_queue,
2407 queue_container.log_file_queue,
2408 queue_container.collector_command_queue,
2409 queue_container.signal_generation_command_queue,
2410 queue_container.spectral_command_queue,
2411 queue_container.data_analysis_command_queue,
2412 queue_container.data_in_queue,
2413 queue_container.data_out_queue,
2414 acquisition_active,
2415 output_active,
2416 )
2417 self.map_command(SineCommands.PERFORM_CONTROL_PREDICTION, self.perform_control_prediction)
2418 self.map_command(SineCommands.START_CONTROL, self.start_control)
2419 self.map_command(SineCommands.STOP_CONTROL, self.stop_environment)
2420 self.map_command(SineCommands.SAVE_CONTROL_DATA, self.save_control_data)
2421 self.map_command(SineCommands.SEND_RESPONSE_PREDICTION, self.send_response_prediction)
2422 self.map_command(SineCommands.SEND_EXCITATION_PREDICTION, self.send_excitation_prediction)
2423 # Persistent data
2424 self.data_acquisition_parameters = None
2425 self.environment_parameters = None
2426 self.queue_container = queue_container
2427 self.plot_downsample = None
2428 # Control data
2429 self.sysid_frequencies = None
2430 self.sysid_frf = None
2431 self.sysid_coherence = None
2432 self.sysid_response_cpsd = None
2433 self.sysid_reference_cpsd = None
2434 self.sysid_condition = None
2435 self.sysid_response_noise = None
2436 self.sysid_reference_noise = None
2437 self.sysid_frames = None
2438 self.control_class = None
2439 self.extra_control_parameters = None
2440 # Specification data
2441 self.specification_signals_combined = None
2442 self.specification_signals = None
2443 self.specification_frequencies = None
2444 self.specification_arguments = None
2445 self.specification_amplitudes = None
2446 self.specification_phases = None
2447 self.specification_start_indices = None
2448 self.specification_end_indices = None
2449 self.ramp_samples = None
2450 # Excitation Signal Data
2451 self.excitation_signals = None
2452 self.excitation_signals_combined = None
2453 self.excitation_signal_frequencies = None
2454 self.excitation_signal_arguments = None
2455 self.excitation_signal_amplitudes = None
2456 self.excitation_signal_phases = None
2457 self.peak_voltages = None
2458 # Predicted Response Data
2459 self.predicted_response_signals_combined = None
2460 self.predicted_response_signals = None
2461 self.predicted_response_amplitudes = None
2462 self.predicted_response_phases = None
2463 self.predicted_warning_matrix = None
2464 self.predicted_abort_matrix = None
2465 self.predicted_amplitude_error = None
2466 # Running data
2467 self.control_test_level = 0
2468 self.control_tones = None
2469 self.control_tone_indices = None
2470 self.control_start_time = None
2471 self.control_end_time = None
2472 self.control_time_delay = None
2473 self.control_write_index = 0
2474 self.control_read_index = 0
2475 self.control_analysis_index = 0
2476 self.control_finished = False
2477 self.control_analysis_finished = False
2478 self.control_startup = True
2479 self.control_first_signal = None
2480 self.control_response_signals_combined = None
2481 self.control_response_amplitudes = None
2482 self.control_response_phases = None
2483 self.control_response_frequencies = None
2484 self.control_response_arguments = None
2485 self.control_target_phases = None
2486 self.control_target_amplitudes = None
2487 self.control_specification_arguments = None
2488 self.control_drive_modifications = None
2489 self.control_block_size = None
2490 self.control_filters = None
2491 self.control_warning_flags = None
2492 self.control_abort_flags = None
2493 self.control_amplitude_errors = None
2494 self.control_start_index = None
2495 self.control_end_index = None
2496 self.good_line_threshold = 0.25
2497
2498 def initialize_environment_test_parameters(self, environment_parameters: SineMetadata):
2499 # Check if all specifications are equal
2500 if (
2501 self.environment_parameters is None
2502 or not np.array_equal(
2503 self.environment_parameters.control_channel_indices,
2504 environment_parameters.control_channel_indices,
2505 )
2506 or not (
2507 all(
2508 [
2509 spec1 == spec2
2510 for spec1, spec2 in zip(
2511 self.environment_parameters.specifications,
2512 environment_parameters.specifications,
2513 )
2514 ]
2515 )
2516 and (
2517 len(self.environment_parameters.specifications)
2518 == len(environment_parameters.specifications)
2519 )
2520 )
2521 ):
2522 self.sysid_frequencies = None
2523 self.sysid_frf = None
2524 self.sysid_coherence = None
2525 self.sysid_response_cpsd = None
2526 self.sysid_reference_cpsd = None
2527 self.sysid_condition = None
2528 self.sysid_response_noise = None
2529 self.sysid_reference_noise = None
2530 self.sysid_frames = None
2531 self.control_class = None
2532 self.extra_control_parameters = None
2533 self.excitation_signals_combined = None
2534 self.excitation_signals = None
2535 self.excitation_signal_frequencies = None
2536 self.excitation_signal_arguments = None
2537 self.excitation_signal_amplitudes = None
2538 self.excitation_signal_phases = None
2539 self.predicted_response_signals_combined = None
2540 self.predicted_response_signals = None
2541 self.predicted_response_amplitudes = None
2542 self.predicted_response_phases = None
2543 self.ramp_samples = None
2544 super().initialize_environment_test_parameters(environment_parameters)
2545 self.environment_parameters: SineMetadata
2546 if environment_parameters.control_python_script is None:
2547 control_class = DefaultSineControlLaw
2548 self.extra_control_parameters = environment_parameters.control_python_parameters
2549 else:
2550 _, file = os.path.split(environment_parameters.control_python_script)
2551 file, _ = os.path.splitext(file)
2552 spec = importlib.util.spec_from_file_location(
2553 file, environment_parameters.control_python_script
2554 )
2555 module = importlib.util.module_from_spec(spec)
2556 spec.loader.exec_module(module)
2557 self.extra_control_parameters = environment_parameters.control_python_parameters
2558 control_class = getattr(module, environment_parameters.control_python_class)
2559 self.control_class = control_class(
2560 self.data_acquisition_parameters.sample_rate,
2561 self.environment_parameters.specifications,
2562 self.data_acquisition_parameters.output_oversample,
2563 self.environment_parameters.ramp_time,
2564 self.environment_parameters.control_convergence,
2565 self.data_acquisition_parameters.samples_per_write,
2566 self.environment_parameters.buffer_blocks,
2567 self.extra_control_parameters, # Required parameters
2568 self.environment_parameters.sysid_frequency_spacing, # Frequency Spacing
2569 self.sysid_frf, # Transfer Functions
2570 self.sysid_response_noise, # Noise levels and correlation
2571 self.sysid_reference_noise, # from the system identification
2572 self.sysid_response_cpsd, # Response levels and correlation
2573 self.sysid_reference_cpsd, # from the system identification
2574 self.sysid_coherence, # Coherence from the system identification
2575 self.sysid_frames, # Number of frames in the FRF matrices
2576 )
2577 self.log("Creating Specification Signals...")
2578 (
2579 self.specification_signals_combined,
2580 self.specification_signals,
2581 self.specification_frequencies,
2582 self.specification_arguments,
2583 self.specification_amplitudes,
2584 self.specification_phases,
2585 self.specification_start_indices,
2586 self.specification_end_indices,
2587 ) = SineSpecification.create_combined_signals(
2588 self.environment_parameters.specifications,
2589 self.data_acquisition_parameters.sample_rate
2590 * self.data_acquisition_parameters.output_oversample,
2591 self.environment_parameters.ramp_samples
2592 * self.data_acquisition_parameters.output_oversample,
2593 )
2594 self.ramp_samples = (
2595 self.environment_parameters.ramp_samples
2596 * self.data_acquisition_parameters.output_oversample
2597 )
2598 self.plot_downsample = (
2599 self.specification_signals_combined.shape[-1]
2600 // self.data_acquisition_parameters.output_oversample
2601 // MAXIMUM_SAMPLES_TO_PLOT
2602 + 1
2603 )
2604 self.gui_update_queue.put(
2605 (
2606 self.environment_name,
2607 (
2608 "specification_for_plotting",
2609 (
2610 self.specification_signals_combined[
2611 ...,
2612 :: self.data_acquisition_parameters.output_oversample
2613 * self.plot_downsample,
2614 ],
2615 self.specification_signals[
2616 ...,
2617 :: self.data_acquisition_parameters.output_oversample
2618 * self.plot_downsample,
2619 ],
2620 self.specification_frequencies[
2621 ...,
2622 :: self.data_acquisition_parameters.output_oversample
2623 * self.plot_downsample,
2624 ],
2625 self.specification_arguments[
2626 ...,
2627 :: self.data_acquisition_parameters.output_oversample
2628 * self.plot_downsample,
2629 ],
2630 self.specification_amplitudes[
2631 ...,
2632 :: self.data_acquisition_parameters.output_oversample
2633 * self.plot_downsample,
2634 ],
2635 wrap(
2636 self.specification_phases[
2637 ...,
2638 :: self.data_acquisition_parameters.output_oversample
2639 * self.plot_downsample,
2640 ]
2641 )
2642 * 180
2643 / np.pi,
2644 self.plot_downsample,
2645 ),
2646 ),
2647 )
2648 )
2649 self.log("Done!")
2650
2651 def system_id_complete(self, data):
2652 # print('Finished System Identification')
2653 self.log("Finished System Identification")
2654 super().system_id_complete(data)
2655 (
2656 self.sysid_frames,
2657 _,
2658 self.sysid_frequencies,
2659 self.sysid_frf,
2660 self.sysid_coherence,
2661 self.sysid_response_cpsd,
2662 self.sysid_reference_cpsd,
2663 self.sysid_condition,
2664 self.sysid_response_noise,
2665 self.sysid_reference_noise,
2666 ) = data
2667 # Perform the control prediction
2668 self.perform_control_prediction(True)
2669
2670 def filter_predicted_signal(self):
2671 """Extract amplitude and phase information from predicted signals"""
2672 # print('Filtering Predicted Signal')
2673 predicted_signals = []
2674 predicted_amplitudes = []
2675 predicted_phases = []
2676 arguments = self.excitation_signal_arguments[
2677 ..., :: self.data_acquisition_parameters.output_oversample
2678 ]
2679 frequencies = self.excitation_signal_frequencies[
2680 ..., :: self.data_acquisition_parameters.output_oversample
2681 ]
2682 for signal in self.predicted_response_signals_combined:
2683 if self.environment_parameters.tracking_filter_type == 0:
2684 block_size = self.data_acquisition_parameters.samples_per_read
2685 generator = [
2686 digital_tracking_filter_generator(
2687 dt=1 / self.environment_parameters.sample_rate,
2688 cutoff_frequency_ratio=self.environment_parameters.tracking_filter_cutoff,
2689 filter_order=self.environment_parameters.tracking_filter_order,
2690 )
2691 for tone in self.excitation_signals
2692 ]
2693 for gen in generator:
2694 gen.send(None)
2695 else:
2696 block_size = self.environment_parameters.vk_filter_blocksize
2697 generator = vold_kalman_filter_generator(
2698 sample_rate=self.environment_parameters.sample_rate,
2699 num_orders=self.excitation_signals.shape[0],
2700 block_size=block_size,
2701 overlap=self.environment_parameters.vk_filter_overlap,
2702 bandwidth=self.environment_parameters.vk_filter_bandwidth,
2703 filter_order=self.environment_parameters.vk_filter_order,
2704 buffer_size_factor=self.environment_parameters.buffer_blocks + 1,
2705 )
2706 generator.send(None)
2707
2708 # print(f"{self.signal.shape=}")
2709 start_index = 0
2710 reconstructed_signals = []
2711 reconstructed_amplitudes = []
2712 reconstructed_phases = []
2713
2714 last_data = False
2715 while not last_data:
2716 end_index = start_index + block_size
2717 block = signal[start_index:end_index]
2718 block_arguments = arguments[:, start_index:end_index]
2719 block_frequencies = frequencies[:, start_index:end_index]
2720 last_data = end_index >= signal.size
2721 # print(f"{block.shape=}, {block_arguments.shape=}, "
2722 # f"{block_frequencies.shape=}, {last_data=}, {block_size=}")
2723 if self.environment_parameters.tracking_filter_type == 0:
2724 amps = []
2725 phss = []
2726 for arg, freq, gen in zip(block_arguments, block_frequencies, generator):
2727 amp, phs = gen.send((block, freq, arg))
2728 amps.append(amp)
2729 phss.append(phs)
2730 reconstructed_amplitudes.append(np.array(amps))
2731 reconstructed_phases.append(np.array(phss))
2732 reconstructed_signals.append(
2733 np.array(amps) * np.cos(block_arguments + np.array(phss))
2734 )
2735 else:
2736 vk_signals, vk_amplitudes, vk_phases = generator.send(
2737 (block, block_arguments, last_data)
2738 )
2739 if vk_signals is not None:
2740 reconstructed_signals.append(vk_signals)
2741 reconstructed_amplitudes.append(vk_amplitudes)
2742 reconstructed_phases.append(vk_phases)
2743 start_index += block_size
2744
2745 predicted_signals.append(np.concatenate(reconstructed_signals, axis=-1))
2746 predicted_amplitudes.append(np.concatenate(reconstructed_amplitudes, axis=-1))
2747 predicted_phases.append(np.concatenate(reconstructed_phases, axis=-1))
2748
2749 self.predicted_response_signals = np.array(predicted_signals).transpose(1, 0, 2)
2750 self.predicted_response_amplitudes = np.array(predicted_amplitudes).transpose(1, 0, 2)
2751 self.predicted_response_phases = np.array(predicted_phases).transpose(1, 0, 2)
2752 # Pull out the data to compute maximum amplitude error
2753 self.predicted_amplitude_error = np.zeros(self.specification_amplitudes.shape[:2])
2754 for tone_index, (specs, preds, start_index, end_index) in enumerate(
2755 zip(
2756 self.specification_amplitudes,
2757 self.predicted_response_amplitudes,
2758 self.specification_start_indices,
2759 self.specification_end_indices,
2760 )
2761 ):
2762 for channel_index, (spec, pred) in enumerate(zip(specs, preds)):
2763 spec = spec[
2764 start_index : end_index : self.data_acquisition_parameters.output_oversample
2765 ]
2766 pred = pred[
2767 start_index
2768 // self.data_acquisition_parameters.output_oversample : start_index
2769 // self.data_acquisition_parameters.output_oversample
2770 + spec.size
2771 ]
2772 max_error = np.max(np.abs(scale2db(pred / spec)))
2773 self.predicted_amplitude_error[tone_index, channel_index] = max_error
2774
2775 def compare_predictions_to_warning_and_abort(self):
2776 """Compares the extracted prediction information to abort and warning levels"""
2777 specs = self.environment_parameters.specifications
2778 amps = self.predicted_response_amplitudes
2779 warning_matrix = np.zeros(amps.shape[:2], dtype=bool)
2780 abort_matrix = np.zeros(amps.shape[:2], dtype=bool)
2781 for tone_index in range(amps.shape[0]):
2782 freqs = self.excitation_signal_frequencies[
2783 tone_index,
2784 self.specification_start_indices[tone_index] : self.specification_end_indices[
2785 tone_index
2786 ] : self.data_acquisition_parameters.output_oversample,
2787 ]
2788 for channel_index in range(amps.shape[1]):
2789 warning_levels = specs[tone_index].interpolate_warning(channel_index, freqs)
2790 abort_levels = specs[tone_index].interpolate_abort(channel_index, freqs)
2791 predicted = amps[
2792 tone_index,
2793 channel_index,
2794 self.specification_start_indices[tone_index]
2795 // self.data_acquisition_parameters.output_oversample : self.specification_start_indices[
2796 tone_index
2797 ]
2798 // self.data_acquisition_parameters.output_oversample
2799 + freqs.size,
2800 ]
2801 warning_ratio = predicted / warning_levels
2802 if np.any(warning_ratio[0] < 1.0):
2803 warning_matrix[tone_index, channel_index] = True
2804 if np.any(warning_ratio[1] > 1.0):
2805 warning_matrix[tone_index, channel_index] = True
2806 abort_ratio = predicted / abort_levels
2807 if np.any(abort_ratio[0] < 1.0):
2808 abort_matrix[tone_index, channel_index] = True
2809 if np.any(abort_ratio[1] > 1.0):
2810 abort_matrix[tone_index, channel_index] = True
2811 self.predicted_warning_matrix = warning_matrix
2812 self.predicted_abort_matrix = abort_matrix
2813
2814 def perform_control_prediction(self, sysid_update):
2815 """Compute the prediction from the test by convolving with the transfer functions"""
2816 # print('Performing Control Prediction')
2817 if self.sysid_frf is None:
2818 self.gui_update_queue.put(
2819 (
2820 "error",
2821 (
2822 "Perform System Identification",
2823 "Perform System ID before performing test predictions",
2824 ),
2825 )
2826 )
2827 return
2828 # print('Computing Drive Signal')
2829 if sysid_update:
2830 self.log("Updating System Identification...")
2831 (
2832 self.excitation_signals,
2833 self.excitation_signal_frequencies,
2834 self.excitation_signal_arguments,
2835 self.excitation_signal_amplitudes,
2836 self.excitation_signal_phases,
2837 ) = self.control_class.system_id_update(
2838 self.environment_parameters.sysid_frequency_spacing,
2839 self.sysid_frf, # Transfer Functions
2840 self.sysid_response_noise, # Noise levels and correlation
2841 self.sysid_reference_noise, # from the system identification
2842 self.sysid_response_cpsd, # Response levels and correlation
2843 self.sysid_reference_cpsd, # from the system identification
2844 self.sysid_coherence, # Coherence from the system identification
2845 self.sysid_frames, # Number of frames in the CPSD and FRF matrices
2846 )
2847 self.excitation_signals_combined = np.sum(self.excitation_signals, axis=0)
2848 self.peak_voltages = np.max(np.abs(self.excitation_signals_combined), axis=-1)
2849 self.log("Done!")
2850 # print('Performing Response Prediction')
2851 # print('Drive Signals {:}'.format(self.next_drive.shape))
2852 drive_signals = self.excitation_signals_combined[
2853 :, :: self.data_acquisition_parameters.output_oversample
2854 ]
2855 impulse_responses = np.moveaxis(np.fft.irfft(self.sysid_frf, axis=0), 0, -1)
2856
2857 self.log("Predicting Test Response...")
2858 self.predicted_response_signals_combined = np.zeros(
2859 (impulse_responses.shape[0], drive_signals.shape[-1])
2860 )
2861
2862 for i, impulse_response_row in enumerate(impulse_responses):
2863 for impulse, drive in zip(impulse_response_row, drive_signals):
2864 # print('Convolving {:},{:}'.format(i,j))
2865 self.predicted_response_signals_combined[i, :] += sig.convolve(
2866 drive, impulse, "full"
2867 )[: drive_signals.shape[-1]]
2868 self.log("Done!")
2869
2870 self.log("Filtering Predicted Signals...")
2871 self.filter_predicted_signal()
2872 self.log("Done!")
2873
2874 # print('From Performing Control Predictions')
2875 # print(f'{self.excitation_signals_combined.shape=}')
2876 # print(f'{self.excitation_signals.shape=}')
2877 # print(f'{self.excitation_signal_frequencies.shape=}')
2878 # print(f'{self.excitation_signal_arguments.shape=}')
2879 # print(f'{self.excitation_signal_amplitudes.shape=}')
2880 # print(f'{self.excitation_signal_phases.shape=}')
2881 # print(f'{self.ramp_samples=}')
2882 # print(f'{self.predicted_response_signals_combined.shape=}')
2883 # print(f'{self.predicted_response_signals.shape=}')
2884 # print(f'{self.predicted_response_amplitudes.shape=}')
2885 # print(f'{self.predicted_response_phases.shape=}')
2886
2887 self.log("Comparing to Warning and Abort Curves")
2888 self.compare_predictions_to_warning_and_abort()
2889 self.log("Done!")
2890
2891 self.log("Showing Test Predictions...")
2892 self.show_test_prediction()
2893 self.log("Done!")
2894
2895 def show_test_prediction(self):
2896 """Starts the process to show the predictions by requesting the current plot choices"""
2897 self.gui_update_queue.put(
2898 (self.environment_name, ("request_prediction_plot_choices", None))
2899 )
2900 self.gui_update_queue.put(
2901 (self.environment_name, ("excitation_voltage_list", self.peak_voltages))
2902 )
2903 self.gui_update_queue.put(
2904 (
2905 self.environment_name,
2906 (
2907 "response_error_matrix",
2908 (
2909 self.predicted_amplitude_error,
2910 self.predicted_warning_matrix,
2911 self.predicted_abort_matrix,
2912 ),
2913 ),
2914 )
2915 )
2916
2917 def send_excitation_prediction(self, excitation_plot_choices):
2918 """Sends the predicted excitation for the channel, tone, and data type requested"""
2919 channel_index, type_index, tone_index = excitation_plot_choices
2920 # print(f'Excitation Predictions: {channel_index=}, {type_index=}, {tone_index}')
2921 if type_index == 0: # Time histories
2922 if tone_index == -1:
2923 ordinate = self.excitation_signals_combined[
2924 channel_index,
2925 :: self.plot_downsample * self.data_acquisition_parameters.output_oversample,
2926 ]
2927 abscissa = (
2928 np.arange(ordinate.shape[-1])
2929 / self.data_acquisition_parameters.sample_rate
2930 * self.plot_downsample
2931 )
2932 else:
2933 ordinate = self.excitation_signals[
2934 tone_index,
2935 channel_index,
2936 :: self.plot_downsample * self.data_acquisition_parameters.output_oversample,
2937 ]
2938 abscissa = (
2939 np.arange(ordinate.shape[-1])
2940 / self.data_acquisition_parameters.sample_rate
2941 * self.plot_downsample
2942 )
2943 elif type_index == 1: # Amplitude Vs Time
2944 ordinate = self.excitation_signal_amplitudes[
2945 tone_index,
2946 channel_index,
2947 :: self.plot_downsample * self.data_acquisition_parameters.output_oversample,
2948 ]
2949 abscissa = (
2950 np.arange(ordinate.shape[-1])
2951 / self.data_acquisition_parameters.sample_rate
2952 * self.plot_downsample
2953 )
2954 elif type_index == 2: # Phase Vs Time
2955 ordinate = (
2956 wrap(
2957 self.excitation_signal_phases[
2958 tone_index,
2959 channel_index,
2960 :: self.plot_downsample
2961 * self.data_acquisition_parameters.output_oversample,
2962 ]
2963 )
2964 * 180
2965 / np.pi
2966 )
2967 abscissa = (
2968 np.arange(ordinate.shape[-1])
2969 / self.data_acquisition_parameters.sample_rate
2970 * self.plot_downsample
2971 )
2972 elif type_index == 3: # Amplitude Vs Frequency
2973 ordinate = self.excitation_signal_amplitudes[
2974 tone_index,
2975 channel_index,
2976 :: self.plot_downsample * self.data_acquisition_parameters.output_oversample,
2977 ]
2978 abscissa = self.specification_frequencies[
2979 tone_index,
2980 :: self.plot_downsample * self.data_acquisition_parameters.output_oversample,
2981 ]
2982 elif type_index == 4: # Phase Vs Frequency
2983 ordinate = (
2984 wrap(
2985 self.excitation_signal_phases[
2986 tone_index,
2987 channel_index,
2988 :: self.plot_downsample
2989 * self.data_acquisition_parameters.output_oversample,
2990 ]
2991 )
2992 * 180
2993 / np.pi
2994 )
2995 abscissa = self.specification_frequencies[
2996 tone_index,
2997 :: self.plot_downsample * self.data_acquisition_parameters.output_oversample,
2998 ]
2999 else:
3000 raise ValueError(f"Undefined type_index {type_index}")
3001 # print(f'{ordinate.shape=}, {abscissa.shape=}')
3002 # print(f'{abscissa.min()=}, {abscissa.max()=}')
3003 self.gui_update_queue.put(
3004 (self.environment_name, ("excitation_prediction", (abscissa, ordinate)))
3005 )
3006
3007 def send_response_prediction(self, response_plot_choices):
3008 """Sends the response predictions at the requested channel, tone, and data type"""
3009 channel_index, type_index, tone_index = response_plot_choices
3010 # print(f'Response Predictions: {channel_index=}, {type_index=}, {tone_index}')
3011 if type_index == 0: # Time histories
3012 if tone_index == -1:
3013 ordinate = [
3014 self.specification_signals_combined[
3015 channel_index,
3016 :: self.plot_downsample
3017 * self.data_acquisition_parameters.output_oversample,
3018 ],
3019 self.predicted_response_signals_combined[
3020 channel_index, :: self.plot_downsample
3021 ],
3022 ]
3023 abscissa = (
3024 np.arange(max(v.shape[-1] for v in ordinate))
3025 / self.data_acquisition_parameters.sample_rate
3026 * self.plot_downsample
3027 )
3028 else:
3029 ordinate = [
3030 self.specification_signals[
3031 tone_index,
3032 channel_index,
3033 :: self.plot_downsample
3034 * self.data_acquisition_parameters.output_oversample,
3035 ],
3036 self.predicted_response_signals[
3037 tone_index, channel_index, :: self.plot_downsample
3038 ],
3039 ]
3040 abscissa = (
3041 np.arange(max(v.shape[-1] for v in ordinate))
3042 / self.data_acquisition_parameters.sample_rate
3043 * self.plot_downsample
3044 )
3045 elif type_index == 1: # Amplitude Vs Time
3046 ordinate = [
3047 self.specification_amplitudes[
3048 tone_index,
3049 channel_index,
3050 :: self.plot_downsample * self.data_acquisition_parameters.output_oversample,
3051 ],
3052 self.predicted_response_amplitudes[
3053 tone_index, channel_index, :: self.plot_downsample
3054 ],
3055 ]
3056 abscissa = (
3057 np.arange(max(v.shape[-1] for v in ordinate))
3058 / self.data_acquisition_parameters.sample_rate
3059 * self.plot_downsample
3060 )
3061 elif type_index == 2: # Phase Vs Time
3062 ordinate = [
3063 self.specification_phases[
3064 tone_index,
3065 channel_index,
3066 :: self.plot_downsample * self.data_acquisition_parameters.output_oversample,
3067 ]
3068 * 180
3069 / np.pi,
3070 self.predicted_response_phases[tone_index, channel_index, :: self.plot_downsample]
3071 * 180
3072 / np.pi,
3073 ]
3074 abscissa = (
3075 np.arange(max(v.shape[-1] for v in ordinate))
3076 / self.data_acquisition_parameters.sample_rate
3077 * self.plot_downsample
3078 )
3079 elif type_index == 3: # Amplitude Vs Frequency
3080 ordinate = [
3081 self.specification_amplitudes[
3082 tone_index,
3083 channel_index,
3084 :: self.plot_downsample * self.data_acquisition_parameters.output_oversample,
3085 ],
3086 self.predicted_response_amplitudes[
3087 tone_index, channel_index, :: self.plot_downsample
3088 ],
3089 ]
3090 abscissa = self.specification_frequencies[
3091 tone_index,
3092 :: self.plot_downsample * self.data_acquisition_parameters.output_oversample,
3093 ]
3094 elif type_index == 4: # Phase Vs Frequency
3095 ordinate = [
3096 self.specification_phases[
3097 tone_index,
3098 channel_index,
3099 :: self.plot_downsample * self.data_acquisition_parameters.output_oversample,
3100 ]
3101 * 180
3102 / np.pi,
3103 self.predicted_response_phases[tone_index, channel_index, :: self.plot_downsample]
3104 * 180
3105 / np.pi,
3106 ]
3107 abscissa = self.specification_frequencies[
3108 tone_index,
3109 :: self.plot_downsample * self.data_acquisition_parameters.output_oversample,
3110 ]
3111 else:
3112 raise ValueError(f"Undefined type_index {type_index}")
3113 # print(f'{ordinate[0].shape=}, {ordinate[1].shape=}, {abscissa.shape=}')
3114 # print(f'{abscissa.min()=}, {abscissa.max()=}')
3115 self.gui_update_queue.put(
3116 (self.environment_name, ("response_prediction", (abscissa, ordinate)))
3117 )
3118
3119 def compute_spec_amplitudes_and_phases(self):
3120 """Computes amplitude and phase information from the specification"""
3121 spec_ordinates = []
3122 spec_amplitudes = []
3123 spec_phases = []
3124 spec_frequencies = []
3125 spec_arguments = []
3126
3127 for channel_index in range(len(self.environment_parameters.control_channel_indices)):
3128 spec = self.environment_parameters.specification
3129 # Convert octave per min to octave per second
3130 sweep_rates = spec["sweep_rate"].copy()
3131 sweep_rates[spec["sweep_type"] == 1] = sweep_rates[spec["sweep_type"] == 1] / 60
3132 # Create the sweep types array
3133 sweep_types = [
3134 "lin" if sweep_type == 0 else "log" for sweep_type in spec["sweep_type"][:-1]
3135 ]
3136 spec_ordinate, spec_argument, spec_frequency, spec_amplitude, spec_phase = sine_sweep(
3137 1 / self.data_acquisition_parameters.sample_rate,
3138 spec["frequency"],
3139 sweep_rates,
3140 sweep_types,
3141 spec["amplitude"][:, channel_index],
3142 spec["phase"][:, channel_index],
3143 return_frequency=True,
3144 return_argument=True,
3145 return_amplitude=True,
3146 return_phase=True,
3147 )
3148 spec_ordinates.append(spec_ordinate)
3149 spec_amplitudes.append(spec_amplitude)
3150 spec_phases.append(spec_phase)
3151 spec_arguments.append(spec_argument)
3152 spec_frequencies.append(spec_frequency)
3153
3154 return (
3155 np.array(spec_ordinates),
3156 np.array(spec_arguments),
3157 np.array(spec_frequencies),
3158 np.array(spec_amplitudes),
3159 np.array(spec_phases),
3160 )
3161
3162 def get_signal_generation_metadata(self):
3163 """Gets a SignalGenerationMetadata object for the current environment"""
3164 return SignalGenerationMetadata(
3165 samples_per_write=self.data_acquisition_parameters.samples_per_write,
3166 level_ramp_samples=self.environment_parameters.ramp_time
3167 * self.environment_parameters.sample_rate
3168 * self.data_acquisition_parameters.output_oversample,
3169 output_transformation_matrix=self.environment_parameters.reference_transformation_matrix,
3170 )
3171
3172 def start_control(self, data):
3173 """
3174 Starts up and runs the control with the specified test level,
3175 tones, start and end times
3176 """
3177 if self.control_startup:
3178 self.log("Starting Environment")
3179 # Read in the starting parameters
3180 (
3181 self.control_test_level,
3182 self.control_tones,
3183 self.control_start_time,
3184 self.control_end_time,
3185 ) = data
3186 if self.control_tones is not None and len(self.control_tones) == 0:
3187 self.control_tones = None
3188 if self.control_tones is None:
3189 self.control_tones = slice(None)
3190 self.control_tone_indices = np.arange(self.excitation_signal_arguments.shape[0])
3191 else:
3192 self.control_tone_indices = self.control_tones
3193 # Precompute the number of channels for convenience
3194 n_control_channels = (
3195 len(self.environment_parameters.control_channel_indices)
3196 if self.environment_parameters.response_transformation_matrix is None
3197 else self.environment_parameters.response_transformation_matrix.shape[0]
3198 )
3199 n_output_channels = (
3200 len(self.environment_parameters.output_channel_indices)
3201 if self.environment_parameters.reference_transformation_matrix is None
3202 else self.environment_parameters.reference_transformation_matrix.shape[0]
3203 )
3204
3205 self.control_time_delay = (
3206 None # We will need to compute this when we get our first data point
3207 )
3208 if DEBUG:
3209 num_files = len(glob(FILE_OUTPUT.format("*")))
3210 np.savez(
3211 FILE_OUTPUT.format(num_files),
3212 excitation_signal=self.control_first_signal,
3213 done_controlling=False,
3214 )
3215 # print('Parsing Indices')
3216 # Parse out frequency information to get the indices into the full
3217 # arrays that we are controlling to
3218 if self.control_start_time is None:
3219 self.control_start_index = 0
3220 else:
3221 self.control_start_index = int(
3222 self.control_start_time
3223 * self.data_acquisition_parameters.sample_rate
3224 * self.data_acquisition_parameters.output_oversample
3225 )
3226 if self.control_start_index < 0:
3227 self.control_start_index = 0
3228 if self.control_end_time is None:
3229 self.control_end_index = None
3230 else:
3231 self.control_end_index = (
3232 int(
3233 self.control_end_time
3234 * self.data_acquisition_parameters.sample_rate
3235 * self.data_acquisition_parameters.output_oversample
3236 )
3237 + 2 * self.ramp_samples
3238 )
3239 if (
3240 self.control_end_index is None
3241 or self.control_end_index > self.specification_arguments.shape[-1]
3242 ):
3243 self.control_end_index = self.specification_arguments.shape[-1]
3244 # Check to make sure if we are controlling to a subset of the tones that we
3245 # aren't starting with a bunch of zeros
3246 if self.control_tones is not None:
3247 first_nonzero_index = np.min(
3248 np.argmax(
3249 self.excitation_signal_amplitudes[self.control_tones] != 0,
3250 axis=-1,
3251 )
3252 )
3253 if first_nonzero_index > self.control_start_index:
3254 self.control_start_index = first_nonzero_index
3255 control_slice = slice(self.control_start_index, self.control_end_index)
3256
3257 # print('Initializing Control')
3258 # Call the control law to get the first of its signals
3259 self.control_first_signal = self.control_class.initialize_control(
3260 self.control_tones, self.control_start_index, self.control_end_index
3261 )
3262
3263 # print('Constructing Control Arrays')
3264 # Construct the full arrays that we are controlling to
3265 self.control_specification_arguments = self.excitation_signal_arguments[
3266 self.control_tones, control_slice
3267 ]
3268
3269 # print('Setting Up Tracking Filters')
3270 # Set up the tracking filters to track amplitude and phase information
3271 self.control_filters = []
3272 if self.environment_parameters.tracking_filter_type == 0:
3273 self.control_block_size = self.data_acquisition_parameters.samples_per_read
3274 else:
3275 self.control_block_size = self.environment_parameters.vk_filter_blocksize
3276 for signal in self.predicted_response_signals_combined:
3277 if self.environment_parameters.tracking_filter_type == 0:
3278 generator = [
3279 digital_tracking_filter_generator(
3280 dt=1 / self.environment_parameters.sample_rate,
3281 cutoff_frequency_ratio=self.environment_parameters.tracking_filter_cutoff,
3282 filter_order=self.environment_parameters.tracking_filter_order,
3283 )
3284 for tone in self.control_specification_arguments
3285 ]
3286 for gen in generator:
3287 gen.send(None)
3288 self.control_filters.append(generator)
3289 else:
3290 generator = vold_kalman_filter_generator(
3291 sample_rate=self.environment_parameters.sample_rate,
3292 num_orders=self.control_specification_arguments.shape[0],
3293 block_size=self.control_block_size,
3294 overlap=self.environment_parameters.vk_filter_overlap,
3295 bandwidth=self.environment_parameters.vk_filter_bandwidth,
3296 filter_order=self.environment_parameters.vk_filter_order,
3297 buffer_size_factor=self.environment_parameters.buffer_blocks + 1,
3298 )
3299 generator.send(None)
3300 self.control_filters.append(generator)
3301
3302 # Set up empty warning and abort flags
3303 self.control_warning_flags = np.zeros(
3304 (self.control_specification_arguments.shape[0], n_control_channels),
3305 dtype=bool,
3306 )
3307 self.control_abort_flags = np.zeros(
3308 (self.control_specification_arguments.shape[0], n_control_channels),
3309 dtype=bool,
3310 )
3311 self.control_amplitude_errors = np.zeros(
3312 (self.control_specification_arguments.shape[0], n_control_channels)
3313 )
3314
3315 # print('Setting up Signal Generation')
3316 # Set up the signal generation
3317 self.siggen_shutdown_achieved = False
3318 self.queue_container.signal_generation_command_queue.put(
3319 self.environment_name,
3320 (
3321 SignalGenerationCommands.INITIALIZE_PARAMETERS,
3322 self.get_signal_generation_metadata(),
3323 ),
3324 )
3325 self.queue_container.signal_generation_command_queue.put(
3326 self.environment_name,
3327 (
3328 SignalGenerationCommands.INITIALIZE_SIGNAL_GENERATOR,
3329 ContinuousTransientSignalGenerator(
3330 num_samples_per_frame=self.data_acquisition_parameters.samples_per_write,
3331 num_signals=n_output_channels,
3332 signal=self.control_first_signal,
3333 last_signal=False,
3334 ),
3335 ),
3336 )
3337 self.queue_container.signal_generation_command_queue.put(
3338 self.environment_name,
3339 (SignalGenerationCommands.SET_TEST_LEVEL, self.control_test_level),
3340 )
3341 # Tell the signal generation to start generating signals
3342 self.queue_container.signal_generation_command_queue.put(
3343 self.environment_name, (SignalGenerationCommands.GENERATE_SIGNALS, None)
3344 )
3345
3346 # print('Setting up last arguments')
3347 self.control_write_index = self.control_first_signal.shape[-1]
3348 self.control_read_index = 0
3349 self.control_analysis_index = 0
3350 self.control_finished = False
3351 self.control_analysis_finished = False
3352 self.control_response_signals_combined = []
3353 self.control_response_amplitudes = []
3354 self.control_response_phases = []
3355 self.control_drive_modifications = []
3356 self.control_response_frequencies = self.excitation_signal_frequencies[
3357 self.control_tones,
3358 self.control_start_index : self.control_end_index : self.data_acquisition_parameters.output_oversample,
3359 ]
3360 self.control_response_arguments = self.excitation_signal_arguments[
3361 self.control_tones,
3362 self.control_start_index : self.control_end_index : self.data_acquisition_parameters.output_oversample,
3363 ]
3364 self.control_target_phases = self.specification_phases[
3365 self.control_tones,
3366 :,
3367 self.control_start_index : self.control_end_index : self.data_acquisition_parameters.output_oversample,
3368 ]
3369 self.control_target_amplitudes = self.specification_amplitudes[
3370 self.control_tones,
3371 :,
3372 self.control_start_index : self.control_end_index : self.data_acquisition_parameters.output_oversample,
3373 ]
3374 self.control_startup = False
3375 # See if any data has come in
3376 try:
3377 # print('Listening for Data')
3378 acquisition_data, last_acquisition = self.queue_container.data_in_queue.get_nowait()
3379 # print('Got Data')
3380 if last_acquisition:
3381 self.log(
3382 "Acquired Last Data, Signal Generation Shutdown "
3383 f"Achieved: {self.siggen_shutdown_achieved}"
3384 )
3385 else:
3386 self.log("Acquired Data")
3387 scale_factor = 0.0 if self.control_test_level < 1e-10 else 1 / self.control_test_level
3388 # print('Parsing Control and Excitation Data')
3389 control_data = (
3390 acquisition_data[self.environment_parameters.control_channel_indices] * scale_factor
3391 )
3392 if self.environment_parameters.response_transformation_matrix is not None:
3393 control_data = (
3394 self.environment_parameters.response_transformation_matrix @ control_data
3395 )
3396 excitation_data = (
3397 acquisition_data[self.environment_parameters.output_channel_indices] * scale_factor
3398 )
3399 if self.environment_parameters.reference_transformation_matrix is not None:
3400 excitation_data = (
3401 self.environment_parameters.reference_transformation_matrix @ excitation_data
3402 )
3403 block_size = acquisition_data.shape[-1]
3404 block_slice = slice(self.control_read_index, self.control_read_index + block_size)
3405 # print(f'Read Block Range {block_slice}')
3406 # Send the time data to the GUI
3407 # print('Sending Time Data')
3408 self.gui_update_queue.put(
3409 (
3410 self.environment_name,
3411 (
3412 "time_data",
3413 (
3414 excitation_data[..., :: self.plot_downsample],
3415 control_data[..., :: self.plot_downsample],
3416 ),
3417 ),
3418 )
3419 )
3420 self.control_response_signals_combined.append(control_data)
3421 # Find the time delay between the first signal and what we've
3422 # measured
3423 # print('Computing Time Delay')
3424 if self.control_first_signal is not None:
3425 first_signal = self.control_first_signal[
3426 ..., :: self.data_acquisition_parameters.output_oversample
3427 ][..., : excitation_data.shape[-1]]
3428 reference_fft = np.fft.rfft(first_signal, axis=-1)
3429 this_fft = np.fft.rfft(excitation_data, axis=-1)
3430 freq = np.fft.rfftfreq(
3431 first_signal.shape[-1],
3432 1 / self.data_acquisition_parameters.sample_rate,
3433 )
3434 good_lines = (
3435 np.abs(reference_fft) / np.max(np.abs(reference_fft), axis=-1, keepdims=True)
3436 > self.good_line_threshold
3437 )
3438 good_lines[..., 0] = False
3439 phase_difference = np.angle(this_fft / reference_fft)
3440 phase_slope = np.median(
3441 phase_difference[good_lines]
3442 / np.broadcast_to(freq, phase_difference.shape)[good_lines]
3443 )
3444 self.control_time_delay = phase_slope / (2 * np.pi)
3445 # print(f"Time Delay: {self.control_time_delay=}")
3446 if DEBUG:
3447 np.savez(
3448 "debug_data/first_signal_sine_control.npz",
3449 first_signal=self.control_first_signal,
3450 excitation_data=excitation_data,
3451 )
3452 self.control_first_signal = None
3453 # Analyze the recovered data
3454 if not self.control_analysis_finished:
3455 # print('Analyzing Data')
3456 achieved_signals = []
3457 achieved_amplitudes = []
3458 achieved_phases = []
3459 block_frequencies = self.control_response_frequencies[..., block_slice]
3460 block_arguments = self.control_response_arguments[..., block_slice]
3461 # print('Block Frequencies')
3462 # print(block_frequencies)
3463 # Check if this is the last control data we will be getting
3464 self.control_analysis_finished = (
3465 block_slice.stop >= self.control_response_frequencies.shape[-1]
3466 )
3467 # print(f'Is Last Data? {self.control_analysis_finished=}')
3468 # Truncate just in case we've gotten some extra data in the acquisition
3469 block_signal = control_data[..., : block_arguments.shape[-1]]
3470 # print('Filtering Data to extract amplitude and phase')
3471 start_time = time.time()
3472 if self.environment_parameters.tracking_filter_type == 0:
3473 for signal, tone_filters in zip(block_signal, self.control_filters):
3474 amps = []
3475 phss = []
3476 for tone_argument, tone_frequency, tone_filter in zip(
3477 block_arguments, block_frequencies, tone_filters
3478 ):
3479 amp, phs = tone_filter.send((signal, tone_frequency, tone_argument))
3480 amps.append(amp)
3481 phss.append(phs) # Radians
3482 achieved_amplitudes.append(np.array(amps))
3483 achieved_phases.append(np.array(phss))
3484 achieved_signals.append(
3485 np.array(amps) * np.cos(block_arguments + np.array(phss)) # Radians
3486 )
3487 else:
3488 for signal, vk_filter in zip(block_signal, self.control_filters):
3489 vk_signals, vk_amplitudes, vk_phases = vk_filter.send(
3490 (signal, block_arguments, self.control_analysis_finished)
3491 )
3492 achieved_amplitudes.append(vk_amplitudes)
3493 achieved_phases.append(vk_phases) # Radians
3494 achieved_signals.append(vk_signals)
3495
3496 finish_time = time.time()
3497 self.log(f"Signal filtering achieved in {finish_time - start_time:0.2f}s.")
3498 achieved_signals = np.array(achieved_signals)
3499 achieved_amplitudes = np.array(achieved_amplitudes)
3500 achieved_phases = np.array(achieved_phases)
3501 if not np.all(
3502 achieved_signals == None # noqa: E711 # pylint: disable=singleton-comparison
3503 ):
3504 # print('Got Amplitude and Phase Data')
3505 block_start = self.control_analysis_index
3506 block_end = self.control_analysis_index + achieved_signals.shape[-1]
3507 block_slice = slice(block_start, block_end)
3508 # print(f'Analysis Block Range {block_slice}')
3509 achieved_frequencies = self.control_response_frequencies[..., block_slice]
3510 # print('Analysis Frequencies')
3511 # print(achieved_frequencies)
3512 # print(f'Achieved Frequency Size {achieved_frequencies.shape=}')
3513 achieved_signals = achieved_signals.transpose(1, 0, 2)
3514 self.log(f"Analyzing Dataset With Size {achieved_signals.shape}")
3515 # print(f'Achieved Signals Size {achieved_signals.shape=}')
3516 achieved_amplitudes = achieved_amplitudes.transpose(1, 0, 2)
3517 # print(f'Achieved Amplitudes Size {achieved_signals.shape=}')
3518 # Correct for time delay on the phases, newaxis to broadcast across signals
3519 achieved_phases = (
3520 achieved_phases.transpose(1, 0, 2)
3521 - self.control_time_delay
3522 * 2
3523 * np.pi
3524 * achieved_frequencies[:, np.newaxis, :]
3525 )
3526 # print(f'Achieved Phases Size {achieved_phases.shape=}')
3527 # Here I want to do the best fit to the phases, need to compare phase
3528 # achieved vs phase desired
3529 if self.environment_parameters.phase_fit:
3530 # print('Fitting Phases')
3531 target = self.control_target_amplitudes[..., block_slice] * np.exp(
3532 1j * self.control_target_phases[..., block_slice]
3533 )
3534 achieved = achieved_amplitudes * np.exp(1j * achieved_phases)
3535 phase_change_fit = np.angle(np.sum(target * achieved.conj()))
3536 achieved_phases = achieved_phases + phase_change_fit
3537 # print('Computing Drive Updates')
3538 drive_modification = self.control_class.update_control(
3539 achieved_signals,
3540 achieved_amplitudes,
3541 achieved_phases, # Radians
3542 achieved_frequencies,
3543 self.control_time_delay,
3544 )
3545 self.control_drive_modifications.append(drive_modification)
3546 # Need to develop data for the table. First we need to pick out "valid"
3547 # data, which will exclude the ramp-ups and ramp-downs
3548 for tone_index in range(achieved_signals.shape[0]):
3549 full_tone_index = self.control_tone_indices[tone_index]
3550 compare_start = max(
3551 (
3552 self.specification_start_indices[full_tone_index]
3553 - self.control_start_index
3554 )
3555 // self.data_acquisition_parameters.output_oversample,
3556 block_start,
3557 self.ramp_samples // self.data_acquisition_parameters.output_oversample,
3558 )
3559 compare_end = min(
3560 (
3561 self.specification_end_indices[full_tone_index]
3562 - self.control_start_index
3563 )
3564 // self.data_acquisition_parameters.output_oversample,
3565 block_end,
3566 (self.control_end_index - self.control_start_index - self.ramp_samples)
3567 // self.data_acquisition_parameters.output_oversample,
3568 )
3569 if compare_start >= compare_end:
3570 continue
3571 block_start_offset = compare_start - block_start
3572 block_end_offset = compare_end - block_start
3573 amplitudes = achieved_amplitudes[
3574 tone_index, :, block_start_offset:block_end_offset
3575 ]
3576 compare_amplitudes = self.control_target_amplitudes[
3577 tone_index, :, compare_start:compare_end
3578 ]
3579 compare_frequencies = self.control_response_frequencies[
3580 tone_index, compare_start:compare_end
3581 ]
3582 self.control_amplitude_errors[tone_index] = np.max(
3583 np.abs(scale2db(amplitudes / compare_amplitudes)), axis=-1
3584 )
3585 if np.any(np.isinf(self.control_amplitude_errors[tone_index])):
3586 self.log(
3587 f"Found Infinities:\n{amplitudes.shape=} "
3588 f"{compare_amplitudes.shape=}"
3589 )
3590 self.log(f"Infinity Frequencies: {compare_frequencies}")
3591 self.log(
3592 f"Comparison Amplitudes: "
3593 f"{compare_amplitudes[np.isinf(self.control_amplitude_errors[tone_index])]}"
3594 )
3595 for channel_index in range(amplitudes.shape[0]):
3596 compare_warnings = self.environment_parameters.specifications[
3597 full_tone_index
3598 ].interpolate_warning(channel_index, compare_frequencies)
3599 compare_aborts = self.environment_parameters.specifications[
3600 full_tone_index
3601 ].interpolate_abort(channel_index, compare_frequencies)
3602 warning_ratio = amplitudes[channel_index] / compare_warnings
3603 abort_ratio = amplitudes[channel_index] / compare_aborts
3604 if np.any(warning_ratio[0] < 1.0):
3605 self.control_warning_flags[tone_index, channel_index] = True
3606 self.log(
3607 f"Lower Warning at Tone {full_tone_index} Channel "
3608 f"{channel_index} Frequency "
3609 f"{compare_frequencies[warning_ratio[0] < 1.0]}"
3610 )
3611 self.log(
3612 f"Amplitudes: {amplitudes[channel_index, warning_ratio[0] < 1.0]}"
3613 )
3614 self.log(
3615 f"Warning Level: {compare_warnings[0, warning_ratio[0] < 1.0]}"
3616 )
3617 if np.any(warning_ratio[1] > 1.0):
3618 self.control_warning_flags[tone_index, channel_index] = True
3619 self.log(
3620 f"Upper Warning at Tone {full_tone_index} Channel "
3621 f"{channel_index} Frequency "
3622 f"{compare_frequencies[warning_ratio[1] > 1.0]}"
3623 )
3624 self.log(
3625 f"Amplitudes: {amplitudes[channel_index, warning_ratio[1] > 1.0]}"
3626 )
3627 self.log(
3628 f"Warning Level: {compare_warnings[1, warning_ratio[1] > 1.0]}"
3629 )
3630 if np.any(abort_ratio[0] < 1.0):
3631 self.control_abort_flags[tone_index, channel_index] = True
3632 self.log(
3633 f"Lower Abort at Tone {full_tone_index} Channel "
3634 f"{channel_index} Frequency "
3635 f"{compare_frequencies[abort_ratio[0] < 1.0]}"
3636 )
3637 self.log(
3638 f"Amplitudes: {amplitudes[channel_index, abort_ratio[0] < 1.0]}"
3639 )
3640 self.log(f"Abort Level: {compare_aborts[0, abort_ratio[0] < 1.0]}")
3641 if np.any(abort_ratio[1] > 1.0):
3642 self.control_abort_flags[tone_index, channel_index] = True
3643 self.log(
3644 f"Upper Abort at Tone {full_tone_index} Channel {channel_index} "
3645 f"Frequency {compare_frequencies[abort_ratio[1] > 1.0]}"
3646 )
3647 self.log(
3648 f"Amplitudes: {amplitudes[channel_index, abort_ratio[1] > 1.0]}"
3649 )
3650 self.log(f"Abort Level: {compare_aborts[1, abort_ratio[1] > 1.0]}")
3651 # print('Populating Full Block Data')
3652 full_achieved_signals = np.zeros(
3653 (self.specification_signals.shape[0],) + achieved_signals.shape[1:]
3654 )
3655 full_achieved_signals[self.control_tones] = achieved_signals
3656 full_achieved_amplitudes = np.zeros(
3657 (self.specification_amplitudes.shape[0],) + achieved_amplitudes.shape[1:]
3658 )
3659 full_achieved_amplitudes[self.control_tones] = achieved_amplitudes
3660 full_achieved_phases = np.zeros(
3661 (self.specification_phases.shape[0],) + achieved_phases.shape[1:]
3662 )
3663 full_achieved_phases[self.control_tones] = achieved_phases
3664 full_achieved_frequencies = np.zeros(
3665 (self.specification_frequencies.shape[0],) + achieved_frequencies.shape[1:]
3666 )
3667 full_achieved_frequencies[self.control_tones] = achieved_frequencies
3668 full_drive_modification = np.zeros(
3669 (self.specification_frequencies.shape[0],) + drive_modification.shape[1:],
3670 dtype=complex,
3671 )
3672 full_drive_modification[self.control_tones] = drive_modification
3673 full_achieved_amplitude_errors = np.zeros(
3674 (self.specification_signals.shape[0],)
3675 + self.control_amplitude_errors.shape[1:]
3676 )
3677 full_achieved_amplitude_errors[self.control_tones] = (
3678 self.control_amplitude_errors
3679 )
3680 full_achieved_warning_flags = np.zeros(
3681 (self.specification_signals.shape[0],)
3682 + self.control_warning_flags.shape[1:],
3683 dtype=bool,
3684 )
3685 full_achieved_warning_flags[self.control_tones] = self.control_warning_flags
3686 full_achieved_abort_flags = np.zeros(
3687 (self.specification_signals.shape[0],) + self.control_abort_flags.shape[1:],
3688 dtype=bool,
3689 )
3690 full_achieved_abort_flags[self.control_tones] = self.control_abort_flags
3691 self.control_response_amplitudes.append(achieved_amplitudes)
3692 self.control_response_phases.append(achieved_phases)
3693
3694 # print('Sending Block Data to GUI')
3695 self.gui_update_queue.put(
3696 (
3697 self.environment_name,
3698 (
3699 "control_data",
3700 (
3701 full_achieved_signals[..., :: self.plot_downsample],
3702 full_achieved_amplitudes[..., :: self.plot_downsample],
3703 full_achieved_phases[..., :: self.plot_downsample]
3704 * 180
3705 / np.pi,
3706 full_achieved_frequencies[..., :: self.plot_downsample],
3707 full_drive_modification,
3708 full_achieved_amplitude_errors,
3709 full_achieved_warning_flags,
3710 full_achieved_abort_flags,
3711 ),
3712 ),
3713 )
3714 )
3715
3716 self.control_analysis_index += achieved_signals.shape[-1]
3717 # Check if we're done analyzing control outputs and generate the next outputs
3718 if self.control_analysis_finished:
3719 # print('Analysis is Finished')
3720 self.log("Analysis Finished")
3721 (
3722 self.excitation_signals,
3723 self.excitation_signal_frequencies,
3724 self.excitation_signal_arguments,
3725 self.excitation_signal_amplitudes,
3726 self.excitation_signal_phases, # Degrees
3727 self.ramp_samples,
3728 ) = self.control_class.finalize_control()
3729 self.control_read_index += acquisition_data.shape[-1]
3730 # Now we need to see if we need to write more data to the controller
3731 # This will be related to our current read index and write index for the control;
3732 # we don't want to run out of samples being generated on the output hardware
3733 # print('Checking if New Data is Needed')
3734 if (
3735 self.control_write_index // self.data_acquisition_parameters.output_oversample
3736 < self.control_read_index
3737 + self.environment_parameters.buffer_blocks
3738 * self.data_acquisition_parameters.samples_per_write
3739 ) and not self.control_finished:
3740 # print('Generating New Data')
3741 self.log("Generating New Data")
3742 excitation_signal, self.control_finished = self.control_class.generate_signal()
3743 # print('Data Generation Complete')
3744 if self.control_finished:
3745 # print('Generated Last Data')
3746 self.log("Control Finished")
3747 if DEBUG:
3748 print("Writing Debug File")
3749 num_files = len(glob(FILE_OUTPUT.format("*")))
3750 np.savez(
3751 FILE_OUTPUT.format(num_files),
3752 excitation_signal=excitation_signal,
3753 done_controlling=self.control_finished,
3754 )
3755 self.log(f"Excitation Size: {np.sqrt(np.mean(excitation_signal**2))=}")
3756 self.queue_container.time_history_to_generate_queue.put(
3757 (excitation_signal, self.control_finished)
3758 )
3759 self.control_write_index += excitation_signal.shape[-1]
3760 except mp.queues.Empty:
3761 # print("Didn't Find Data")
3762 last_acquisition = False
3763 # See if we need to keep going
3764 if self.siggen_shutdown_achieved and last_acquisition:
3765 self.shutdown()
3766 else:
3767 self.queue_container.environment_command_queue.put(
3768 self.environment_name, (SineCommands.START_CONTROL, None)
3769 )
3770
3771 def shutdown(self):
3772 """Handles the environment after it has shut down"""
3773 self.log("Environment Shut Down")
3774 self.log(f"Before Flush: {self.queue_container.time_history_to_generate_queue.qsize()=}")
3775 flush_queue(self.queue_container.time_history_to_generate_queue, timeout=0.01)
3776 self.log(f"After Flush: {self.queue_container.time_history_to_generate_queue.qsize()=}")
3777 self.gui_update_queue.put((self.environment_name, ("enable_control", None)))
3778 self.control_startup = True
3779
3780 def stop_environment(self, data):
3781 """Sends a signal to start the shutdown process"""
3782 self.queue_container.signal_generation_command_queue.put(
3783 self.environment_name, (SignalGenerationCommands.START_SHUTDOWN, None)
3784 )
3785
3786 def save_control_data(self, filename):
3787 """Saves the control data to a numpy file"""
3788 output_dict = {}
3789 for label in [
3790 "control_response_signals_combined",
3791 "control_response_amplitudes",
3792 "control_response_phases",
3793 "control_drive_modifications",
3794 ]:
3795 for index, array in enumerate(getattr(self, label)):
3796 output_dict[f"{label}_{index}"] = array
3797 for label in [
3798 "control_response_frequencies",
3799 "control_response_arguments",
3800 "control_target_phases",
3801 "control_target_amplitudes",
3802 ]:
3803 output_dict[label] = getattr(self, label)
3804 output_dict["sample_rate"] = self.data_acquisition_parameters.sample_rate
3805 output_dict["output_oversample"] = self.data_acquisition_parameters.output_oversample
3806 output_dict["names"] = [spec.name for spec in self.environment_parameters.specifications]
3807 np.savez(filename, **output_dict)
3808
3809
3810# %% Process
3811
3812
3813def sine_process(
3814 environment_name: str,
3815 input_queue: VerboseMessageQueue,
3816 gui_update_queue: Queue,
3817 controller_communication_queue: VerboseMessageQueue,
3818 log_file_queue: Queue,
3819 data_in_queue: Queue,
3820 data_out_queue: Queue,
3821 acquisition_active,
3822 output_active,
3823):
3824 """A function to be used by multiprocessing to run the Sine environment. It sets up
3825 the class and kicks off the run loop.
3826
3827 Parameters
3828 ----------
3829 environment_name : str
3830 The name of the environment
3831 input_queue : VerboseMessageQueue
3832 A queue used to provide commands to the environment
3833 gui_update_queue : Queue
3834 A queue used to provide updates to the user interface from the environment
3835 controller_communication_queue : VerboseMessageQueue
3836 A queue used to communicate with the larger controller
3837 log_file_queue : Queue
3838 A queue used to handle logging
3839 data_in_queue : Queue
3840 A queue used to send data to the environment from the acqusition process
3841 data_out_queue : Queue
3842 A queue used to send data to the output process from the environment
3843 acquisition_active : int
3844 A multiprocessing value used as a flag to show when the acquisition is running
3845 output_active : int
3846 A multiprocessing value used as a flag to show when the output is running
3847 """
3848 try:
3849 # Create vibration queues
3850 queue_container = SineQueues(
3851 environment_name,
3852 input_queue,
3853 gui_update_queue,
3854 controller_communication_queue,
3855 data_in_queue,
3856 data_out_queue,
3857 log_file_queue,
3858 )
3859
3860 spectral_proc = mp.Process(
3861 target=spectral_processing_process,
3862 args=(
3863 environment_name,
3864 queue_container.spectral_command_queue,
3865 queue_container.data_for_spectral_computation_queue,
3866 queue_container.updated_spectral_quantities_queue,
3867 queue_container.environment_command_queue,
3868 queue_container.gui_update_queue,
3869 queue_container.log_file_queue,
3870 ),
3871 )
3872 spectral_proc.start()
3873 analysis_proc = mp.Process(
3874 target=sysid_data_analysis_process,
3875 args=(
3876 environment_name,
3877 queue_container.data_analysis_command_queue,
3878 queue_container.updated_spectral_quantities_queue,
3879 queue_container.time_history_to_generate_queue,
3880 queue_container.environment_command_queue,
3881 queue_container.gui_update_queue,
3882 queue_container.log_file_queue,
3883 ),
3884 )
3885 analysis_proc.start()
3886 siggen_proc = mp.Process(
3887 target=signal_generation_process,
3888 args=(
3889 environment_name,
3890 queue_container.signal_generation_command_queue,
3891 queue_container.time_history_to_generate_queue,
3892 queue_container.data_out_queue,
3893 queue_container.environment_command_queue,
3894 queue_container.log_file_queue,
3895 queue_container.gui_update_queue,
3896 ),
3897 )
3898 siggen_proc.start()
3899 collection_proc = mp.Process(
3900 target=data_collector_process,
3901 args=(
3902 environment_name,
3903 queue_container.collector_command_queue,
3904 queue_container.data_in_queue,
3905 [queue_container.data_for_spectral_computation_queue],
3906 queue_container.environment_command_queue,
3907 queue_container.log_file_queue,
3908 queue_container.gui_update_queue,
3909 ),
3910 )
3911 collection_proc.start()
3912
3913 process_class = SineEnvironment(
3914 environment_name, queue_container, acquisition_active, output_active
3915 )
3916 process_class.run()
3917
3918 # Rejoin all the processes
3919 process_class.log("Joining Subprocesses")
3920 process_class.log("Joining Spectral Computation")
3921 spectral_proc.join()
3922 process_class.log("Joining Data Analysis")
3923 analysis_proc.join()
3924 process_class.log("Joining Signal Generation")
3925 siggen_proc.join()
3926 process_class.log("Joining Data Collection")
3927 collection_proc.join()
3928 except Exception: # pylint: disable=broad-exception-caught
3929 print(traceback.format_exc())