1"""
2Defines data analysis performed for environments that use system identification
3
4Abstract environment that can be used to create new environment control strategies
5in the controller that use system identification.
6
7Rattlesnake Vibration Control Software
8Copyright (C) 2021 National Technology & Engineering Solutions of Sandia, LLC
9(NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S.
10Government retains certain rights in this software.
11
12This program is free software: you can redistribute it and/or modify
13it under the terms of the GNU General Public License as published by
14the Free Software Foundation, either version 3 of the License, or
15(at your option) any later version.
16
17This program is distributed in the hope that it will be useful,
18but WITHOUT ANY WARRANTY; without even the implied warranty of
19MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20GNU General Public License for more details.
21
22You should have received a copy of the GNU General Public License
23along with this program. If not, see <https://www.gnu.org/licenses/>.
24"""
25
26import multiprocessing as mp
27from enum import Enum
28
29from .abstract_message_process import AbstractMessageProcess
30from .abstract_sysid_environment import AbstractSysIdMetadata
31from .utilities import VerboseMessageQueue, flush_queue
32
33
34class SysIDDataAnalysisCommands(Enum):
35 """Valid commands to send to the data analysis process of an environment using system id"""
36
37 INITIALIZE_PARAMETERS = 0
38 RUN_NOISE = 1
39 RUN_TRANSFER_FUNCTION = 2
40 START_SHUTDOWN_AND_RUN_SYSID = 3
41 START_SHUTDOWN = 4
42 STOP_SYSTEM_ID = 5
43 SHUTDOWN_ACHIEVED = 6
44 SYSTEM_ID_COMPLETE = 7
45 LOAD_TRANSFER_FUNCTION = 8
46 LOAD_NOISE = 9
47
48
49class AbstractSysIDAnalysisProcess(AbstractMessageProcess):
50 """Process to perform data analysis and control calculations in an environment
51 using system id"""
52
53 def __init__(
54 self,
55 process_name: str,
56 command_queue: VerboseMessageQueue,
57 data_in_queue: mp.queues.Queue,
58 data_out_queue: mp.queues.Queue,
59 environment_command_queue: VerboseMessageQueue,
60 log_file_queue: mp.queues.Queue,
61 gui_update_queue: mp.queues.Queue,
62 environment_name: str,
63 ):
64 """Initialize the environment process
65
66 Parameters
67 ----------
68 process_name : str
69 The name of the process
70 command_queue : VerboseMessageQueue
71 A queue used to send commands to this process
72 data_in_queue : mp.queues.Queue
73 A queue receiving frames of data from the data collector
74 data_out_queue : mp.queues.Queue
75 A queue to put the next output or analysis results for the environment to use
76 environment_command_queue : VerboseMessageQueue
77 A queue used to send commands to the main environment process
78 log_file_queue : mp.queues.Queue
79 A queue used to send log file strings
80 gui_update_queue : mp.queues.Queue
81 A queue used to send updates back to the graphical user interface
82 environment_name : str
83 The name of the environment owning this process
84 """
85 super().__init__(process_name, log_file_queue, command_queue, gui_update_queue)
86 self.map_command(
87 SysIDDataAnalysisCommands.INITIALIZE_PARAMETERS,
88 self.initialize_sysid_parameters,
89 )
90 self.map_command(SysIDDataAnalysisCommands.RUN_NOISE, self.run_sysid_noise)
91 self.map_command(
92 SysIDDataAnalysisCommands.RUN_TRANSFER_FUNCTION,
93 self.run_sysid_transfer_function,
94 )
95 self.map_command(SysIDDataAnalysisCommands.STOP_SYSTEM_ID, self.stop_sysid)
96 self.map_command(SysIDDataAnalysisCommands.LOAD_NOISE, self.load_sysid_noise)
97 self.map_command(
98 SysIDDataAnalysisCommands.LOAD_TRANSFER_FUNCTION,
99 self.load_sysid_transfer_function,
100 )
101 self.environment_name = environment_name
102 self.environment_command_queue = environment_command_queue
103 self.data_in_queue = data_in_queue
104 self.data_out_queue = data_out_queue
105 self.parameters = None
106 self.frames = None
107 self.frequencies = None
108 self.sysid_frf = None
109 self.sysid_coherence = None
110 self.sysid_response_cpsd = None
111 self.sysid_reference_cpsd = None
112 self.sysid_response_noise = None
113 self.sysid_reference_noise = None
114 self.sysid_condition = None
115 self.startup = True
116
117 def initialize_sysid_parameters(self, data: AbstractSysIdMetadata):
118 """Stores parameters describing the system identification into the object
119
120 Parameters
121 ----------
122 data : AbstractSysIdMetadata
123 A metadata object containing the parameters to define the system identification
124 """
125 self.parameters = data
126
127 def load_sysid_noise(self, spectral_data):
128 """Loads noise data from a previous system identification
129
130 Parameters
131 ----------
132 spectral_data : tuple
133 A tuple containing frames, frequencies, system id FRFs, coherence, response cpsd,
134 reference_cpsd and condition number
135 """
136 self.log("Obtained Spectral Data")
137 (
138 self.frames,
139 self.frequencies,
140 _,
141 _,
142 self.sysid_response_noise,
143 self.sysid_reference_noise,
144 _,
145 ) = spectral_data
146
147 def load_sysid_transfer_function(self, spectral_data, skip_sysid=True):
148 """Loads system ID data from a previous system identification
149
150 Parameters
151 ----------
152 spectral_data : tuple
153 A tuple containing frames, frequencies, system id FRFs, coherence, response cpsd,
154 reference_cpsd and condition number
155 skip_sysid : bool, optional
156 If True, send the system identification complete flag to the controller. By default True
157 """
158 self.log("Obtained Spectral Data")
159 (
160 self.frames,
161 self.frequencies,
162 self.sysid_frf,
163 self.sysid_coherence,
164 self.sysid_response_cpsd,
165 self.sysid_reference_cpsd,
166 self.sysid_condition,
167 ) = spectral_data
168 if skip_sysid:
169 self.environment_command_queue.put(
170 self.process_name,
171 (
172 SysIDDataAnalysisCommands.SYSTEM_ID_COMPLETE,
173 (
174 self.frames,
175 0,
176 self.frequencies,
177 self.sysid_frf,
178 self.sysid_coherence,
179 self.sysid_response_cpsd,
180 self.sysid_reference_cpsd,
181 self.sysid_condition,
182 self.sysid_response_noise,
183 self.sysid_reference_noise,
184 ),
185 ),
186 )
187
188 def run_sysid_noise(self, auto_shutdown):
189 """Starts and runs the system identification noise phase.
190
191 Parameters
192 ----------
193 auto_shutdown : bool
194 If True, the environment will automatically shut down when the requested number of
195 frames is reached. If False, the noise characterization will run until manually
196 stopped.
197 """
198 if self.startup:
199 self.startup = False
200 self.frames = 0
201 spectral_data = flush_queue(self.data_in_queue)
202 if len(spectral_data) > 0:
203 self.load_sysid_noise(spectral_data[-1])
204 self.gui_update_queue.put(
205 (
206 self.environment_name,
207 (
208 "noise_update",
209 (
210 self.frames,
211 self.parameters.sysid_noise_averages,
212 self.frequencies,
213 self.sysid_response_noise,
214 self.sysid_reference_noise,
215 ),
216 ),
217 )
218 )
219 if auto_shutdown and self.parameters.sysid_noise_averages == self.frames:
220 self.environment_command_queue.put(
221 self.process_name,
222 (SysIDDataAnalysisCommands.START_SHUTDOWN_AND_RUN_SYSID, None),
223 )
224 self.stop_sysid(None)
225 else:
226 self.command_queue.put(
227 self.process_name, (SysIDDataAnalysisCommands.RUN_NOISE, auto_shutdown)
228 )
229
230 def run_sysid_transfer_function(self, auto_shutdown):
231 """Starts and runs the system identification
232
233 Parameters
234 ----------
235 auto_shutdown : bool
236 If True, the system identification will stop automatically upon reaching the requested
237 number of measurement frames. If False, it will run indefinitely until manually
238 stopped.
239 """
240 if self.startup:
241 self.startup = False
242 self.frames = 0
243 spectral_data = flush_queue(self.data_in_queue)
244 if len(spectral_data) > 0:
245 self.load_sysid_transfer_function(spectral_data[-1], skip_sysid=False)
246 self.gui_update_queue.put(
247 (
248 self.environment_name,
249 (
250 "sysid_update",
251 (
252 self.frames,
253 self.parameters.sysid_averages,
254 self.frequencies,
255 self.sysid_frf,
256 self.sysid_coherence,
257 self.sysid_response_cpsd,
258 self.sysid_reference_cpsd,
259 self.sysid_condition,
260 ),
261 ),
262 )
263 )
264 if auto_shutdown and self.parameters.sysid_averages == self.frames:
265 self.environment_command_queue.put(
266 self.process_name,
267 (SysIDDataAnalysisCommands.START_SHUTDOWN, (False, True)),
268 )
269 self.stop_sysid(None)
270 self.environment_command_queue.put(
271 self.process_name,
272 (
273 SysIDDataAnalysisCommands.SYSTEM_ID_COMPLETE,
274 (
275 self.frames,
276 self.parameters.sysid_averages,
277 self.frequencies,
278 self.sysid_frf,
279 self.sysid_coherence,
280 self.sysid_response_cpsd,
281 self.sysid_reference_cpsd,
282 self.sysid_condition,
283 self.sysid_response_noise,
284 self.sysid_reference_noise,
285 ),
286 ),
287 )
288 else:
289 self.command_queue.put(
290 self.process_name,
291 (SysIDDataAnalysisCommands.RUN_TRANSFER_FUNCTION, auto_shutdown),
292 )
293
294 def stop_sysid(self, data): # pylint: disable=unused-argument
295 """Stops the currently running system identification phase
296
297 Parameters
298 ----------
299 data : ignored
300 This argument is not used, but is required by the calling signature of functions
301 that get called via the command map.
302 """
303 # Remove any run_transfer_function or run_control from the queue
304 instructions = self.command_queue.flush(self.process_name)
305 for instruction in instructions:
306 if not instruction[0] in [
307 SysIDDataAnalysisCommands.RUN_NOISE,
308 SysIDDataAnalysisCommands.RUN_TRANSFER_FUNCTION,
309 ]:
310 self.command_queue.put(self.process_name, instruction)
311 flush_queue(self.data_out_queue)
312 self.startup = True
313 self.environment_command_queue.put(
314 self.process_name, (SysIDDataAnalysisCommands.SHUTDOWN_ACHIEVED, None)
315 )
316
317
318def sysid_data_analysis_process(
319 environment_name: str,
320 command_queue: VerboseMessageQueue,
321 data_in_queue: mp.queues.Queue,
322 data_out_queue: mp.queues.Queue,
323 environment_command_queue: VerboseMessageQueue,
324 gui_update_queue: mp.queues.Queue,
325 log_file_queue: mp.queues.Queue,
326 process_name=None,
327):
328 """An function called by multiprocessing to start up the system identification analysis
329 process.
330
331 Some environments may override the AbstractSysIDAnalysisProcess class and therefore should
332 redefine this function to call that class.
333
334 Parameters
335 ----------
336 environment_name : str
337 The name of the environment
338 command_queue : VerboseMessageQueue
339 A queue used to send commands to this process
340 data_in_queue : mp.queues.Queue
341 A queue used to send frames of data and spectral quantities to the data analysis process
342 data_out_queue : mp.queues.Queue
343 A queue used to send control and analysis results back to the environment
344 environment_command_queue : VerboseMessageQueue
345 A queue used to send commands to the environment
346 gui_update_queue : mp.queues.Queue
347 A queue used to send updates to the graphical user interface
348 log_file_queue : mp.queues.Queue
349 A queue used to send log file messages
350 process_name : _type_, optional
351 A name for the process. If not specified, it will be the environment name appended with
352 Data Analysis.
353 """
354 data_analysis_instance = AbstractSysIDAnalysisProcess(
355 environment_name + " Data Analysis" if process_name is None else process_name,
356 command_queue,
357 data_in_queue,
358 data_out_queue,
359 environment_command_queue,
360 log_file_queue,
361 gui_update_queue,
362 environment_name,
363 )
364
365 data_analysis_instance.run()