Coverage for / opt / hostedtoolcache / Python / 3.11.14 / x64 / lib / python3.11 / site-packages / rattlesnake / components / modal_environment.py: 31%
1147 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-27 18:22 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-27 18:22 +0000
1# -*- coding: utf-8 -*-
2"""
3This file defines a Modal Testing Environment where users can perform
4hammer or shaker modal tests and export FRFs and other relevant data.
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.
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.
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.
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"""
25import inspect
26import multiprocessing as mp
27import multiprocessing.sharedctypes # pylint: disable=unused-import
28import os
29import time
30from enum import Enum
31from glob import glob
32from multiprocessing.queues import Queue
34import netCDF4 as nc4
35import numpy as np
36import openpyxl
37import scipy.signal as sig
38from qtpy import QtWidgets, uic
39from qtpy.QtCore import Qt
41from .abstract_environment import AbstractEnvironment, AbstractMetadata, AbstractUI
42from .environments import (
43 ControlTypes,
44 environment_definition_ui_paths,
45 environment_run_ui_paths,
46)
47from .signal_generation import (
48 BurstRandomSignalGenerator,
49 ChirpSignalGenerator,
50 PseudorandomSignalGenerator,
51 RandomSignalGenerator,
52 SineSignalGenerator,
53 SquareSignalGenerator,
54)
55from .ui_utilities import ModalMDISubWindow, multiline_plotter
56from .utilities import (
57 DataAcquisitionParameters,
58 GlobalCommands,
59 VerboseMessageQueue,
60 error_message_qt,
61 flush_queue,
62 load_python_module,
63)
65CONTROL_TYPE = ControlTypes.MODAL
66MAXIMUM_NAME_LENGTH = 50
68WAIT_TIME = 0.02
71class ModalCommands(Enum):
72 """Valid commands for the modal environment"""
74 START_CONTROL = 0
75 STOP_CONTROL = 1
76 ACCEPT_FRAME = 2
77 RUN_CONTROL = 3
78 CHECK_FOR_COMPLETE_SHUTDOWN = 4
81class ModalQueues:
82 """A set of queues used by the modal environment"""
84 def __init__(
85 self,
86 environment_name: str,
87 environment_command_queue: VerboseMessageQueue,
88 gui_update_queue: mp.queues.Queue,
89 controller_communication_queue: VerboseMessageQueue,
90 data_in_queue: mp.queues.Queue,
91 data_out_queue: mp.queues.Queue,
92 log_file_queue: VerboseMessageQueue,
93 ):
94 """
95 Creates a namespace to store all the queues used by the Modal Environment
97 Parameters
98 ----------
99 environment_command_queue : VerboseMessageQueue
100 Queue from which the environment will receive instructions.
101 gui_update_queue : mp.queues.Queue
102 Queue to which the environment will put GUI updates.
103 controller_communication_queue : VerboseMessageQueue
104 Queue to which the environment will put global contorller instructions.
105 data_in_queue : mp.queues.Queue
106 Queue from which the environment will receive data from acquisition.
107 data_out_queue : mp.queues.Queue
108 Queue to which the environment will write data for output.
109 log_file_queue : VerboseMessageQueue
110 Queue to which the environment will write log file messages.
111 """
112 self.environment_command_queue = environment_command_queue
113 self.gui_update_queue = gui_update_queue
114 self.controller_communication_queue = controller_communication_queue
115 self.data_in_queue = data_in_queue
116 self.data_out_queue = data_out_queue
117 self.log_file_queue = log_file_queue
118 self.data_for_spectral_computation_queue = mp.Queue()
119 self.updated_spectral_quantities_queue = mp.Queue()
120 self.signal_generation_update_queue = mp.Queue()
121 self.spectral_command_queue = VerboseMessageQueue(
122 log_file_queue, environment_name + " Spectral Computation Command Queue"
123 )
124 self.collector_command_queue = VerboseMessageQueue(
125 log_file_queue, environment_name + " Data Collector Command Queue"
126 )
127 self.signal_generation_command_queue = VerboseMessageQueue(
128 log_file_queue, environment_name + " Signal Generation Command Queue"
129 )
132class ModalMetadata(AbstractMetadata):
133 """Class for storing metadata for an environment.
135 This class is used as a storage container for parameters used by an
136 environment. It is returned by the environment UI's
137 ``collect_environment_definition_parameters`` function as well as its
138 ``initialize_environment`` function. Various parts of the controller and
139 environment will query the class's data members for parameter values.
140 """
142 def __init__(
143 self,
144 sample_rate: float,
145 samples_per_frame: int,
146 averaging_type: str,
147 num_averages: int,
148 averaging_coefficient: float,
149 frf_technique: str,
150 frf_window: str,
151 overlap_percent: float,
152 trigger_type: str,
153 accept_type: str,
154 wait_for_steady_state: float,
155 trigger_channel: int,
156 pretrigger_percent: float,
157 trigger_slope_positive: bool,
158 trigger_level_percent: float,
159 hysteresis_level_percent: float,
160 hysteresis_frame_percent: float,
161 signal_generator_type: str,
162 signal_generator_level: float,
163 signal_generator_min_frequency: float,
164 signal_generator_max_frequency: float,
165 signal_generator_on_percent: float,
166 acceptance_function,
167 reference_channel_indices,
168 response_channel_indices,
169 output_channel_indices,
170 data_acquisition_parameters: DataAcquisitionParameters,
171 exponential_window_value_at_frame_end: float,
172 ):
173 self.sample_rate = sample_rate
174 self.samples_per_frame = samples_per_frame
175 self.averaging_type = averaging_type
176 self.num_averages = num_averages
177 self.averaging_coefficient = averaging_coefficient
178 self.frf_technique = frf_technique
179 self.frf_window = frf_window
180 self.overlap = overlap_percent / 100
181 self.trigger_type = trigger_type
182 self.accept_type = accept_type
183 self.wait_for_steady_state = wait_for_steady_state
184 self.trigger_channel = trigger_channel
185 self.pretrigger = pretrigger_percent / 100
186 self.trigger_slope_positive = trigger_slope_positive
187 self.trigger_level = trigger_level_percent / 100
188 self.hysteresis_level = hysteresis_level_percent / 100
189 self.hysteresis_length = hysteresis_frame_percent / 100
190 self.signal_generator_type = signal_generator_type
191 self.signal_generator_level = signal_generator_level
192 self.signal_generator_min_frequency = signal_generator_min_frequency
193 self.signal_generator_max_frequency = signal_generator_max_frequency
194 self.signal_generator_on_fraction = signal_generator_on_percent / 100
195 self.acceptance_function = acceptance_function
196 self.reference_channel_indices = reference_channel_indices
197 self.response_channel_indices = response_channel_indices
198 self.output_channel_indices = output_channel_indices
199 self.exponential_window_value_at_frame_end = exponential_window_value_at_frame_end
200 # Set up signal generator
201 self.output_oversample = data_acquisition_parameters.output_oversample
202 self.signal_generator = self.get_signal_generator()
204 def get_signal_generator(self):
205 """Gets a signal generator object that the modal environment will use to generate signals"""
206 if self.signal_generator_type == "none":
207 signal_generator = PseudorandomSignalGenerator(
208 rms=0.0,
209 sample_rate=self.sample_rate,
210 num_samples_per_frame=self.samples_per_frame,
211 num_signals=len(self.output_channel_indices),
212 low_frequency_cutoff=self.signal_generator_min_frequency,
213 high_frequency_cutoff=self.signal_generator_max_frequency,
214 output_oversample=self.output_oversample,
215 )
216 elif self.signal_generator_type == "random":
217 signal_generator = RandomSignalGenerator(
218 rms=self.signal_generator_level,
219 sample_rate=self.sample_rate,
220 num_samples_per_frame=self.samples_per_frame,
221 num_signals=len(self.output_channel_indices),
222 low_frequency_cutoff=self.signal_generator_min_frequency,
223 high_frequency_cutoff=self.signal_generator_max_frequency,
224 cola_overlap=0.5,
225 cola_window="hann",
226 cola_exponent=0.5,
227 output_oversample=self.output_oversample,
228 )
229 elif self.signal_generator_type == "pseudorandom":
230 signal_generator = PseudorandomSignalGenerator(
231 rms=self.signal_generator_level,
232 sample_rate=self.sample_rate,
233 num_samples_per_frame=self.samples_per_frame,
234 num_signals=len(self.output_channel_indices),
235 low_frequency_cutoff=self.signal_generator_min_frequency,
236 high_frequency_cutoff=self.signal_generator_max_frequency,
237 output_oversample=self.output_oversample,
238 )
239 elif self.signal_generator_type == "burst":
240 signal_generator = BurstRandomSignalGenerator(
241 rms=self.signal_generator_level,
242 sample_rate=self.sample_rate,
243 num_samples_per_frame=self.samples_per_frame,
244 num_signals=len(self.output_channel_indices),
245 low_frequency_cutoff=self.signal_generator_min_frequency,
246 high_frequency_cutoff=self.signal_generator_max_frequency,
247 on_fraction=self.signal_generator_on_fraction,
248 ramp_fraction=0.05,
249 output_oversample=self.output_oversample,
250 )
251 elif self.signal_generator_type == "chirp":
252 signal_generator = ChirpSignalGenerator(
253 level=self.signal_generator_level,
254 sample_rate=self.sample_rate,
255 num_samples_per_frame=self.samples_per_frame,
256 num_signals=len(self.output_channel_indices),
257 low_frequency_cutoff=self.signal_generator_min_frequency,
258 high_frequency_cutoff=self.signal_generator_max_frequency,
259 output_oversample=self.output_oversample,
260 )
261 elif self.signal_generator_type == "square":
262 signal_generator = SquareSignalGenerator(
263 level=self.signal_generator_level,
264 sample_rate=self.sample_rate,
265 num_samples_per_frame=self.samples_per_frame,
266 num_signals=len(self.output_channel_indices),
267 frequency=self.signal_generator_min_frequency,
268 phase=0,
269 on_fraction=self.signal_generator_on_fraction,
270 output_oversample=self.output_oversample,
271 )
272 elif self.signal_generator_type == "sine":
273 signal_generator = SineSignalGenerator(
274 level=self.signal_generator_level,
275 sample_rate=self.sample_rate,
276 num_samples_per_frame=self.samples_per_frame,
277 num_signals=len(self.output_channel_indices),
278 frequency=self.signal_generator_min_frequency,
279 phase=0,
280 output_oversample=self.output_oversample,
281 )
282 else:
283 raise ValueError(f"Invalid Signal Type {self.signal_generator_type}")
284 return signal_generator
286 @property
287 def samples_per_acquire(self):
288 """Property returning the samples per acquisition step given the overlap"""
289 return int(self.samples_per_frame * (1 - self.overlap))
291 @property
292 def frame_time(self):
293 """Property returning the time per measurement frame"""
294 return self.samples_per_frame / self.sample_rate
296 @property
297 def nyquist_frequency(self):
298 """Property returning half the sample rate"""
299 return self.sample_rate / 2
301 @property
302 def fft_lines(self):
303 """Property returning the frequency lines given the sampling parameters"""
304 return self.samples_per_frame // 2 + 1
306 @property
307 def skip_frames(self):
308 """Property returning the number of frames to skip while waiting for steady state"""
309 return int(
310 np.ceil(
311 self.wait_for_steady_state
312 * self.sample_rate
313 / (self.samples_per_frame * (1 - self.overlap))
314 )
315 )
317 @property
318 def frequency_spacing(self):
319 """Property returning frequency line spacing given the sampling parameters"""
320 return self.sample_rate / self.samples_per_frame
322 def get_trigger_levels(self, channels):
323 """Gets the trigger levels for a channel based on the channel table information
325 Parameters
326 ----------
327 channels : list of Channel
328 A list of channels in the environment
330 Returns
331 -------
332 trigger_level_v
333 The trigger level in volts
334 trigger_level_eu
335 The trigger level in engineering units defined in the channel table
336 hysterisis_level_v
337 The level that the signal must return below before another trigger can be accepted,
338 in volts
339 hysterisis_level_eu
340 The level that the signal must return below before another trigger can be accepted,
341 in engineering units defined in the channel table
342 """
343 channel = channels[self.trigger_channel]
344 try:
345 volt_range = float(channel.maximum_value)
346 if volt_range == 0.0:
347 volt_range = 10.0
348 except (ValueError, TypeError):
349 volt_range = 10.0
350 try:
351 mv_per_eu = float(channel.sensitivity)
352 if mv_per_eu == 0.0:
353 mv_per_eu = 1000.0
354 except (ValueError, TypeError):
355 mv_per_eu = 1000.0
356 v_per_eu = mv_per_eu / 1000.0
357 trigger_level_v = self.trigger_level * volt_range
358 trigger_level_eu = trigger_level_v / v_per_eu
359 hysterisis_level_v = self.hysteresis_level * volt_range
360 hysterisis_level_eu = hysterisis_level_v / v_per_eu
361 return (
362 trigger_level_v,
363 trigger_level_eu,
364 hysterisis_level_v,
365 hysterisis_level_eu,
366 )
368 @property
369 def disabled_signals(self):
370 """Returns a list of indices corresponding to output signals that have been disabled"""
371 return [
372 i
373 for i, index in enumerate(self.output_channel_indices)
374 if not (
375 index in self.response_channel_indices or index in self.reference_channel_indices
376 )
377 ]
379 @property
380 def hysteresis_samples(self):
381 """Property returning the number of samples that a signal must be below the hysterisis
382 level"""
383 return int(self.hysteresis_length * self.samples_per_frame)
385 def store_to_netcdf(
386 self, netcdf_group_handle: nc4._netCDF4.Group # pylint: disable=c-extension-no-member
387 ):
388 """Store parameters to a group in a netCDF streaming file.
390 This function stores parameters from the environment into the netCDF
391 file in a group with the environment's name as its name. The function
392 will receive a reference to the group within the dataset and should
393 store the environment's parameters into that group in the form of
394 attributes, dimensions, or variables.
396 This function is the "write" counterpart to the retrieve_metadata
397 function in the AbstractUI class, which will read parameters from
398 the netCDF file to populate the parameters in the user interface.
400 Parameters
401 ----------
402 netcdf_group_handle : nc4._netCDF4.Group
403 A reference to the Group within the netCDF dataset where the
404 environment's metadata is stored.
406 """
407 netcdf_group_handle.samples_per_frame = self.samples_per_frame
408 netcdf_group_handle.averaging_type = self.averaging_type
409 netcdf_group_handle.num_averages = self.num_averages
410 netcdf_group_handle.averaging_coefficient = self.averaging_coefficient
411 netcdf_group_handle.frf_technique = self.frf_technique
412 netcdf_group_handle.frf_window = self.frf_window
413 netcdf_group_handle.overlap = self.overlap
414 netcdf_group_handle.trigger_type = self.trigger_type
415 netcdf_group_handle.accept_type = self.accept_type
416 netcdf_group_handle.wait_for_steady_state = self.wait_for_steady_state
417 netcdf_group_handle.trigger_channel = self.trigger_channel
418 netcdf_group_handle.pretrigger = self.pretrigger
419 netcdf_group_handle.trigger_slope_positive = 1 if self.trigger_slope_positive else 0
420 netcdf_group_handle.trigger_level = self.trigger_level
421 netcdf_group_handle.hysteresis_level = self.hysteresis_level
422 netcdf_group_handle.hysteresis_length = self.hysteresis_length
423 netcdf_group_handle.signal_generator_type = self.signal_generator_type
424 netcdf_group_handle.signal_generator_level = self.signal_generator_level
425 netcdf_group_handle.signal_generator_min_frequency = self.signal_generator_min_frequency
426 netcdf_group_handle.signal_generator_max_frequency = self.signal_generator_max_frequency
427 netcdf_group_handle.signal_generator_on_fraction = self.signal_generator_on_fraction
428 netcdf_group_handle.exponential_window_value_at_frame_end = (
429 self.exponential_window_value_at_frame_end
430 )
431 netcdf_group_handle.acceptance_function = (
432 self.acceptance_function[0] + ":" + self.acceptance_function[1]
433 if self.acceptance_function is not None
434 else "None"
435 )
436 # Reference channels
437 netcdf_group_handle.createDimension(
438 "reference_channels", len(self.reference_channel_indices)
439 )
440 var = netcdf_group_handle.createVariable(
441 "reference_channel_indices", "i4", ("reference_channels")
442 )
443 var[...] = self.reference_channel_indices
444 # Response channels
445 netcdf_group_handle.createDimension("response_channels", len(self.response_channel_indices))
446 var = netcdf_group_handle.createVariable(
447 "response_channel_indices", "i4", ("response_channels")
448 )
449 var[...] = self.response_channel_indices
451 @classmethod
452 def from_ui(cls, ui):
453 """
454 Creates a ModalMetadata object from the user interface
456 Parameters
457 ----------
458 ui : ModalUI
459 A Modal User Interface.
461 Returns
462 -------
463 test_parameters : ModalMetadata
464 Parameters corresponding to the data in the user interface
466 """
467 signal_generator_level = 0
468 signal_generator_min_frequency = 0
469 signal_generator_max_frequency = 0
470 signal_generator_on_percent = 0
471 if ui.definition_widget.signal_generator_selector.currentIndex() == 0: # None
472 signal_generator_type = "none"
473 elif ui.definition_widget.signal_generator_selector.currentIndex() == 1: # Random
474 signal_generator_type = "random"
475 signal_generator_level = ui.definition_widget.random_rms_selector.value()
476 signal_generator_min_frequency = (
477 ui.definition_widget.random_min_frequency_selector.value()
478 )
479 signal_generator_max_frequency = (
480 ui.definition_widget.random_max_frequency_selector.value()
481 )
482 elif ui.definition_widget.signal_generator_selector.currentIndex() == 2: # Burst Random
483 signal_generator_type = "burst"
484 signal_generator_level = ui.definition_widget.burst_rms_selector.value()
485 signal_generator_min_frequency = (
486 ui.definition_widget.burst_min_frequency_selector.value()
487 )
488 signal_generator_max_frequency = (
489 ui.definition_widget.burst_max_frequency_selector.value()
490 )
491 signal_generator_on_percent = ui.definition_widget.burst_on_percentage_selector.value()
492 elif ui.definition_widget.signal_generator_selector.currentIndex() == 3: # Pseudorandom
493 signal_generator_type = "pseudorandom"
494 signal_generator_level = ui.definition_widget.pseudorandom_rms_selector.value()
495 signal_generator_min_frequency = (
496 ui.definition_widget.pseudorandom_min_frequency_selector.value()
497 )
498 signal_generator_max_frequency = (
499 ui.definition_widget.pseudorandom_max_frequency_selector.value()
500 )
501 elif ui.definition_widget.signal_generator_selector.currentIndex() == 4: # Chirp
502 signal_generator_type = "chirp"
503 signal_generator_level = ui.definition_widget.chirp_level_selector.value()
504 signal_generator_min_frequency = (
505 ui.definition_widget.chirp_min_frequency_selector.value()
506 )
507 signal_generator_max_frequency = (
508 ui.definition_widget.chirp_max_frequency_selector.value()
509 )
510 elif ui.definition_widget.signal_generator_selector.currentIndex() == 5: # Square
511 signal_generator_type = "square"
512 signal_generator_level = ui.definition_widget.square_level_selector.value()
513 signal_generator_min_frequency = ui.definition_widget.square_frequency_selector.value()
514 signal_generator_on_percent = ui.definition_widget.square_percent_on_selector.value()
515 elif ui.definition_widget.signal_generator_selector.currentIndex() == 6: # Sine
516 signal_generator_type = "sine"
517 signal_generator_level = ui.definition_widget.sine_level_selector.value()
518 signal_generator_min_frequency = ui.definition_widget.sine_frequency_selector.value()
519 else:
520 index = ui.definition_widget.signal_generator_selector.currentIndex()
521 raise ValueError(f"Invalid Signal Generator {index} (How did you get here?)")
522 return cls(
523 ui.definition_widget.sample_rate_display.value(),
524 ui.definition_widget.samples_per_frame_selector.value(),
525 ui.definition_widget.system_id_averaging_scheme_selector.itemText(
526 ui.definition_widget.system_id_averaging_scheme_selector.currentIndex()
527 ),
528 ui.definition_widget.system_id_frames_to_average_selector.value(),
529 ui.definition_widget.system_id_averaging_coefficient_selector.value(),
530 ui.definition_widget.system_id_frf_technique_selector.itemText(
531 ui.definition_widget.system_id_frf_technique_selector.currentIndex()
532 ),
533 ui.definition_widget.system_id_transfer_function_computation_window_selector.itemText(
534 ui.definition_widget.system_id_transfer_function_computation_window_selector.currentIndex()
535 ).lower(),
536 ui.definition_widget.system_id_overlap_percentage_selector.value(),
537 ui.definition_widget.triggering_type_selector.itemText(
538 ui.definition_widget.triggering_type_selector.currentIndex()
539 ),
540 ui.definition_widget.acceptance_selector.itemText(
541 ui.definition_widget.acceptance_selector.currentIndex()
542 ),
543 ui.definition_widget.wait_for_steady_selector.value(),
544 ui.definition_widget.trigger_channel_selector.currentIndex(),
545 ui.definition_widget.pretrigger_selector.value(),
546 ui.definition_widget.trigger_slope_selector.currentIndex() == 0,
547 ui.definition_widget.trigger_level_selector.value(),
548 ui.definition_widget.hysteresis_selector.value(),
549 ui.definition_widget.hysteresis_length_selector.value(),
550 signal_generator_type,
551 signal_generator_level,
552 signal_generator_min_frequency,
553 signal_generator_max_frequency,
554 signal_generator_on_percent,
555 ui.acceptance_function,
556 ui.reference_indices,
557 ui.response_indices,
558 ui.all_output_channel_indices,
559 ui.data_acquisition_parameters,
560 ui.definition_widget.window_value_selector.value() / 100,
561 )
563 def generate_signal(self):
564 """Generates a single frame of data"""
565 if self.signal_generator is None:
566 return np.zeros(
567 (
568 len(self.output_channel_indices),
569 self.samples_per_frame * self.output_oversample,
570 )
571 )
572 else:
573 return self.signal_generator.generate_frame()[0]
576from .data_collector import ( # noqa # pylint: disable=wrong-import-position
577 Acceptance,
578 AcquisitionType,
579 CollectorMetadata,
580 DataCollectorCommands,
581 TriggerSlope,
582 Window,
583 data_collector_process,
584)
585from .signal_generation_process import ( # noqa # pylint: disable=wrong-import-position
586 SignalGenerationCommands,
587 SignalGenerationMetadata,
588 signal_generation_process,
589)
590from .spectral_processing import ( # noqa # pylint: disable=wrong-import-position
591 AveragingTypes,
592 Estimator,
593 SpectralProcessingCommands,
594 SpectralProcessingMetadata,
595 spectral_processing_process,
596)
599class ModalUI(AbstractUI):
600 """Modal User Interface class defining the interface with the controller
602 This class is used to define the interface between the User Interface of the
603 Modal environment in the controller and the main controller."""
605 def __init__(
606 self,
607 environment_name: str,
608 definition_tabwidget: QtWidgets.QTabWidget,
609 system_id_tabwidget: QtWidgets.QTabWidget, # pylint: disable=unused-argument
610 test_predictions_tabwidget: QtWidgets.QTabWidget, # pylint: disable=unused-argument
611 run_tabwidget: QtWidgets.QTabWidget,
612 environment_command_queue: VerboseMessageQueue,
613 controller_communication_queue: VerboseMessageQueue,
614 log_file_queue: Queue,
615 ):
616 """
617 Constructs a Modal User Interface
619 Given the tab widgets from the main interface as well as communication
620 queues, this class assembles the user interface components specific to
621 the Modal Environment
623 Parameters
624 ----------
625 definition_tabwidget : QtWidgets.QTabWidget
626 QTabWidget containing the environment subtabs on the Control
627 Definition main tab
628 system_id_tabwidget : QtWidgets.QTabWidget
629 QTabWidget containing the environment subtabs on the System
630 Identification main tab
631 test_predictions_tabwidget : QtWidgets.QTabWidget
632 QTabWidget containing the environment subtabs on the Test Predictions
633 main tab
634 run_tabwidget : QtWidgets.QTabWidget
635 QTabWidget containing the environment subtabs on the Run
636 main tab.
637 environment_command_queue : VerboseMessageQueue
638 Queue for sending commands to the Modal Environment
639 controller_communication_queue : VerboseMessageQueue
640 Queue for sending global commands to the controller
641 log_file_queue : Queue
642 Queue where log file messages can be written.
644 """
645 super().__init__(
646 environment_name,
647 environment_command_queue,
648 controller_communication_queue,
649 log_file_queue,
650 )
651 # Add the page to the control definition tabwidget
652 self.definition_widget = QtWidgets.QWidget()
653 uic.loadUi(environment_definition_ui_paths[CONTROL_TYPE], self.definition_widget)
654 definition_tabwidget.addTab(self.definition_widget, self.environment_name)
655 # Add the page to the run tabwidget
656 self.run_widget = QtWidgets.QWidget()
657 uic.loadUi(environment_run_ui_paths[CONTROL_TYPE], self.run_widget)
658 run_tabwidget.addTab(self.run_widget, self.environment_name)
660 self.trigger_widgets = [
661 self.definition_widget.trigger_channel_selector,
662 self.definition_widget.pretrigger_selector,
663 self.definition_widget.trigger_slope_selector,
664 self.definition_widget.trigger_level_selector,
665 self.definition_widget.trigger_level_voltage_display,
666 self.definition_widget.trigger_level_eu_display,
667 self.definition_widget.hysteresis_selector,
668 self.definition_widget.hysteresis_voltage_display,
669 self.definition_widget.hysteresis_eu_display,
670 self.definition_widget.hysteresis_length_selector,
671 self.definition_widget.hysteresis_samples_display,
672 self.definition_widget.hysteresis_time_display,
673 ]
675 self.signal_generator_widgets = [
676 self.definition_widget.random_rms_selector,
677 self.definition_widget.random_min_frequency_selector,
678 self.definition_widget.random_max_frequency_selector,
679 self.definition_widget.burst_rms_selector,
680 self.definition_widget.burst_min_frequency_selector,
681 self.definition_widget.burst_max_frequency_selector,
682 self.definition_widget.burst_on_percentage_selector,
683 self.definition_widget.pseudorandom_rms_selector,
684 self.definition_widget.pseudorandom_min_frequency_selector,
685 self.definition_widget.pseudorandom_max_frequency_selector,
686 self.definition_widget.chirp_level_selector,
687 self.definition_widget.chirp_min_frequency_selector,
688 self.definition_widget.chirp_max_frequency_selector,
689 self.definition_widget.square_level_selector,
690 self.definition_widget.square_frequency_selector,
691 self.definition_widget.square_percent_on_selector,
692 self.definition_widget.sine_level_selector,
693 self.definition_widget.sine_frequency_selector,
694 ]
696 self.window_parameter_widgets = [
697 self.definition_widget.window_value_label,
698 self.definition_widget.window_value_selector,
699 ]
701 self.definition_widget.reference_channels_selector.setColumnCount(3)
702 self.definition_widget.reference_channels_selector.setVerticalHeaderLabels(
703 ["Enabled", "Reference", "Channel"]
704 )
706 self.data_acquisition_parameters = None
707 self.environment_parameters = None
708 self.channel_names = None
709 self.acceptance_function = None
710 self.plot_data_items = {}
711 self.reference_channel_indices = None
712 self.all_output_channel_indices = None
713 self.response_channel_indices = None
714 self.last_frame = None
715 self.last_frf = None
716 self.last_coherence = None
717 self.last_response_cpsd = None
718 self.last_reference_cpsd = None
719 self.last_condition = None
720 self.acquiring = False
721 self.netcdf_handle = None
722 self.override_table = {}
723 self.reciprocal_responses = []
725 # Store some information into the channel display so the plots have
726 # access to it
727 self.run_widget.channel_display_area.time_abscissa = None
728 self.run_widget.channel_display_area.frequency_abscissa = None
729 self.run_widget.channel_display_area.window_function = None
730 self.run_widget.channel_display_area.last_frame = None
731 self.run_widget.channel_display_area.last_spectrum = None
732 self.run_widget.channel_display_area.last_autospectrum = None
733 self.run_widget.channel_display_area.last_frf = None
734 self.run_widget.channel_display_area.last_coh = None
735 self.run_widget.channel_display_area.channel_names = None
736 self.run_widget.channel_display_area.reference_channel_indices = None
737 self.run_widget.channel_display_area.response_channel_indices = None
739 self.complete_ui()
740 self.connect_callbacks()
742 @property
743 def reference_indices(self):
744 """Returns indices corresponding to the reference channels"""
745 return [
746 i
747 for i in range(self.definition_widget.reference_channels_selector.rowCount())
748 if self.definition_widget.reference_channels_selector.cellWidget(i, 0).isChecked()
749 and self.definition_widget.reference_channels_selector.cellWidget(i, 1).isChecked()
750 ]
752 @property
753 def response_indices(self):
754 """Returns indices corresponding to the response channels in a test"""
755 return [
756 i
757 for i in range(self.definition_widget.reference_channels_selector.rowCount())
758 if self.definition_widget.reference_channels_selector.cellWidget(i, 0).isChecked()
759 and not self.definition_widget.reference_channels_selector.cellWidget(i, 1).isChecked()
760 ]
762 @property
763 def output_channel_indices(self):
764 """Returns indices corresponding to the output channels in a test"""
765 return [
766 i
767 for i in self.all_output_channel_indices
768 if self.definition_widget.reference_channels_selector.cellWidget(i, 0).isChecked()
769 ]
771 @property
772 def initialized_response_names(self):
773 """Returns channel names corresponding to the initialized response channels"""
774 return [
775 self.channel_names[i]
776 for i in range(len(self.channel_names))
777 if i not in self.environment_parameters.response_channel_indices
778 ]
780 @property
781 def initialized_reference_names(self):
782 """Returns channel names corresponding to the initialized reference channels"""
783 return [
784 self.channel_names[i] for i in self.environment_parameters.reference_channel_indices
785 ]
787 def complete_ui(self):
788 """Applies some finishing touches to the UI"""
789 self.definition_widget.system_id_averaging_coefficient_selector.setEnabled(False)
790 for widget in self.trigger_widgets:
791 widget.setEnabled(False)
793 # Set common look and feel for plots
794 plot_widgets = [self.definition_widget.output_signal_plot]
795 for plot_widget in plot_widgets:
796 plot_item = plot_widget.getPlotItem()
797 plot_item.showGrid(True, True, 0.25)
798 plot_item.enableAutoRange()
799 plot_item.getViewBox().enableAutoRange(enable=True)
801 # Disable the currently inactive portions of the definition layout
802 self.definition_widget.system_id_averaging_coefficient_selector.setEnabled(False)
803 for widget in self.window_parameter_widgets:
804 widget.hide()
806 def connect_callbacks(self):
807 """Connects callback functions to the user interface widgets"""
808 # Definition Callbacks
809 self.definition_widget.samples_per_frame_selector.valueChanged.connect(
810 self.update_parameters
811 )
812 self.definition_widget.system_id_overlap_percentage_selector.valueChanged.connect(
813 self.update_parameters
814 )
815 self.definition_widget.triggering_type_selector.currentIndexChanged.connect(
816 self.activate_trigger_options
817 )
818 self.definition_widget.acceptance_selector.currentIndexChanged.connect(
819 self.select_acceptance
820 )
821 self.definition_widget.trigger_channel_selector.currentIndexChanged.connect(
822 self.update_trigger_levels
823 )
824 self.definition_widget.trigger_level_selector.valueChanged.connect(
825 self.update_trigger_levels
826 )
827 self.definition_widget.hysteresis_selector.valueChanged.connect(self.update_trigger_levels)
828 self.definition_widget.regenerate_signal_button.clicked.connect(self.generate_signal)
829 self.definition_widget.signal_generator_selector.currentChanged.connect(self.update_signal)
830 for widget in self.signal_generator_widgets:
831 widget.valueChanged.connect(self.update_signal)
832 self.definition_widget.check_selected_button.clicked.connect(
833 self.check_selected_reference_channels
834 )
835 self.definition_widget.uncheck_selected_button.clicked.connect(
836 self.uncheck_selected_reference_channels
837 )
838 self.definition_widget.enable_selected_button.clicked.connect(self.enable_selected_channels)
839 self.definition_widget.disable_selected_button.clicked.connect(
840 self.disable_selected_channels
841 )
842 self.definition_widget.hysteresis_length_selector.valueChanged.connect(
843 self.update_hysteresis_length
844 )
845 self.definition_widget.system_id_averaging_scheme_selector.currentIndexChanged.connect(
846 self.update_averaging_type
847 )
848 self.definition_widget.system_id_transfer_function_computation_window_selector.currentIndexChanged.connect(
849 self.update_window
850 )
851 # Run Callbacks
852 self.run_widget.preview_test_button.clicked.connect(self.preview_acquisition)
853 self.run_widget.start_test_button.clicked.connect(self.start_control)
854 self.run_widget.stop_test_button.clicked.connect(self.stop_control)
855 self.run_widget.select_file_button.clicked.connect(self.select_file)
856 self.run_widget.accept_average_button.clicked.connect(self.accept_frame)
857 self.run_widget.reject_average_button.clicked.connect(self.reject_frame)
858 self.run_widget.new_window_button.clicked.connect(self.new_window)
859 self.run_widget.new_from_template_combobox.currentIndexChanged.connect(
860 self.new_window_from_template
861 )
862 self.run_widget.tile_layout_button.clicked.connect(
863 self.run_widget.channel_display_area.tileSubWindows
864 )
865 self.run_widget.close_all_button.clicked.connect(self.close_windows)
866 self.run_widget.decrement_channels_button.clicked.connect(self.decrement_channels)
867 self.run_widget.increment_channels_button.clicked.connect(self.increment_channels)
868 self.run_widget.dof_override_table.itemChanged.connect(self.update_override_table)
869 self.run_widget.add_override_button.clicked.connect(self.add_override_channel)
870 self.run_widget.remove_override_button.clicked.connect(self.remove_override_channel)
872 # Definition Callbacks
873 def update_parameters(self):
874 """Updates widget values when fundamental signal processing parameters change"""
875 if self.definition_widget.samples_per_frame_selector.value() % 2 == 1:
876 self.definition_widget.samples_per_frame_selector.blockSignals(True)
877 self.definition_widget.samples_per_frame_selector.setValue(
878 self.definition_widget.samples_per_frame_selector.value() + 1
879 )
880 self.definition_widget.samples_per_frame_selector.blockSignals(False)
881 data = self.collect_environment_definition_parameters()
882 self.definition_widget.samples_per_acquire_display.setValue(data.samples_per_acquire)
883 self.definition_widget.frame_time_display.setValue(data.frame_time)
884 self.definition_widget.nyquist_frequency_display.setValue(data.nyquist_frequency)
885 self.definition_widget.fft_lines_display.setValue(data.fft_lines)
886 self.definition_widget.frequency_spacing_display.setValue(data.frequency_spacing)
887 if self.definition_widget.regenerate_signal_auto_checkbox.isChecked():
888 self.generate_signal()
890 def update_reference_channels(self):
891 """Updates widgets based on changes in the selected reference channels"""
892 self.definition_widget.response_channels_display.setValue(len(self.response_indices))
893 self.definition_widget.reference_channels_display.setValue(len(self.reference_indices))
894 self.definition_widget.output_channels_display.setValue(len(self.output_channel_indices))
895 if self.definition_widget.regenerate_signal_auto_checkbox.isChecked():
896 self.generate_signal()
898 def check_selected_reference_channels(self):
899 """Checks reference channels that are selected in the list widget"""
900 select = self.definition_widget.reference_channels_selector.selectionModel()
901 rows = select.selectedRows()
902 for row in rows:
903 index = row.row()
904 self.definition_widget.reference_channels_selector.cellWidget(index, 1).setChecked(True)
906 def uncheck_selected_reference_channels(self):
907 """Unchecks reference channels that are selected in the list widget"""
908 select = self.definition_widget.reference_channels_selector.selectionModel()
909 rows = select.selectedRows()
910 for row in rows:
911 index = row.row()
912 self.definition_widget.reference_channels_selector.cellWidget(index, 1).setChecked(
913 False
914 )
916 def enable_selected_channels(self):
917 """Enables channels that are selected in the list widget"""
918 select = self.definition_widget.reference_channels_selector.selectionModel()
919 rows = select.selectedRows()
920 for row in rows:
921 index = row.row()
922 self.definition_widget.reference_channels_selector.cellWidget(index, 0).setChecked(True)
924 def disable_selected_channels(self):
925 """Disables channels that are selected in the list widget"""
926 select = self.definition_widget.reference_channels_selector.selectionModel()
927 rows = select.selectedRows()
928 for row in rows:
929 index = row.row()
930 self.definition_widget.reference_channels_selector.cellWidget(index, 0).setChecked(
931 False
932 )
934 def activate_trigger_options(self):
935 """Enables widgets corresponding to the trigger selection"""
936 if self.definition_widget.triggering_type_selector.currentIndex() == 0:
937 for widget in self.trigger_widgets:
938 widget.setEnabled(False)
939 else:
940 for widget in self.trigger_widgets:
941 widget.setEnabled(True)
943 def select_acceptance(self):
944 """Selects the acceptance type and opens up a file dialog if necessary"""
945 if self.definition_widget.acceptance_selector.currentIndex() == 2:
946 # Open up a file dialog
947 filename, _ = QtWidgets.QFileDialog.getOpenFileName(
948 self.definition_widget,
949 "Select Python Module",
950 filter="Python Modules (*.py)",
951 )
952 if filename == "":
953 self.definition_widget.acceptance_selector.setCurrentIndex(0)
954 return
955 module = load_python_module(filename)
956 functions = [
957 function
958 for function in inspect.getmembers(module)
959 if inspect.isfunction(function[1])
960 ]
961 item, ok_pressed = QtWidgets.QInputDialog.getItem(
962 self.definition_widget,
963 "Select Acceptance Function",
964 "Function Name:",
965 [function[0] for function in functions],
966 0,
967 False,
968 )
969 if ok_pressed:
970 self.acceptance_function = [filename, item]
971 else:
972 self.definition_widget.acceptance_selector.setCurrentIndex(0)
973 return
974 else:
975 self.acceptance_function = None
977 def update_trigger_levels(self):
978 """Updates trigger levels based on selected widget values"""
979 data = self.collect_environment_definition_parameters()
980 t_v, t_eu, h_v, h_eu = data.get_trigger_levels(
981 self.data_acquisition_parameters.channel_list
982 )
983 self.definition_widget.trigger_level_voltage_display.setValue(t_v)
984 self.definition_widget.trigger_level_eu_display.setValue(t_eu)
985 self.definition_widget.hysteresis_voltage_display.setValue(h_v)
986 self.definition_widget.hysteresis_eu_display.setValue(h_eu)
987 eu_suffix = self.data_acquisition_parameters.channel_list[data.trigger_channel].unit
988 self.definition_widget.hysteresis_eu_display.setSuffix(
989 (" " + eu_suffix) if not (eu_suffix == "" or eu_suffix is None) else ""
990 )
991 self.definition_widget.trigger_level_eu_display.setSuffix(
992 (" " + eu_suffix) if not (eu_suffix == "" or eu_suffix is None) else ""
993 )
995 def update_hysteresis_length(self):
996 """Updates hysterisis length based on the selected trigger parameters"""
997 data = self.collect_environment_definition_parameters()
998 self.definition_widget.hysteresis_samples_display.setValue(data.hysteresis_samples)
999 self.definition_widget.hysteresis_time_display.setValue(
1000 data.hysteresis_samples / data.sample_rate
1001 )
1003 def update_signal(self):
1004 """Updates the generated signal based on widget value changes"""
1005 if self.definition_widget.regenerate_signal_auto_checkbox.isChecked():
1006 self.generate_signal()
1008 def generate_signal(self):
1009 """Generates an example signal to show in the definition widget"""
1010 if self.data_acquisition_parameters is None:
1011 return
1012 output_oversample = self.data_acquisition_parameters.output_oversample
1013 output_rate = self.data_acquisition_parameters.output_sample_rate
1014 data = self.collect_environment_definition_parameters()
1015 frame_output_samples = int(data.samples_per_frame * output_oversample)
1016 signal = data.generate_signal()
1017 # Reduce down to just one frame
1018 while signal.shape[-1] < frame_output_samples:
1019 signal = np.concatenate((signal, data.generate_signal()), axis=-1)
1020 signal = signal[..., :frame_output_samples]
1021 signal[data.disabled_signals] = 0
1022 times = np.arange(frame_output_samples) / output_rate
1023 for s, plot in zip(signal, self.plot_data_items["signal_representation"]):
1024 plot.setData(times, s)
1026 def update_averaging_type(self):
1027 """Enables exponential averaging coefficient widgets if exponential averaging is chosen"""
1028 if self.definition_widget.system_id_averaging_scheme_selector.currentIndex() == 0:
1029 self.definition_widget.system_id_averaging_coefficient_selector.setEnabled(False)
1030 else:
1031 self.definition_widget.system_id_averaging_coefficient_selector.setEnabled(True)
1033 def update_window(self):
1034 """Shows additional window function options based on the selected window"""
1035 if (
1036 self.definition_widget.system_id_transfer_function_computation_window_selector.currentIndex()
1037 == 2
1038 ):
1039 for widget in self.window_parameter_widgets:
1040 widget.show()
1041 else:
1042 for widget in self.window_parameter_widgets:
1043 widget.hide()
1045 # Run Callbacks
1046 def preview_acquisition(self):
1047 """Tells the environment process to start in preview mode"""
1048 self.run_widget.stop_test_button.setEnabled(True)
1049 self.run_widget.preview_test_button.setEnabled(False)
1050 self.run_widget.start_test_button.setEnabled(False)
1051 self.run_widget.select_file_button.setEnabled(False)
1052 self.controller_communication_queue.put(
1053 self.log_name, (GlobalCommands.START_ENVIRONMENT, self.environment_name)
1054 )
1055 self.environment_command_queue.put(self.log_name, (ModalCommands.START_CONTROL, None))
1056 self.run_widget.dof_override_table.setEnabled(False)
1057 self.run_widget.add_override_button.setEnabled(False)
1058 self.run_widget.remove_override_button.setEnabled(False)
1060 def start_control(self):
1061 """Tells the environment process to start in acquisition mode"""
1062 self.acquiring = True
1063 # Create the output file
1064 filename = self.run_widget.data_file_selector.text()
1065 if filename == "":
1066 error_message_qt("Invalid File", "Please select a file in which to store modal data")
1067 return
1068 if self.run_widget.autoincrement_checkbox.isChecked():
1069 # Add the file increment
1070 path, ext = os.path.splitext(filename)
1071 index = len(glob(path + "*" + ext))
1072 filename = path + f"_{index:04d}" + ext
1073 self.create_netcdf_file(filename)
1074 self.preview_acquisition()
1076 def stop_control(self):
1077 """Tells the environment process to stop the current measurement"""
1078 self.environment_command_queue.put(self.log_name, (ModalCommands.STOP_CONTROL, None))
1080 def select_file(self):
1081 """Brings up a file dialog box to select the save file location"""
1082 filename, _ = QtWidgets.QFileDialog.getSaveFileName(
1083 self.run_widget,
1084 "Select NetCDF File to Save Modal Data",
1085 filter="NetCDF File (*.nc4)",
1086 )
1087 if filename == "":
1088 return
1089 self.run_widget.data_file_selector.setText(filename)
1091 def accept_frame(self):
1092 """Sends a signal to the environment process to accept the current measurement frame"""
1093 self.environment_command_queue.put(self.log_name, (ModalCommands.ACCEPT_FRAME, True))
1094 self.run_widget.accept_average_button.setEnabled(False)
1095 self.run_widget.reject_average_button.setEnabled(False)
1097 def reject_frame(self):
1098 """Sends a signal to the environment process to reject the current measurement frame"""
1099 self.environment_command_queue.put(self.log_name, (ModalCommands.ACCEPT_FRAME, False))
1100 self.run_widget.accept_average_button.setEnabled(False)
1101 self.run_widget.reject_average_button.setEnabled(False)
1103 def new_window(self):
1104 """Creates a new window to display modal data"""
1105 widget = ModalMDISubWindow(self.run_widget.channel_display_area)
1106 self.run_widget.channel_display_area.addSubWindow(widget)
1107 widget.show()
1108 return widget
1109 # print('Windows: {:}'.format(self.run_widget.channel_display_area.subWindowList()))
1111 def new_window_from_template(self):
1112 """Creates a new window from the template functions"""
1113 if self.run_widget.new_from_template_combobox.currentIndex() == 0:
1114 return
1115 elif self.run_widget.new_from_template_combobox.currentIndex() == 6:
1116 # 3x3 channel grid
1117 for i in range(9):
1118 widget = self.new_window()
1119 widget.signal_selector.setCurrentIndex(0)
1120 widget.response_coordinate_selector.setCurrentIndex(i)
1121 widget.lock_response_checkbox.setChecked(False)
1122 elif self.run_widget.new_from_template_combobox.currentIndex() == 5:
1123 # Reference autospectra
1124 for index in self.run_widget.channel_display_area.reference_channel_indices:
1125 widget = self.new_window()
1126 widget.signal_selector.setCurrentIndex(3)
1127 widget.response_coordinate_selector.setCurrentIndex(index)
1128 widget.lock_response_checkbox.setChecked(True)
1129 else:
1130 corresponding_drive_responses = (
1131 self.run_widget.channel_display_area.reciprocal_responses
1132 )
1133 if self.run_widget.new_from_template_combobox.currentIndex() == 1:
1134 # Create drive point FRFs in magnitude
1135 for i, index in enumerate(corresponding_drive_responses):
1136 widget = self.new_window()
1137 widget.signal_selector.setCurrentIndex(4)
1138 widget.response_coordinate_selector.setCurrentIndex(index)
1139 widget.reference_coordinate_selector.setCurrentIndex(i)
1140 widget.data_type_selector.setCurrentIndex(0)
1141 widget.lock_response_checkbox.setChecked(True)
1142 elif self.run_widget.new_from_template_combobox.currentIndex() == 2:
1143 # Create drive point FRFs in imaginary
1144 for i, index in enumerate(corresponding_drive_responses):
1145 widget = self.new_window()
1146 widget.signal_selector.setCurrentIndex(4)
1147 widget.response_coordinate_selector.setCurrentIndex(index)
1148 widget.reference_coordinate_selector.setCurrentIndex(i)
1149 widget.data_type_selector.setCurrentIndex(3)
1150 widget.lock_response_checkbox.setChecked(True)
1151 elif self.run_widget.new_from_template_combobox.currentIndex() == 3:
1152 # Create drive point Coherence
1153 for i, index in enumerate(corresponding_drive_responses):
1154 widget = self.new_window()
1155 widget.signal_selector.setCurrentIndex(6)
1156 widget.response_coordinate_selector.setCurrentIndex(index)
1157 widget.reference_coordinate_selector.setCurrentIndex(i)
1158 widget.lock_response_checkbox.setChecked(True)
1159 elif self.run_widget.new_from_template_combobox.currentIndex() == 4:
1160 # Create drive point Coherence
1161 for i, index in enumerate(corresponding_drive_responses):
1162 for j, index in enumerate(corresponding_drive_responses):
1163 if i <= j:
1164 continue
1165 widget = self.new_window()
1166 widget.signal_selector.setCurrentIndex(7)
1167 widget.response_coordinate_selector.setCurrentIndex(i)
1168 widget.reference_coordinate_selector.setCurrentIndex(j)
1169 widget.lock_response_checkbox.setChecked(True)
1170 self.run_widget.new_from_template_combobox.setCurrentIndex(0)
1172 def close_windows(self):
1173 """Closes all existing windows"""
1174 for window in self.run_widget.channel_display_area.subWindowList():
1175 window.close()
1177 def decrement_channels(self):
1178 """Decrements the unlocked window response channels by the specified number of channels"""
1179 number = -self.run_widget.increment_channels_number.value()
1180 for window in self.run_widget.channel_display_area.subWindowList():
1181 window.widget().increment_channel(number)
1183 def increment_channels(self):
1184 """Increments the unlocked window response channels by the specified number of channels"""
1185 number = self.run_widget.increment_channels_number.value()
1186 for window in self.run_widget.channel_display_area.subWindowList():
1187 window.widget().increment_channel(number)
1189 def add_override_channel(self):
1190 """Adds a row to the channel override table"""
1191 selected_row = self.run_widget.dof_override_table.blockSignals(True)
1192 selected_row = self.run_widget.dof_override_table.rowCount()
1193 self.run_widget.dof_override_table.insertRow(selected_row)
1194 channel_combobox = QtWidgets.QComboBox()
1195 for channel_name in self.channel_names:
1196 channel_combobox.addItem(channel_name)
1197 channel_combobox.currentIndexChanged.connect(self.update_override_table)
1198 self.run_widget.dof_override_table.setCellWidget(selected_row, 0, channel_combobox)
1199 data_item = QtWidgets.QTableWidgetItem()
1200 data_item.setText("1")
1201 self.run_widget.dof_override_table.setItem(selected_row, 1, data_item)
1202 data_item = QtWidgets.QTableWidgetItem()
1203 data_item.setText("X+")
1204 self.run_widget.dof_override_table.setItem(selected_row, 2, data_item)
1205 selected_row = self.run_widget.dof_override_table.blockSignals(False)
1206 self.update_override_table()
1208 def remove_override_channel(self):
1209 """Removes a row from the channel override table"""
1210 selected_row = self.run_widget.dof_override_table.currentRow()
1211 if selected_row >= 0:
1212 self.run_widget.dof_override_table.removeRow(selected_row)
1213 self.update_override_table()
1215 def update_override_table(self):
1216 """Updates channel information in the test based on the override table values"""
1217 self.override_table = {}
1218 for row in range(self.run_widget.dof_override_table.rowCount()):
1219 index = self.run_widget.dof_override_table.cellWidget(row, 0).currentIndex()
1220 new_node = self.run_widget.dof_override_table.item(row, 1).text()
1221 new_direction = self.run_widget.dof_override_table.item(row, 2).text()
1222 self.override_table[index] = [new_node, new_direction]
1223 self.update_channel_names()
1224 self.run_widget.channel_display_area.reciprocal_responses = (
1225 self.get_reciprocal_measurements()
1226 )
1227 # Go through and update all the existing windows in the MDI display
1228 for window in self.run_widget.channel_display_area.subWindowList():
1229 widget = window.widget()
1230 current_response = widget.response_coordinate_selector.currentIndex()
1231 current_reference = widget.reference_coordinate_selector.currentIndex()
1232 current_data_type = widget.data_type_selector.currentIndex()
1233 widget.channel_names = self.channel_names
1234 widget.reference_names = [
1235 self.channel_names[i]
1236 for i in self.run_widget.channel_display_area.reference_channel_indices
1237 ]
1238 widget.response_names = [
1239 self.channel_names[i]
1240 for i in self.run_widget.channel_display_area.response_channel_indices
1241 ]
1242 widget.reciprocal_responses = self.run_widget.channel_display_area.reciprocal_responses
1243 widget.update_ui()
1244 widget.response_coordinate_selector.setCurrentIndex(current_response)
1245 widget.reference_coordinate_selector.setCurrentIndex(current_reference)
1246 widget.data_type_selector.setCurrentIndex(current_data_type)
1248 def get_reciprocal_measurements(self):
1249 """Finds all reciprocal measurements in the test"""
1250 node_numbers = np.array(
1251 [
1252 (channel.node_number if i not in self.override_table else self.override_table[i][0])
1253 for i, channel in enumerate(self.data_acquisition_parameters.channel_list)
1254 ]
1255 )
1256 node_directions = np.array(
1257 [
1258 (
1259 ""
1260 if channel.node_direction is None
1261 else "".join(
1262 [
1263 char
1264 for char in (
1265 channel.node_direction
1266 if i not in self.override_table
1267 else self.override_table[i][1]
1268 )
1269 if char not in "+-"
1270 ]
1271 )
1272 )
1273 for i, channel in enumerate(self.data_acquisition_parameters.channel_list)
1274 ]
1275 )
1276 reference_node_numbers = node_numbers[self.environment_parameters.reference_channel_indices]
1277 reference_node_directions = node_directions[
1278 self.environment_parameters.reference_channel_indices
1279 ]
1280 response_node_numbers = node_numbers[self.environment_parameters.response_channel_indices]
1281 response_node_directions = node_directions[
1282 self.environment_parameters.response_channel_indices
1283 ]
1284 corresponding_drive_responses = []
1285 for node, direction in zip(reference_node_numbers, reference_node_directions):
1286 # print('Node: {:} Direction: {:}'.format(node,direction))
1287 # print('Response Node Numbers:')
1288 # print(response_node_numbers)
1289 # print('Response Node Directions:')
1290 # print(response_node_directions)
1291 # print('Node Match:')
1292 # print(response_node_numbers == node)
1293 # print('Direction Match:')
1294 # print(response_node_directions == direction)
1295 index = np.where(
1296 (response_node_numbers == node) & (response_node_directions == direction)
1297 )[0]
1298 # print('Index:')
1299 # print(index)
1300 if len(index) == 0:
1301 corresponding_drive_responses.append(None)
1302 print(f"Warning: No Drive Point Found for Reference {node}{direction}")
1303 elif len(index) > 1:
1304 corresponding_drive_responses.append(None)
1305 print(f"Warning: Multiple Drive Points Found for Reference {node}{direction}")
1306 else:
1307 corresponding_drive_responses.append(index[0])
1308 # print(corresponding_drive_responses)
1309 return corresponding_drive_responses
1311 def create_netcdf_file(self, filename):
1312 """Creates an output NetCDF4 file to save modal data to
1314 Parameters
1315 ----------
1316 filename : str
1317 The file name to which the netCDF4 file will be stored
1318 """
1319 self.netcdf_handle = nc4.Dataset( # pylint: disable=no-member
1320 filename, "w", format="NETCDF4", clobber=True
1321 )
1322 # Create dimensions
1323 self.netcdf_handle.createDimension(
1324 "response_channels", len(self.data_acquisition_parameters.channel_list)
1325 )
1326 self.netcdf_handle.createDimension(
1327 "output_channels",
1328 len(
1329 [
1330 channel
1331 for channel in self.data_acquisition_parameters.channel_list
1332 if channel.feedback_device is not None
1333 ]
1334 ),
1335 )
1336 self.netcdf_handle.createDimension(
1337 "num_environments", len(self.data_acquisition_parameters.environment_names)
1338 )
1339 self.netcdf_handle.createDimension("time_samples", None)
1340 # Create attributes
1341 self.netcdf_handle.sample_rate = self.data_acquisition_parameters.sample_rate
1342 self.netcdf_handle.time_per_write = (
1343 self.data_acquisition_parameters.samples_per_write
1344 / self.data_acquisition_parameters.output_sample_rate
1345 )
1346 self.netcdf_handle.time_per_read = (
1347 self.data_acquisition_parameters.samples_per_read
1348 / self.data_acquisition_parameters.sample_rate
1349 )
1350 self.netcdf_handle.hardware = self.data_acquisition_parameters.hardware
1351 self.netcdf_handle.hardware_file = (
1352 "None"
1353 if self.data_acquisition_parameters.hardware_file is None
1354 else self.data_acquisition_parameters.hardware_file
1355 )
1356 self.netcdf_handle.output_oversample = self.data_acquisition_parameters.output_oversample
1357 for name, value in self.data_acquisition_parameters.extra_parameters.items():
1358 setattr(self.netcdf_handle, name, value)
1359 # Create Variables
1360 self.netcdf_handle.createVariable("time_data", "f8", ("response_channels", "time_samples"))
1361 var = self.netcdf_handle.createVariable("environment_names", str, ("num_environments",))
1362 this_environment_index = None
1363 for i, name in enumerate(self.data_acquisition_parameters.environment_names):
1364 var[i] = name
1365 if name == self.environment_name:
1366 this_environment_index = i
1367 var = self.netcdf_handle.createVariable(
1368 "environment_active_channels",
1369 "i1",
1370 ("response_channels", "num_environments"),
1371 )
1372 var[...] = self.data_acquisition_parameters.environment_active_channels.astype("int8")[
1373 self.data_acquisition_parameters.environment_active_channels[:, this_environment_index],
1374 :,
1375 ]
1376 # Create channel table variables
1377 labels = [
1378 ["node_number", str],
1379 ["node_direction", str],
1380 ["comment", str],
1381 ["serial_number", str],
1382 ["triax_dof", str],
1383 ["sensitivity", str],
1384 ["unit", str],
1385 ["make", str],
1386 ["model", str],
1387 ["expiration", str],
1388 ["physical_device", str],
1389 ["physical_channel", str],
1390 ["channel_type", str],
1391 ["minimum_value", str],
1392 ["maximum_value", str],
1393 ["coupling", str],
1394 ["excitation_source", str],
1395 ["excitation", str],
1396 ["feedback_device", str],
1397 ["feedback_channel", str],
1398 ["warning_level", str],
1399 ["abort_level", str],
1400 ]
1401 for label, netcdf_datatype in labels:
1402 var = self.netcdf_handle.createVariable(
1403 "/channels/" + label, netcdf_datatype, ("response_channels",)
1404 )
1405 channel_data = [
1406 getattr(channel, label) for channel in self.data_acquisition_parameters.channel_list
1407 ]
1408 if netcdf_datatype == "i1":
1409 channel_data = np.array([1 if val else 0 for val in channel_data])
1410 else:
1411 channel_data = ["" if val is None else val for val in channel_data]
1412 for i, cd in enumerate(channel_data):
1413 if label == "node_number" and i in self.override_table:
1414 var[i] = self.override_table[i][0]
1415 elif label == "node_direction" and i in self.override_table:
1416 var[i] = self.override_table[i][1]
1417 else:
1418 var[i] = cd
1419 group_handle = self.netcdf_handle.createGroup(self.environment_name)
1420 self.environment_parameters.store_to_netcdf(group_handle)
1421 group_handle.createDimension("fft_lines", self.environment_parameters.fft_lines)
1422 group_handle.createVariable(
1423 "frf_data_real",
1424 "f8",
1425 ("fft_lines", "response_channels", "reference_channels"),
1426 )
1427 group_handle.createVariable(
1428 "frf_data_imag",
1429 "f8",
1430 ("fft_lines", "response_channels", "reference_channels"),
1431 )
1432 group_handle.createVariable("coherence", "f8", ("fft_lines", "response_channels"))
1434 def collect_environment_definition_parameters(self) -> AbstractMetadata:
1435 """
1436 Collect the parameters from the user interface defining the environment
1438 Returns
1439 -------
1440 ModalMetadata
1441 A metadata or parameters object containing the parameters defining
1442 the corresponding environment.
1444 """
1445 return ModalMetadata.from_ui(self)
1447 def update_channel_names(self):
1448 """Updates channel names based on the override channel table"""
1449 self.channel_names = []
1450 for i, channel in enumerate(self.data_acquisition_parameters.channel_list):
1451 channel_type_str = "" if channel.channel_type is None else channel.channel_type
1452 node_num_str = (
1453 channel.node_number if i not in self.override_table else self.override_table[i][0]
1454 )
1455 node_dir_str = (
1456 channel.node_direction
1457 if i not in self.override_table
1458 else self.override_table[i][1]
1459 )
1460 self.channel_names.append(
1461 f"{channel_type_str} {node_num_str} {node_dir_str}"[:MAXIMUM_NAME_LENGTH]
1462 )
1463 self.run_widget.channel_display_area.channel_names = self.channel_names
1465 def initialize_data_acquisition(self, data_acquisition_parameters: DataAcquisitionParameters):
1466 """Update the user interface with data acquisition parameters
1468 This function is called when the Data Acquisition parameters are
1469 initialized. This function should set up the environment user interface
1470 accordingly.
1472 Parameters
1473 ----------
1474 data_acquisition_parameters : DataAcquisitionParameters :
1475 Container containing the data acquisition parameters, including
1476 channel table and sampling information.
1478 """
1479 self.data_acquisition_parameters = data_acquisition_parameters
1480 self.definition_widget.sample_rate_display.setValue(data_acquisition_parameters.sample_rate)
1481 self.all_output_channel_indices = [
1482 index
1483 for index, channel in enumerate(self.data_acquisition_parameters.channel_list)
1484 if channel.feedback_device is not None
1485 ]
1486 self.update_channel_names()
1487 self.definition_widget.reference_channels_selector.setRowCount(0)
1488 self.definition_widget.trigger_channel_selector.blockSignals(True)
1489 self.definition_widget.trigger_channel_selector.clear()
1490 for i, channel_name in enumerate(self.channel_names):
1491 self.definition_widget.trigger_channel_selector.addItem(channel_name)
1492 self.definition_widget.reference_channels_selector.insertRow(i)
1493 item = QtWidgets.QTableWidgetItem()
1494 item.setText(channel_name)
1495 item.setFlags(item.flags() & ~Qt.ItemIsEditable)
1496 self.definition_widget.reference_channels_selector.setItem(i, 2, item)
1497 ref_checkbox = QtWidgets.QCheckBox()
1498 ref_checkbox.stateChanged.connect(self.update_reference_channels)
1499 self.definition_widget.reference_channels_selector.setCellWidget(i, 1, ref_checkbox)
1500 enabled_checkbox = QtWidgets.QCheckBox()
1501 enabled_checkbox.setChecked(True)
1502 enabled_checkbox.stateChanged.connect(self.update_reference_channels)
1503 self.definition_widget.reference_channels_selector.setCellWidget(i, 0, enabled_checkbox)
1504 self.definition_widget.trigger_channel_selector.blockSignals(False)
1505 self.update_trigger_levels()
1507 checked_state = self.definition_widget.regenerate_signal_auto_checkbox.isChecked()
1508 self.definition_widget.regenerate_signal_auto_checkbox.setChecked(False)
1509 self.definition_widget.signal_generator_selector.setCurrentIndex(0)
1510 self.definition_widget.samples_per_frame_selector.setValue(
1511 data_acquisition_parameters.sample_rate
1512 )
1513 self.definition_widget.random_max_frequency_selector.setValue(
1514 data_acquisition_parameters.sample_rate / 2
1515 )
1516 self.definition_widget.random_min_frequency_selector.setValue(0)
1517 self.definition_widget.burst_max_frequency_selector.setValue(
1518 data_acquisition_parameters.sample_rate / 2
1519 )
1520 self.definition_widget.burst_min_frequency_selector.setValue(0)
1521 self.definition_widget.chirp_max_frequency_selector.setValue(
1522 data_acquisition_parameters.sample_rate / 2
1523 )
1524 self.definition_widget.chirp_min_frequency_selector.setValue(0)
1525 self.definition_widget.pseudorandom_max_frequency_selector.setValue(
1526 data_acquisition_parameters.sample_rate / 2
1527 )
1528 self.definition_widget.pseudorandom_min_frequency_selector.setValue(0)
1530 self.definition_widget.response_channels_display.setValue(len(self.channel_names))
1531 self.definition_widget.reference_channels_display.setValue(0)
1532 num_outputs = len(self.output_channel_indices)
1533 self.definition_widget.output_channels_display.setValue(num_outputs)
1534 if num_outputs == 0:
1535 for i in range(self.definition_widget.signal_generator_selector.count() - 1):
1536 self.definition_widget.signal_generator_selector.setTabEnabled(i + 1, False)
1538 self.definition_widget.output_signal_plot.getPlotItem().clear()
1539 self.plot_data_items["signal_representation"] = multiline_plotter(
1540 (0, 1),
1541 np.zeros((len(self.all_output_channel_indices), 2)),
1542 widget=self.definition_widget.output_signal_plot,
1543 other_pen_options={"width": 1},
1544 names=[f"Output {i + 1}" for i in range(len(self.all_output_channel_indices))],
1545 )
1546 self.definition_widget.regenerate_signal_auto_checkbox.setChecked(checked_state)
1547 if checked_state:
1548 self.generate_signal()
1550 for widget in [
1551 self.definition_widget.random_min_frequency_selector,
1552 self.definition_widget.random_max_frequency_selector,
1553 self.definition_widget.burst_min_frequency_selector,
1554 self.definition_widget.burst_max_frequency_selector,
1555 self.definition_widget.pseudorandom_min_frequency_selector,
1556 self.definition_widget.pseudorandom_max_frequency_selector,
1557 self.definition_widget.chirp_min_frequency_selector,
1558 self.definition_widget.chirp_max_frequency_selector,
1559 self.definition_widget.square_frequency_selector,
1560 self.definition_widget.sine_frequency_selector,
1561 ]:
1562 widget.setMaximum(self.data_acquisition_parameters.sample_rate / 2)
1564 def initialize_environment(self) -> AbstractMetadata:
1565 """
1566 Update the user interface with environment parameters
1568 This function is called when the Environment parameters are initialized.
1569 This function should set up the user interface accordingly. It must
1570 return the parameters class of the environment that inherits from
1571 AbstractMetadata.
1573 Returns
1574 ModalMetadata
1575 An AbstractMetadata-inheriting object that contains the parameters
1576 defining the environment.
1578 """
1579 self.environment_parameters = self.collect_environment_definition_parameters()
1580 self.reference_channel_indices = self.environment_parameters.reference_channel_indices
1581 self.response_channel_indices = self.environment_parameters.response_channel_indices
1582 self.run_widget.channel_display_area.reference_channel_indices = (
1583 self.reference_channel_indices
1584 )
1585 self.run_widget.channel_display_area.response_channel_indices = (
1586 self.response_channel_indices
1587 )
1588 for window in self.run_widget.channel_display_area.subWindowList():
1589 widget = window.widget()
1590 current_response = widget.response_coordinate_selector.currentIndex()
1591 current_reference = widget.reference_coordinate_selector.currentIndex()
1592 current_data_type = widget.data_type_selector.currentIndex()
1593 widget.reference_names = np.array(
1594 [
1595 widget.channel_names[i]
1596 for i in self.run_widget.channel_display_area.reference_channel_indices
1597 ]
1598 )
1599 widget.response_names = np.array(
1600 [
1601 widget.channel_names[i]
1602 for i in self.run_widget.channel_display_area.response_channel_indices
1603 ]
1604 )
1605 widget.update_ui()
1606 widget.response_coordinate_selector.setCurrentIndex(current_response)
1607 widget.reference_coordinate_selector.setCurrentIndex(current_reference)
1608 widget.data_type_selector.setCurrentIndex(current_data_type)
1609 self.run_widget.total_averages_display.setValue(self.environment_parameters.num_averages)
1610 self.run_widget.channel_display_area.time_abscissa = (
1611 np.arange(self.environment_parameters.samples_per_frame)
1612 / self.environment_parameters.sample_rate
1613 )
1614 self.run_widget.channel_display_area.frequency_abscissa = np.fft.rfftfreq(
1615 self.environment_parameters.samples_per_frame,
1616 1 / self.environment_parameters.sample_rate,
1617 )
1618 if self.environment_parameters.frf_window == "rectangle":
1619 window = 1
1620 elif self.environment_parameters.frf_window == "exponential":
1621 window_parameter = -(self.environment_parameters.samples_per_frame) / np.log(
1622 self.environment_parameters.exponential_window_value_at_frame_end
1623 )
1624 window = sig.get_window(
1625 ("exponential", 0, window_parameter),
1626 self.environment_parameters.samples_per_frame,
1627 fftbins=True,
1628 )
1629 else:
1630 window = sig.get_window(
1631 self.environment_parameters.frf_window,
1632 self.environment_parameters.samples_per_frame,
1633 fftbins=True,
1634 )
1635 self.run_widget.channel_display_area.window_function = window
1636 self.run_widget.channel_display_area.reciprocal_responses = (
1637 self.get_reciprocal_measurements()
1638 )
1640 return self.environment_parameters
1642 def retrieve_metadata(
1643 self, netcdf_handle: nc4._netCDF4.Dataset # pylint: disable=c-extension-no-member
1644 ):
1645 """Collects environment parameters from a netCDF dataset.
1647 This function retrieves parameters from a netCDF dataset that was written
1648 by the controller during streaming. It must populate the widgets
1649 in the user interface with the proper information.
1651 This function is the "read" counterpart to the store_to_netcdf
1652 function in the ModalMetadata class, which will write parameters to
1653 the netCDF file to document the metadata.
1655 Note that the entire dataset is passed to this function, so the function
1656 should collect parameters pertaining to the environment from a Group
1657 in the dataset sharing the environment's name, e.g.
1659 ``group = netcdf_handle.groups[self.environment_name]``
1660 ``self.definition_widget.parameter_selector.setValue(group.parameter)``
1662 Parameters
1663 ----------
1664 netcdf_handle : nc4._netCDF4.Dataset :
1665 The netCDF dataset from which the data will be read. It should have
1666 a group name with the enviroment's name.
1668 """
1669 netcdf_group_handle = netcdf_handle[self.environment_name]
1670 self.definition_widget.samples_per_frame_selector.setValue(
1671 netcdf_group_handle.samples_per_frame
1672 )
1673 self.definition_widget.system_id_averaging_scheme_selector.setCurrentIndex(
1674 self.definition_widget.system_id_averaging_scheme_selector.findText(
1675 netcdf_group_handle.averaging_type
1676 )
1677 )
1678 self.definition_widget.system_id_frames_to_average_selector.setValue(
1679 netcdf_group_handle.num_averages
1680 )
1681 self.definition_widget.system_id_averaging_coefficient_selector.setValue(
1682 netcdf_group_handle.averaging_coefficient
1683 )
1684 self.definition_widget.system_id_frf_technique_selector.setCurrentIndex(
1685 self.definition_widget.system_id_frf_technique_selector.findText(
1686 netcdf_group_handle.frf_technique
1687 )
1688 )
1689 self.definition_widget.system_id_transfer_function_computation_window_selector.setCurrentIndex(
1690 self.definition_widget.system_id_transfer_function_computation_window_selector.findText(
1691 netcdf_group_handle.frf_window.capitalize()
1692 )
1693 )
1694 self.definition_widget.system_id_overlap_percentage_selector.setValue(
1695 netcdf_group_handle.overlap * 100
1696 )
1697 self.definition_widget.triggering_type_selector.setCurrentIndex(
1698 self.definition_widget.triggering_type_selector.findText(
1699 netcdf_group_handle.trigger_type
1700 )
1701 )
1702 acceptance = netcdf_group_handle.accept_type
1703 self.definition_widget.acceptance_selector.blockSignals(True)
1704 self.definition_widget.acceptance_selector.setCurrentIndex(
1705 self.definition_widget.acceptance_selector.findText(acceptance)
1706 )
1707 self.definition_widget.acceptance_selector.blockSignals(False)
1708 if acceptance == "Autoreject...":
1709 self.acceptance_function = netcdf_group_handle.acceptance_function.split(":")
1710 else:
1711 self.acceptance_function = None
1712 self.definition_widget.wait_for_steady_selector.setValue(
1713 netcdf_group_handle.wait_for_steady_state
1714 )
1715 self.definition_widget.trigger_channel_selector.setCurrentIndex(
1716 netcdf_group_handle.trigger_channel
1717 )
1718 self.definition_widget.pretrigger_selector.setValue(netcdf_group_handle.pretrigger * 100)
1719 self.definition_widget.trigger_slope_selector.setCurrentIndex(
1720 0 if netcdf_group_handle.trigger_slope_positive == 1 else 1
1721 )
1722 self.definition_widget.trigger_level_selector.setValue(
1723 100 * netcdf_group_handle.trigger_level
1724 )
1725 self.definition_widget.hysteresis_selector.setValue(
1726 100 * netcdf_group_handle.hysteresis_level
1727 )
1728 self.definition_widget.hysteresis_length_selector.setValue(
1729 100 * netcdf_group_handle.hysteresis_length
1730 )
1731 self.definition_widget.signal_generator_selector.setCurrentIndex(
1732 [
1733 "none",
1734 "random",
1735 "burst",
1736 "pseudorandom",
1737 "chirp",
1738 "square",
1739 "sine",
1740 ].index(netcdf_group_handle.signal_generator_type)
1741 )
1742 self.definition_widget.random_rms_selector.setValue(
1743 netcdf_group_handle.signal_generator_level
1744 )
1745 self.definition_widget.random_min_frequency_selector.setValue(
1746 netcdf_group_handle.signal_generator_min_frequency
1747 )
1748 self.definition_widget.random_max_frequency_selector.setValue(
1749 netcdf_group_handle.signal_generator_max_frequency
1750 )
1751 self.definition_widget.burst_rms_selector.setValue(
1752 netcdf_group_handle.signal_generator_level
1753 )
1754 self.definition_widget.burst_min_frequency_selector.setValue(
1755 netcdf_group_handle.signal_generator_min_frequency
1756 )
1757 self.definition_widget.burst_max_frequency_selector.setValue(
1758 netcdf_group_handle.signal_generator_max_frequency
1759 )
1760 self.definition_widget.burst_on_percentage_selector.setValue(
1761 100 * netcdf_group_handle.signal_generator_on_fraction
1762 )
1763 self.definition_widget.pseudorandom_rms_selector.setValue(
1764 netcdf_group_handle.signal_generator_level
1765 )
1766 self.definition_widget.pseudorandom_min_frequency_selector.setValue(
1767 netcdf_group_handle.signal_generator_min_frequency
1768 )
1769 self.definition_widget.pseudorandom_max_frequency_selector.setValue(
1770 netcdf_group_handle.signal_generator_max_frequency
1771 )
1772 self.definition_widget.chirp_level_selector.setValue(
1773 netcdf_group_handle.signal_generator_level
1774 )
1775 self.definition_widget.chirp_min_frequency_selector.setValue(
1776 netcdf_group_handle.signal_generator_min_frequency
1777 )
1778 self.definition_widget.chirp_max_frequency_selector.setValue(
1779 netcdf_group_handle.signal_generator_max_frequency
1780 )
1781 self.definition_widget.square_level_selector.setValue(
1782 netcdf_group_handle.signal_generator_level
1783 )
1784 self.definition_widget.square_frequency_selector.setValue(
1785 netcdf_group_handle.signal_generator_min_frequency
1786 )
1787 self.definition_widget.square_percent_on_selector.setValue(
1788 100 * netcdf_group_handle.signal_generator_on_fraction
1789 )
1790 self.definition_widget.sine_level_selector.setValue(
1791 netcdf_group_handle.signal_generator_level
1792 )
1793 self.definition_widget.sine_frequency_selector.setValue(
1794 netcdf_group_handle.signal_generator_min_frequency
1795 )
1796 self.definition_widget.window_value_selector.setValue(
1797 netcdf_group_handle.exponential_window_value_at_frame_end * 100
1798 )
1799 response_inds = netcdf_group_handle.variables["response_channel_indices"][...]
1800 reference_inds = netcdf_group_handle.variables["reference_channel_indices"][...]
1801 for row in range(self.definition_widget.reference_channels_selector.rowCount()):
1802 if row in reference_inds:
1803 widget = self.definition_widget.reference_channels_selector.cellWidget(row, 1)
1804 widget.setChecked(True)
1805 widget = self.definition_widget.reference_channels_selector.cellWidget(row, 0)
1806 widget.setChecked(True)
1807 elif row in response_inds:
1808 widget = self.definition_widget.reference_channels_selector.cellWidget(row, 0)
1809 widget.setChecked(True)
1810 widget = self.definition_widget.reference_channels_selector.cellWidget(row, 1)
1811 widget.setChecked(False)
1812 else:
1813 widget = self.definition_widget.reference_channels_selector.cellWidget(row, 0)
1814 widget.setChecked(False)
1815 widget = self.definition_widget.reference_channels_selector.cellWidget(row, 1)
1816 widget.setChecked(False)
1818 def update_gui(self, queue_data: tuple):
1819 """Update the environment's graphical user interface
1821 This function will receive data from the gui_update_queue that
1822 specifies how the user interface should be updated. Data will usually
1823 be received as ``(instruction,data)`` pairs, where the ``instruction`` notes
1824 what operation should be taken or which widget should be modified, and
1825 the ``data`` notes what data should be used in the update.
1827 Parameters
1828 ----------
1829 queue_data : tuple
1830 A tuple containing ``(instruction,data)`` pairs where ``instruction``
1831 defines and operation or widget to be modified and ``data`` contains
1832 the data used to perform the operation.
1833 """
1834 # print('Got GUI Update {:}'.format(queue_data[0]))
1835 message, data = queue_data
1836 if message == "spectral_update":
1837 (
1838 frames,
1839 _,
1840 _,
1841 self.last_frf,
1842 self.last_coherence,
1843 last_response_cpsd,
1844 last_reference_cpsd,
1845 self.last_condition,
1846 ) = data
1847 self.run_widget.channel_display_area.last_frf = self.last_frf
1848 self.run_widget.channel_display_area.last_coh = self.last_coherence.T
1849 if last_response_cpsd.ndim == 3:
1850 self.last_response_cpsd = np.einsum("fii->fi", last_response_cpsd)
1851 else:
1852 self.last_response_cpsd = last_response_cpsd
1853 if last_reference_cpsd.ndim == 3:
1854 self.last_reference_cpsd = np.einsum("fii->fi", last_reference_cpsd)
1855 else:
1856 self.last_reference_cpsd = last_reference_cpsd
1857 # Assemble autospectrum
1858 self.run_widget.channel_display_area.last_autospectrum = np.zeros(
1859 (
1860 len(self.data_acquisition_parameters.channel_list),
1861 self.last_response_cpsd.shape[0],
1862 )
1863 )
1864 for i, index in enumerate(self.environment_parameters.reference_channel_indices):
1865 self.run_widget.channel_display_area.last_autospectrum[index, :] = (
1866 self.last_reference_cpsd[:, i].real
1867 )
1868 for i, index in enumerate(self.environment_parameters.response_channel_indices):
1869 self.run_widget.channel_display_area.last_autospectrum[index, :] = (
1870 self.last_response_cpsd[:, i].real
1871 )
1872 self.run_widget.current_average_display.setValue(frames)
1873 for window in self.run_widget.channel_display_area.subWindowList():
1874 widget = window.widget()
1875 if widget.signal_selector.currentIndex() in [3, 4, 5, 6, 7]:
1876 widget.update_data()
1877 if self.acquiring and self.netcdf_handle is not None:
1878 group = self.netcdf_handle.groups[self.environment_name]
1879 group.variables["frf_data_real"][:] = np.real(self.last_frf)
1880 group.variables["frf_data_imag"][:] = np.imag(self.last_frf)
1881 group.variables["coherence"][:] = self.last_coherence
1882 if self.acquiring and frames >= self.environment_parameters.num_averages:
1883 # print('Stopping Control')
1884 self.stop_control()
1885 self.acquiring = False
1886 # else:
1887 # print('Continuing Control')
1888 elif message == "time_frame":
1889 frame, accepted = data
1890 self.run_widget.channel_display_area.last_frame = frame
1891 self.run_widget.channel_display_area.last_spectrum = np.abs(np.fft.rfft(frame, axis=-1))
1892 for window in self.run_widget.channel_display_area.subWindowList():
1893 widget = window.widget()
1894 if widget.signal_selector.currentIndex() not in [3, 4, 5, 6, 7]:
1895 widget.update_data()
1896 if self.netcdf_handle is not None and accepted:
1897 # Get current timestep
1898 num_timesteps = self.netcdf_handle.dimensions["time_samples"].size
1899 current_frame = num_timesteps // self.environment_parameters.samples_per_frame
1900 if current_frame < self.environment_parameters.num_averages:
1901 timesteps = slice(num_timesteps, None, None)
1902 self.netcdf_handle.variables["time_data"][:, timesteps] = frame
1903 if self.environment_parameters.accept_type == "Manual" and not accepted:
1904 self.run_widget.accept_average_button.setEnabled(True)
1905 self.run_widget.reject_average_button.setEnabled(True)
1907 elif message == "finished":
1908 self.run_widget.stop_test_button.setEnabled(False)
1909 self.run_widget.preview_test_button.setEnabled(True)
1910 self.run_widget.start_test_button.setEnabled(True)
1911 self.run_widget.select_file_button.setEnabled(True)
1912 self.run_widget.dof_override_table.setEnabled(True)
1913 self.run_widget.add_override_button.setEnabled(True)
1914 self.run_widget.remove_override_button.setEnabled(True)
1915 if self.netcdf_handle is not None:
1916 self.netcdf_handle.close()
1917 self.netcdf_handle = None
1919 elif message == "enable":
1920 widget = None
1921 for parent in [
1922 self.definition_widget,
1923 self.run_widget,
1924 ]:
1925 try:
1926 widget = getattr(parent, data)
1927 break
1928 except AttributeError:
1929 continue
1930 if widget is None:
1931 raise ValueError(f"Cannot Enable Widget {data}: not found in UI")
1932 widget.setEnabled(True)
1933 elif message == "disable":
1934 widget = None
1935 for parent in [
1936 self.definition_widget,
1937 self.run_widget,
1938 ]:
1939 try:
1940 widget = getattr(parent, data)
1941 break
1942 except AttributeError:
1943 continue
1944 if widget is None:
1945 raise ValueError(f"Cannot Disable Widget {data}: not found in UI")
1946 widget.setEnabled(False)
1947 else:
1948 widget = None
1949 for parent in [self.definition_widget, self.run_widget]:
1950 try:
1951 widget = getattr(parent, message)
1952 break
1953 except AttributeError:
1954 continue
1955 if widget is None:
1956 raise ValueError(f"Cannot Update Widget {message}: not found in UI")
1957 if isinstance(widget, QtWidgets.QDoubleSpinBox):
1958 widget.setValue(data)
1959 elif isinstance(widget, QtWidgets.QSpinBox):
1960 widget.setValue(data)
1961 elif isinstance(widget, QtWidgets.QLineEdit):
1962 widget.setText(data)
1963 elif isinstance(widget, QtWidgets.QListWidget):
1964 widget.clear()
1965 widget.addItems([f"{d:.3f}" for d in data])
1967 @staticmethod
1968 def create_environment_template(
1969 environment_name: str, workbook: openpyxl.workbook.workbook.Workbook
1970 ):
1971 """Creates a template worksheet in an Excel workbook defining the
1972 environment.
1974 This function creates a template worksheet in an Excel workbook that
1975 when filled out could be read by the controller to re-create the
1976 environment.
1978 This function is the "write" counterpart to the
1979 ``set_parameters_from_template`` function in the ``ModalUI`` class,
1980 which reads the values from the template file to populate the user
1981 interface.
1983 Parameters
1984 ----------
1985 environment_name : str :
1986 The name of the environment that will specify the worksheet's name
1987 workbook : openpyxl.workbook.workbook.Workbook :
1988 A reference to an ``openpyxl`` workbook.
1990 """
1991 worksheet = workbook.create_sheet(environment_name)
1992 worksheet.cell(1, 1, "Control Type")
1993 worksheet.cell(1, 2, "Modal")
1994 worksheet.cell(2, 1, "Samples Per Frame:")
1995 worksheet.cell(2, 2, "# Number of Samples per Measurement Frame")
1996 worksheet.cell(3, 1, "Averaging Type:")
1997 worksheet.cell(3, 2, "# Averaging Type")
1998 worksheet.cell(4, 1, "Number of Averages:")
1999 worksheet.cell(4, 2, "# Number of Averages used when computing the FRF")
2000 worksheet.cell(5, 1, "Averaging Coefficient:")
2001 worksheet.cell(5, 2, "# Averaging Coefficient for Exponential Averaging")
2002 worksheet.cell(6, 1, "FRF Technique:")
2003 worksheet.cell(6, 2, "# FRF Technique")
2004 worksheet.cell(7, 1, "FRF Window:")
2005 worksheet.cell(7, 2, "# Window used to compute FRF")
2006 worksheet.cell(8, 1, "Exponential Window End Value:")
2007 worksheet.cell(
2008 8,
2009 2,
2010 "# Exponential Window Value at the end of the measurement frame (0.5 or 50%, not 50)",
2011 )
2012 worksheet.cell(9, 1, "FRF Overlap:")
2013 worksheet.cell(9, 2, "# Overlap for FRF calculations (0.5 or 50%, not 50)")
2014 worksheet.cell(10, 1, "Triggering Type:")
2015 worksheet.cell(10, 2, '# One of "Free Run", "First Frame", or "Every Frame"')
2016 worksheet.cell(11, 1, "Average Acceptance:")
2017 worksheet.cell(11, 2, '# One of "Accept All", "Manual", or "Autoreject"')
2018 worksheet.cell(12, 1, "Trigger Channel")
2019 worksheet.cell(12, 2, "# Channel number (1-based) to use for triggering")
2020 worksheet.cell(13, 1, "Pretrigger")
2021 worksheet.cell(13, 2, "# Amount of frame to use as pretrigger (0.5 or 50%, not 50)")
2022 worksheet.cell(14, 1, "Trigger Slope")
2023 worksheet.cell(14, 2, '# One of "Positive" or "Negative"')
2024 worksheet.cell(15, 1, "Trigger Level")
2025 worksheet.cell(
2026 15,
2027 2,
2028 "# Level to use to trigger the test as a fraction of the total range of the channel "
2029 "(0.5 or 50%, not 50)",
2030 )
2031 worksheet.cell(16, 1, "Hysteresis Level")
2032 worksheet.cell(
2033 16,
2034 2,
2035 "# Level that a channel must fall below before another trigger can be considered "
2036 "(0.5 or 50%, not 50)",
2037 )
2038 worksheet.cell(17, 1, "Hysteresis Frame Fraction")
2039 worksheet.cell(
2040 17,
2041 2,
2042 "# Fraction of the frame that a channel maintain hysteresis condition before another "
2043 "trigger can be considered (0.5 or 50%, not 50)",
2044 )
2045 worksheet.cell(18, 1, "Signal Generator Type")
2046 worksheet.cell(
2047 18,
2048 2,
2049 '# One of "None", "Random", "Burst Random", "Pseudorandom", "Chirp", "Square", or '
2050 '"Sine"',
2051 )
2052 worksheet.cell(19, 1, "Signal Generator Level")
2053 worksheet.cell(
2054 19,
2055 2,
2056 "# RMS voltage level for random signals, Peak voltage level for chirp, sine, and "
2057 "square pulse",
2058 )
2059 worksheet.cell(20, 1, "Signal Generator Frequency 1")
2060 worksheet.cell(
2061 20,
2062 2,
2063 "# Minimum frequency for broadband signals or frequency for sine and square pulse",
2064 )
2065 worksheet.cell(21, 1, "Signal Generator Frequency 2")
2066 worksheet.cell(
2067 21,
2068 2,
2069 "# Maximum frequency for broadband signals. Ignored for sine and square pulse",
2070 )
2071 worksheet.cell(22, 1, "Signal Generator On Fraction")
2072 worksheet.cell(
2073 22,
2074 2,
2075 "# Fraction of time that the burst or square wave is on (0.5 or 50%, not 50)",
2076 )
2077 worksheet.cell(23, 1, "Wait Time for Steady State")
2078 worksheet.cell(
2079 23,
2080 2,
2081 "# Time to wait after output starts to allow the system to reach steady state",
2082 )
2083 worksheet.cell(24, 1, "Autoaccept Script")
2084 worksheet.cell(24, 2, "# File in which an autoacceptance function is defined")
2085 worksheet.cell(25, 1, "Autoaccept Function")
2086 worksheet.cell(25, 2, "# Function name in which the autoacceptance function is defined")
2087 worksheet.cell(26, 1, "Reference Channels")
2088 worksheet.cell(26, 2, "# List of channels, one per cell on this row")
2089 worksheet.cell(27, 1, "Disabled Channels")
2090 worksheet.cell(27, 2, "# List of channels, one per cell on this row")
2092 def set_parameters_from_template(self, worksheet: openpyxl.worksheet.worksheet.Worksheet):
2093 """
2094 Collects parameters for the user interface from the Excel template file
2096 This function reads a filled out template worksheet to create an
2097 environment. Cells on this worksheet contain parameters needed to
2098 specify the environment, so this function should read those cells and
2099 update the UI widgets with those parameters.
2101 This function is the "read" counterpart to the
2102 ``create_environment_template`` function in the ``ModalUI`` class,
2103 which writes a template file that can be filled out by a user.
2106 Parameters
2107 ----------
2108 worksheet : openpyxl.worksheet.worksheet.Worksheet
2109 An openpyxl worksheet that contains the environment template.
2110 Cells on this worksheet should contain the parameters needed for the
2111 user interface.
2113 """
2114 self.definition_widget.samples_per_frame_selector.setValue(worksheet.cell(2, 2).value)
2115 self.definition_widget.system_id_averaging_scheme_selector.setCurrentIndex(
2116 self.definition_widget.system_id_averaging_scheme_selector.findText(
2117 worksheet.cell(3, 2).value
2118 )
2119 )
2120 self.definition_widget.system_id_frames_to_average_selector.setValue(
2121 worksheet.cell(4, 2).value
2122 )
2123 self.definition_widget.system_id_averaging_coefficient_selector.setValue(
2124 worksheet.cell(5, 2).value
2125 )
2126 self.definition_widget.system_id_frf_technique_selector.setCurrentIndex(
2127 self.definition_widget.system_id_frf_technique_selector.findText(
2128 worksheet.cell(6, 2).value
2129 )
2130 )
2131 self.definition_widget.system_id_transfer_function_computation_window_selector.setCurrentIndex(
2132 self.definition_widget.system_id_transfer_function_computation_window_selector.findText(
2133 worksheet.cell(7, 2).value
2134 )
2135 )
2136 self.definition_widget.window_value_selector.setValue(worksheet.cell(8, 2).value * 100)
2137 self.definition_widget.system_id_overlap_percentage_selector.setValue(
2138 worksheet.cell(9, 2).value * 100
2139 )
2140 self.definition_widget.triggering_type_selector.setCurrentIndex(
2141 self.definition_widget.triggering_type_selector.findText(worksheet.cell(10, 2).value)
2142 )
2143 acceptance = worksheet.cell(11, 2).value
2144 self.definition_widget.acceptance_selector.blockSignals(True)
2145 if acceptance == "Autoreject":
2146 self.definition_widget.acceptance_selector.setCurrentIndex(2)
2147 self.acceptance_function = [
2148 worksheet.cell(24, 2).value,
2149 worksheet.cell(25, 2).value,
2150 ]
2151 else:
2152 self.definition_widget.acceptance_selector.setCurrentIndex(
2153 self.definition_widget.acceptance_selector.findText(acceptance)
2154 )
2155 self.acceptance_function = None
2156 self.definition_widget.acceptance_selector.blockSignals(False)
2157 self.definition_widget.trigger_channel_selector.setCurrentIndex(
2158 worksheet.cell(12, 2).value - 1
2159 )
2160 self.definition_widget.pretrigger_selector.setValue(worksheet.cell(13, 2).value * 100)
2161 self.definition_widget.trigger_slope_selector.setCurrentIndex(
2162 self.definition_widget.trigger_slope_selector.findText(worksheet.cell(14, 2).value)
2163 )
2164 self.definition_widget.trigger_level_selector.setValue(worksheet.cell(15, 2).value * 100)
2165 self.definition_widget.hysteresis_selector.setValue(worksheet.cell(16, 2).value * 100)
2166 self.definition_widget.hysteresis_length_selector.setValue(
2167 worksheet.cell(17, 2).value * 100
2168 )
2169 signal_index = [
2170 "None",
2171 "Random",
2172 "Burst Random",
2173 "Pseudorandom",
2174 "Chirp",
2175 "Square",
2176 "Sine",
2177 ].index(worksheet.cell(18, 2).value)
2178 self.definition_widget.signal_generator_selector.setCurrentIndex(signal_index)
2179 level = worksheet.cell(19, 2).value
2180 freq_1 = worksheet.cell(20, 2).value
2181 freq_2 = worksheet.cell(21, 2).value
2182 sig_on = worksheet.cell(22, 2).value * 100
2183 for widget in [
2184 self.definition_widget.random_rms_selector,
2185 self.definition_widget.burst_rms_selector,
2186 self.definition_widget.pseudorandom_rms_selector,
2187 self.definition_widget.chirp_level_selector,
2188 self.definition_widget.square_level_selector,
2189 self.definition_widget.sine_level_selector,
2190 ]:
2191 widget.setValue(level)
2192 for widget in [
2193 self.definition_widget.random_min_frequency_selector,
2194 self.definition_widget.burst_min_frequency_selector,
2195 self.definition_widget.pseudorandom_min_frequency_selector,
2196 self.definition_widget.chirp_min_frequency_selector,
2197 self.definition_widget.square_frequency_selector,
2198 self.definition_widget.sine_frequency_selector,
2199 ]:
2200 widget.setValue(freq_1)
2201 for widget in [
2202 self.definition_widget.random_max_frequency_selector,
2203 self.definition_widget.burst_max_frequency_selector,
2204 self.definition_widget.pseudorandom_max_frequency_selector,
2205 self.definition_widget.chirp_max_frequency_selector,
2206 ]:
2207 widget.setValue(freq_2)
2208 for widget in [
2209 self.definition_widget.burst_on_percentage_selector,
2210 self.definition_widget.square_percent_on_selector,
2211 ]:
2212 widget.setValue(sig_on)
2213 self.definition_widget.wait_for_steady_selector.setValue(worksheet.cell(23, 2).value)
2214 column_index = 2
2215 while True:
2216 value = worksheet.cell(26, column_index).value
2217 if value is None or (isinstance(value, str) and value.strip() == ""):
2218 break
2219 widget = self.definition_widget.reference_channels_selector.cellWidget(
2220 int(value) - 1, 1
2221 )
2222 widget.setChecked(True)
2223 column_index += 1
2224 for i in range(self.definition_widget.reference_channels_selector.rowCount()):
2225 widget = self.definition_widget.reference_channels_selector.cellWidget(int(i), 0)
2226 widget.setChecked(True)
2227 column_index = 2
2228 while True:
2229 value = worksheet.cell(27, column_index).value
2230 if value is None or (isinstance(value, str) and value.strip() == ""):
2231 break
2232 widget = self.definition_widget.reference_channels_selector.cellWidget(
2233 int(value) - 1, 0
2234 )
2235 widget.setChecked(False)
2236 column_index += 1
2239class ModalEnvironment(AbstractEnvironment):
2240 """Modal Environment class defining the interface with the controller"""
2242 def __init__(
2243 self,
2244 environment_name: str,
2245 queues: ModalQueues,
2246 acquisition_active: mp.sharedctypes.Synchronized,
2247 output_active: mp.sharedctypes.Synchronized,
2248 ):
2249 super().__init__(
2250 environment_name,
2251 queues.environment_command_queue,
2252 queues.gui_update_queue,
2253 queues.controller_communication_queue,
2254 queues.log_file_queue,
2255 queues.data_in_queue,
2256 queues.data_out_queue,
2257 acquisition_active,
2258 output_active,
2259 )
2260 self.queue_container = queues
2261 self.data_acquisition_parameters = None
2262 self.environment_parameters = None
2263 self.frame_number = 0
2264 self.siggen_shutdown_achieved = False
2265 self.collector_shutdown_achieved = False
2266 self.spectral_shutdown_achieved = False
2268 # Map commands
2269 self.map_command(ModalCommands.ACCEPT_FRAME, self.accept_frame)
2270 self.map_command(ModalCommands.START_CONTROL, self.start_environment)
2271 self.map_command(ModalCommands.RUN_CONTROL, self.run_control)
2272 self.map_command(ModalCommands.STOP_CONTROL, self.stop_environment)
2273 self.map_command(ModalCommands.CHECK_FOR_COMPLETE_SHUTDOWN, self.check_for_shutdown)
2274 self.map_command(
2275 SignalGenerationCommands.SHUTDOWN_ACHIEVED, self.siggen_shutdown_achieved_fn
2276 )
2277 self.map_command(
2278 DataCollectorCommands.SHUTDOWN_ACHIEVED, self.collector_shutdown_achieved_fn
2279 )
2280 self.map_command(
2281 SpectralProcessingCommands.SHUTDOWN_ACHIEVED,
2282 self.spectral_shutdown_achieved_fn,
2283 )
2285 def initialize_data_acquisition_parameters(
2286 self, data_acquisition_parameters: DataAcquisitionParameters
2287 ):
2288 """Initialize the data acquisition parameters in the environment.
2290 The environment will receive the global data acquisition parameters from
2291 the controller, and must set itself up accordingly.
2293 Parameters
2294 ----------
2295 data_acquisition_parameters : DataAcquisitionParameters :
2296 A container containing data acquisition parameters, including
2297 channels active in the environment as well as sampling parameters.
2298 """
2299 self.data_acquisition_parameters = data_acquisition_parameters
2301 def initialize_environment_test_parameters(self, environment_parameters: ModalMetadata):
2302 """
2303 Initialize the environment parameters specific to this environment
2305 The environment will recieve parameters defining itself from the
2306 user interface and must set itself up accordingly.
2308 Parameters
2309 ----------
2310 environment_parameters : ModalMetadata
2311 A container containing the parameters defining the environment
2313 """
2314 self.environment_parameters = environment_parameters
2316 # Set up the collector
2317 self.queue_container.collector_command_queue.put(
2318 self.environment_name,
2319 (
2320 DataCollectorCommands.INITIALIZE_COLLECTOR,
2321 self.get_data_collector_metadata(),
2322 ),
2323 )
2324 # Set up the signal generation
2325 self.queue_container.signal_generation_command_queue.put(
2326 self.environment_name,
2327 (
2328 SignalGenerationCommands.INITIALIZE_PARAMETERS,
2329 self.get_signal_generation_metadata(),
2330 ),
2331 )
2332 # Set up the spectral processing
2333 self.queue_container.spectral_command_queue.put(
2334 self.environment_name,
2335 (
2336 SpectralProcessingCommands.INITIALIZE_PARAMETERS,
2337 self.get_spectral_processing_metadata(),
2338 ),
2339 )
2341 def get_data_collector_metadata(self) -> CollectorMetadata:
2342 """Collects metadata used to define the data collector"""
2343 num_channels = len(self.data_acquisition_parameters.channel_list)
2344 reference_channel_indices = self.environment_parameters.reference_channel_indices
2345 response_channel_indices = self.environment_parameters.response_channel_indices
2346 if self.environment_parameters.trigger_type == "Free Run":
2347 acquisition_type = AcquisitionType.FREE_RUN
2348 elif self.environment_parameters.trigger_type == "First Frame":
2349 acquisition_type = AcquisitionType.TRIGGER_FIRST_FRAME
2350 elif self.environment_parameters.trigger_type == "Every Frame":
2351 acquisition_type = AcquisitionType.TRIGGER_EVERY_FRAME
2352 else:
2353 raise ValueError(
2354 f"Invalid Acquisition Type: {self.environment_parameters.trigger_type}"
2355 )
2356 if self.environment_parameters.accept_type == "Accept All":
2357 acceptance = Acceptance.AUTOMATIC
2358 acceptance_function = None
2359 elif self.environment_parameters.accept_type == "Manual":
2360 acceptance = Acceptance.MANUAL
2361 acceptance_function = None
2362 elif self.environment_parameters.accept_type == "Autoreject...":
2363 acceptance = Acceptance.AUTOMATIC
2364 acceptance_function = self.environment_parameters.acceptance_function
2365 else:
2366 raise ValueError(f"Invalid Acceptance Type: {self.environment_parameters.accept_type}")
2367 overlap_fraction = self.environment_parameters.overlap
2368 trigger_channel_index = self.environment_parameters.trigger_channel
2369 trigger_slope = (
2370 TriggerSlope.POSITIVE
2371 if self.environment_parameters.trigger_slope_positive
2372 else TriggerSlope.NEGATIVE
2373 )
2374 (_, trigger_level, _, trigger_hysteresis) = self.environment_parameters.get_trigger_levels(
2375 self.data_acquisition_parameters.channel_list
2376 )
2377 trigger_hysteresis_samples = self.environment_parameters.hysteresis_samples
2378 pretrigger_fraction = self.environment_parameters.pretrigger
2379 frame_size = self.environment_parameters.samples_per_frame
2380 if self.environment_parameters.frf_window == "hann":
2381 window = Window.HANN
2382 elif self.environment_parameters.frf_window == "rectangle":
2383 window = Window.RECTANGLE
2384 elif self.environment_parameters.frf_window == "exponential":
2385 window = Window.EXPONENTIAL
2386 else:
2387 raise ValueError(f"Invalid Window Type: {self.environment_parameters.frf_window}")
2388 window_parameter = -(frame_size) / np.log(
2389 self.environment_parameters.exponential_window_value_at_frame_end
2390 )
2391 return CollectorMetadata(
2392 num_channels,
2393 response_channel_indices,
2394 reference_channel_indices,
2395 acquisition_type,
2396 acceptance,
2397 acceptance_function,
2398 overlap_fraction,
2399 trigger_channel_index,
2400 trigger_slope,
2401 trigger_level,
2402 trigger_hysteresis,
2403 trigger_hysteresis_samples,
2404 pretrigger_fraction,
2405 frame_size,
2406 window,
2407 response_transformation_matrix=None,
2408 reference_transformation_matrix=None,
2409 window_parameter_2=window_parameter,
2410 )
2412 def get_spectral_processing_metadata(self) -> SpectralProcessingMetadata:
2413 """Collects metadata to define the spectral processing"""
2414 averaging_type = (
2415 AveragingTypes.LINEAR
2416 if self.environment_parameters.averaging_type == "Linear"
2417 else AveragingTypes.EXPONENTIAL
2418 )
2419 averages = self.environment_parameters.num_averages
2420 exponential_averaging_coefficient = self.environment_parameters.averaging_coefficient
2421 if self.environment_parameters.frf_technique == "H1":
2422 frf_estimator = Estimator.H1
2423 elif self.environment_parameters.frf_technique == "H2":
2424 frf_estimator = Estimator.H2
2425 elif self.environment_parameters.frf_technique == "H3":
2426 frf_estimator = Estimator.H3
2427 elif self.environment_parameters.frf_technique == "Hv":
2428 frf_estimator = Estimator.HV
2429 else:
2430 raise ValueError(
2431 f"Invalid FRF Estimator {self.environment_parameters.frf_technique}. "
2432 "How did you get here?"
2433 )
2434 num_response_channels = len(self.environment_parameters.response_channel_indices)
2435 num_reference_channels = len(self.environment_parameters.reference_channel_indices)
2436 frequency_spacing = self.environment_parameters.frequency_spacing
2437 sample_rate = self.environment_parameters.sample_rate
2438 num_frequency_lines = self.environment_parameters.fft_lines
2439 return SpectralProcessingMetadata(
2440 averaging_type,
2441 averages,
2442 exponential_averaging_coefficient,
2443 frf_estimator,
2444 num_response_channels,
2445 num_reference_channels,
2446 frequency_spacing,
2447 sample_rate,
2448 num_frequency_lines,
2449 compute_cpsd=False,
2450 compute_apsd=True,
2451 )
2453 def get_signal_generation_metadata(self) -> SignalGenerationMetadata:
2454 """Collects metadata to define the signal generator"""
2455 return SignalGenerationMetadata(
2456 samples_per_write=self.data_acquisition_parameters.samples_per_write,
2457 level_ramp_samples=1,
2458 output_transformation_matrix=None,
2459 disabled_signals=self.environment_parameters.disabled_signals,
2460 )
2462 def get_signal_generator(self):
2463 """Gets the signal generator object used to generate signals for the environment"""
2464 return self.environment_parameters.get_signal_generator()
2466 def start_environment(self, data): # pylint: disable=unused-argument
2467 """Starts the environment
2469 Parameters
2470 ----------
2471 data : NoneType
2472 Requred by the message/data data-passing strategy in Rattlesnake, but not needed by
2473 this method
2474 """
2475 self.log("Starting Modal")
2476 self.siggen_shutdown_achieved = False
2477 self.collector_shutdown_achieved = False
2478 self.spectral_shutdown_achieved = False
2480 # Set up the collector
2481 self.queue_container.collector_command_queue.put(
2482 self.environment_name,
2483 (
2484 DataCollectorCommands.FORCE_INITIALIZE_COLLECTOR,
2485 self.get_data_collector_metadata(),
2486 ),
2487 )
2489 self.queue_container.collector_command_queue.put(
2490 self.environment_name,
2491 (
2492 DataCollectorCommands.SET_TEST_LEVEL,
2493 (self.environment_parameters.skip_frames, 1),
2494 ),
2495 )
2496 time.sleep(0.01)
2498 # Set up the signal generation
2499 self.queue_container.signal_generation_command_queue.put(
2500 self.environment_name,
2501 (
2502 SignalGenerationCommands.INITIALIZE_PARAMETERS,
2503 self.get_signal_generation_metadata(),
2504 ),
2505 )
2507 self.queue_container.signal_generation_command_queue.put(
2508 self.environment_name,
2509 (
2510 SignalGenerationCommands.INITIALIZE_SIGNAL_GENERATOR,
2511 self.get_signal_generator(),
2512 ),
2513 )
2515 self.queue_container.signal_generation_command_queue.put(
2516 self.environment_name, (SignalGenerationCommands.MUTE, None)
2517 )
2519 self.queue_container.signal_generation_command_queue.put(
2520 self.environment_name, (SignalGenerationCommands.ADJUST_TEST_LEVEL, 1.0)
2521 )
2523 # Tell the collector to start acquiring data
2524 self.queue_container.collector_command_queue.put(
2525 self.environment_name, (DataCollectorCommands.ACQUIRE, None)
2526 )
2528 # Tell the signal generation to start generating signals
2529 self.queue_container.signal_generation_command_queue.put(
2530 self.environment_name, (SignalGenerationCommands.GENERATE_SIGNALS, None)
2531 )
2533 # Set up the spectral processing
2534 self.queue_container.spectral_command_queue.put(
2535 self.environment_name,
2536 (
2537 SpectralProcessingCommands.INITIALIZE_PARAMETERS,
2538 self.get_spectral_processing_metadata(),
2539 ),
2540 )
2542 # Tell the spectral analysis to clear and start acquiring
2543 self.queue_container.spectral_command_queue.put(
2544 self.environment_name,
2545 (SpectralProcessingCommands.CLEAR_SPECTRAL_PROCESSING, None),
2546 )
2548 self.queue_container.spectral_command_queue.put(
2549 self.environment_name,
2550 (SpectralProcessingCommands.RUN_SPECTRAL_PROCESSING, None),
2551 )
2553 self.queue_container.environment_command_queue.put(
2554 self.environment_name, (ModalCommands.RUN_CONTROL, None)
2555 )
2557 def run_control(self, data): # pylint: disable=unused-argument
2558 """Runs the environment
2560 Parameters
2561 ----------
2562 data : NoneType
2563 Requred by the message/data data-passing strategy in Rattlesnake, but not needed by
2564 this method
2565 """
2566 # Pull data off the spectral queue
2567 spectral_data = flush_queue(
2568 self.queue_container.updated_spectral_quantities_queue, timeout=WAIT_TIME
2569 )
2570 if len(spectral_data) > 0:
2571 self.log("Received Data")
2572 (
2573 frames,
2574 frequencies,
2575 frf,
2576 coherence,
2577 response_cpsd,
2578 reference_cpsd,
2579 condition,
2580 ) = spectral_data[-1]
2581 self.gui_update_queue.put(
2582 (
2583 self.environment_name,
2584 (
2585 "spectral_update",
2586 (
2587 frames,
2588 self.environment_parameters.num_averages,
2589 frequencies,
2590 frf,
2591 coherence,
2592 response_cpsd,
2593 reference_cpsd,
2594 condition,
2595 ),
2596 ),
2597 )
2598 )
2599 else:
2600 time.sleep(WAIT_TIME)
2601 self.queue_container.environment_command_queue.put(
2602 self.environment_name, (ModalCommands.RUN_CONTROL, None)
2603 )
2605 def siggen_shutdown_achieved_fn(self, data): # pylint: disable=unused-argument
2606 """Sets the signal generation shutdown flag to True
2608 Parameters
2609 ----------
2610 data : NoneType
2611 Requred by the message/data data-passing strategy in Rattlesnake, but not needed by
2612 this method
2613 """
2614 self.siggen_shutdown_achieved = True
2616 def collector_shutdown_achieved_fn(self, data): # pylint: disable=unused-argument
2617 """Sets the collector shutdown flag to True
2619 Parameters
2620 ----------
2621 data : NoneType
2622 Requred by the message/data data-passing strategy in Rattlesnake, but not needed by
2623 this method
2624 """
2625 self.collector_shutdown_achieved = True
2627 def spectral_shutdown_achieved_fn(self, data): # pylint: disable=unused-argument
2628 """Sets the spectral processing shutdown flag to True
2630 Parameters
2631 ----------
2632 data : NoneType
2633 Requred by the message/data data-passing strategy in Rattlesnake, but not needed by
2634 this method
2635 """
2636 self.spectral_shutdown_achieved = True
2638 def check_for_shutdown(self, data): # pylint: disable=unused-argument
2639 """Checks if all environment subprocesses have shut down successfully.
2641 Parameters
2642 ----------
2643 data : NoneType
2644 Requred by the message/data data-passing strategy in Rattlesnake, but not needed by
2645 this method
2646 """
2647 if (
2648 self.siggen_shutdown_achieved
2649 and self.collector_shutdown_achieved
2650 and self.spectral_shutdown_achieved
2651 ):
2652 self.log("Shutdown Achieved")
2653 self.gui_update_queue.put((self.environment_name, ("finished", None)))
2654 else:
2655 # Recheck some time later
2656 time.sleep(1)
2657 self.environment_command_queue.put(
2658 self.environment_name, (ModalCommands.CHECK_FOR_COMPLETE_SHUTDOWN, None)
2659 )
2661 def accept_frame(self, data):
2662 """Accepts or rejects the previous measurement frame"""
2663 self.queue_container.collector_command_queue.put(
2664 self.environment_name, (DataCollectorCommands.ACCEPT, data)
2665 )
2667 def stop_environment(self, data):
2668 """Stop the environment gracefully
2670 This function defines the operations to shut down the environment
2671 gracefully so there is no hard stop that might damage test equipment
2672 or parts.
2674 Parameters
2675 ----------
2676 data : Ignored
2677 This parameter is not used by the function but must be present
2678 due to the calling signature of functions called through the
2679 ``command_map``
2681 """
2682 self.log("Stopping Control")
2683 flush_queue(self.queue_container.environment_command_queue)
2684 self.queue_container.collector_command_queue.put(
2685 self.environment_name, (DataCollectorCommands.SET_TEST_LEVEL, (1000, 1))
2686 )
2687 self.queue_container.signal_generation_command_queue.put(
2688 self.environment_name, (SignalGenerationCommands.START_SHUTDOWN, None)
2689 )
2690 self.queue_container.spectral_command_queue.put(
2691 self.environment_name,
2692 (SpectralProcessingCommands.STOP_SPECTRAL_PROCESSING, None),
2693 )
2694 self.queue_container.environment_command_queue.put(
2695 self.environment_name, (ModalCommands.CHECK_FOR_COMPLETE_SHUTDOWN, None)
2696 )
2698 def quit(self, data):
2699 """Returns True to stop the ``run`` while loop and exit the process
2701 Parameters
2702 ----------
2703 data : Ignored
2704 This parameter is not used by the function but must be present
2705 due to the calling signature of functions called through the
2706 ``command_map``
2708 Returns
2709 -------
2710 True :
2711 This function returns True to signal to the ``run`` while loop
2712 that it is time to close down the environment.
2714 """
2715 for queue in [
2716 self.queue_container.spectral_command_queue,
2717 self.queue_container.signal_generation_command_queue,
2718 self.queue_container.collector_command_queue,
2719 ]:
2720 queue.put(self.environment_name, (GlobalCommands.QUIT, None))
2721 return True
2724def modal_process(
2725 environment_name: str,
2726 input_queue: VerboseMessageQueue,
2727 gui_update_queue: Queue,
2728 controller_communication_queue: VerboseMessageQueue,
2729 log_file_queue: Queue,
2730 data_in_queue: Queue,
2731 data_out_queue: Queue,
2732 acquisition_active: mp.sharedctypes.Synchronized,
2733 output_active: mp.sharedctypes.Synchronized,
2734):
2735 """Modal environment process function called by multiprocessing
2737 This function defines the Modal Environment process that
2738 gets run by the multiprocessing module when it creates a new process. It
2739 creates a ModalEnvironment object and runs it.
2741 Parameters
2742 ----------
2743 environment_name : str :
2744 Name of the environment
2745 input_queue : VerboseMessageQueue :
2746 Queue containing instructions for the environment
2747 gui_update_queue : Queue :
2748 Queue where GUI updates are put
2749 controller_communication_queue : Queue :
2750 Queue for global communications with the controller
2751 log_file_queue : Queue :
2752 Queue for writing log file messages
2753 data_in_queue : Queue :
2754 Queue from which data will be read by the environment
2755 data_out_queue : Queue :
2756 Queue to which data will be written that will be output by the hardware.
2758 """
2759 queue_container = ModalQueues(
2760 environment_name,
2761 input_queue,
2762 gui_update_queue,
2763 controller_communication_queue,
2764 data_in_queue,
2765 data_out_queue,
2766 log_file_queue,
2767 )
2769 spectral_proc = mp.Process(
2770 target=spectral_processing_process,
2771 args=(
2772 environment_name,
2773 queue_container.spectral_command_queue,
2774 queue_container.data_for_spectral_computation_queue,
2775 queue_container.updated_spectral_quantities_queue,
2776 queue_container.environment_command_queue,
2777 queue_container.gui_update_queue,
2778 queue_container.log_file_queue,
2779 ),
2780 )
2781 spectral_proc.start()
2782 siggen_proc = mp.Process(
2783 target=signal_generation_process,
2784 args=(
2785 environment_name,
2786 queue_container.signal_generation_command_queue,
2787 queue_container.signal_generation_update_queue,
2788 queue_container.data_out_queue,
2789 queue_container.environment_command_queue,
2790 queue_container.log_file_queue,
2791 queue_container.gui_update_queue,
2792 ),
2793 )
2794 siggen_proc.start()
2795 collection_proc = mp.Process(
2796 target=data_collector_process,
2797 args=(
2798 environment_name,
2799 queue_container.collector_command_queue,
2800 queue_container.data_in_queue,
2801 [queue_container.data_for_spectral_computation_queue],
2802 queue_container.environment_command_queue,
2803 queue_container.log_file_queue,
2804 queue_container.gui_update_queue,
2805 ),
2806 )
2808 collection_proc.start()
2810 process_class = ModalEnvironment(
2811 environment_name, queue_container, acquisition_active, output_active
2812 )
2813 process_class.run()
2815 # Rejoin all the processes
2816 process_class.log("Joining Subprocesses")
2817 process_class.log("Joining Spectral Computation")
2818 spectral_proc.join()
2819 process_class.log("Joining Signal Generation")
2820 siggen_proc.join()
2821 process_class.log("Joining Data Collection")
2822 collection_proc.join()