Coverage for  / opt / hostedtoolcache / Python / 3.11.14 / x64 / lib / python3.11 / site-packages / rattlesnake / components / spectral_processing.py: 41%

280 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-27 18:22 +0000

1# -*- coding: utf-8 -*- 

2""" 

3Controller subsystem that handles computation of FRFs, CPSDs, and other spectral 

4quantities of interest 

5 

6Rattlesnake Vibration Control Software 

7Copyright (C) 2021 National Technology & Engineering Solutions of Sandia, LLC 

8(NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. 

9Government retains certain rights in this software. 

10 

11This program is free software: you can redistribute it and/or modify 

12it under the terms of the GNU General Public License as published by 

13the Free Software Foundation, either version 3 of the License, or 

14(at your option) any later version. 

15 

16This program is distributed in the hope that it will be useful, 

17but WITHOUT ANY WARRANTY; without even the implied warranty of 

18MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

19GNU General Public License for more details. 

20 

21You should have received a copy of the GNU General Public License 

22along with this program. If not, see <https://www.gnu.org/licenses/>. 

23""" 

24 

25import multiprocessing as mp 

26import time 

27from enum import Enum 

28 

29import numpy as np 

30 

31from .abstract_message_process import AbstractMessageProcess 

32from .utilities import GlobalCommands, VerboseMessageQueue, flush_queue 

33 

34WAIT_TIME = 0.05 

35 

36 

37class SpectralProcessingCommands(Enum): 

38 """Collection of instructions that the FRF Computation Process might get""" 

39 

40 INITIALIZE_PARAMETERS = 0 

41 RUN_SPECTRAL_PROCESSING = 1 

42 CLEAR_SPECTRAL_PROCESSING = 2 

43 STOP_SPECTRAL_PROCESSING = 3 

44 SENT_SPECTRAL_DATA = 4 

45 SHUTDOWN_ACHIEVED = 5 

46 

47 

48class AveragingTypes(Enum): 

49 """Collection of averating types that can exist""" 

50 

51 LINEAR = 0 

52 EXPONENTIAL = 1 

53 

54 

55class Estimator(Enum): 

56 """Collection of FRF Estimators that can exist""" 

57 

58 H1 = 0 

59 H2 = 1 

60 H3 = 2 

61 HV = 3 

62 

63 

64class SpectralProcessingMetadata: 

65 """Metadata required to define the signal processing process""" 

66 

67 def __init__( 

68 self, 

69 averaging_type, 

70 averages, 

71 exponential_averaging_coefficient, 

72 frf_estimator, 

73 num_response_channels, 

74 num_reference_channels, 

75 frequency_spacing, 

76 sample_rate, 

77 num_frequency_lines, 

78 compute_cpsd=True, 

79 compute_frf=True, 

80 compute_coherence=True, 

81 compute_apsd=True, 

82 ): 

83 self.averaging_type = averaging_type 

84 self.averages = averages 

85 self.exponential_averaging_coefficient = exponential_averaging_coefficient 

86 self.frf_estimator = frf_estimator 

87 self.num_response_channels = num_response_channels 

88 self.num_reference_channels = num_reference_channels 

89 self.frequency_spacing = frequency_spacing 

90 self.sample_rate = sample_rate 

91 self.num_frequency_lines = num_frequency_lines 

92 self.compute_cpsd = compute_cpsd 

93 self.compute_frf = compute_frf 

94 self.compute_coherence = compute_coherence 

95 self.compute_apsd = compute_apsd 

96 

97 def __eq__(self, other): 

98 try: 

99 return np.all( 

100 [np.all(value == other.__dict__[field]) for field, value in self.__dict__.items()] 

101 ) 

102 except (AttributeError, KeyError): 

103 return False 

104 

105 @property 

106 def requires_full_spectral_response(self): 

107 """Checks if the requested outputs require calculation the full response CPSD matrix""" 

108 if ( 

109 self.compute_frf and self.frf_estimator in [Estimator.H2, Estimator.H3] 

110 ) or self.compute_cpsd: 

111 return True 

112 else: 

113 return False 

114 

115 @property 

116 def requires_diagonal_spectral_response(self): 

117 """Checks if the requested outputs require calculation of the 

118 diagonals of the response matrix""" 

119 if ( 

120 (self.compute_frf and self.frf_estimator in [Estimator.HV]) 

121 or self.compute_apsd 

122 or self.compute_coherence 

123 ): 

124 return True 

125 else: 

126 return False 

127 

128 @property 

129 def requires_full_spectral_reference(self): 

130 """Checks if the requested outputs require calculation of the full reference CPSD""" 

131 if ( 

132 (self.compute_frf and self.frf_estimator in [Estimator.H1, Estimator.H3, Estimator.HV]) 

133 or self.compute_cpsd 

134 or self.compute_coherence 

135 ): 

136 return True 

137 else: 

138 return False 

139 

140 @property 

141 def requires_diagonal_spectral_reference(self): 

142 """Checks if the requested outputs require calculation of the diagonals of the reference 

143 matrix""" 

144 if self.compute_apsd: 

145 return True 

146 else: 

147 return False 

148 

149 @property 

150 def requires_spectral_reference_response(self): 

151 """Checks if the requested outputs require calculation of the cross spectra between 

152 reference and response""" 

153 if self.compute_frf or self.compute_coherence: 

154 return True 

155 else: 

156 return False 

157 

158 

159class SpectralProcessingProcess(AbstractMessageProcess): 

160 """Class defining a subprocess that computes a FRF from a time history.""" 

161 

162 def __init__( 

163 self, 

164 process_name: str, 

165 command_queue: VerboseMessageQueue, 

166 data_in_queue: mp.queues.Queue, 

167 data_out_queue: mp.queues.Queue, 

168 environment_command_queue: VerboseMessageQueue, 

169 gui_update_queue: mp.queues.Queue, 

170 log_file_queue: mp.queues.Queue, 

171 environment_name: str, 

172 ): 

173 """ 

174 Constructor for the FRF Computation Process 

175 

176 Sets up the ``command_map`` and initializes internal data 

177 

178 Parameters 

179 ---------- 

180 process_name : str 

181 Name for the process that will be used in the Log file. 

182 command_queue : VerboseMessageQueue : 

183 The queue containing instructions for the FRF process 

184 data_for_frf_queue : mp.queues.Queue : 

185 Queue containing input data for the FRF computation 

186 updated_frf_queue : mp.queues.Queue : 

187 Queue where frf process will put computed frfs 

188 gui_update_queue : mp.queues.Queue : 

189 Queue for gui updates 

190 log_file_queue : mp.queues.Queue : 

191 Queue for writing to the log file 

192 environment_name : str 

193 Name of the environment that controls this subprocess. 

194 

195 """ 

196 super().__init__(process_name, log_file_queue, command_queue, gui_update_queue) 

197 self.map_command( 

198 SpectralProcessingCommands.INITIALIZE_PARAMETERS, self.initialize_parameters 

199 ) 

200 self.map_command( 

201 SpectralProcessingCommands.RUN_SPECTRAL_PROCESSING, 

202 self.run_spectral_processing, 

203 ) 

204 self.map_command( 

205 SpectralProcessingCommands.CLEAR_SPECTRAL_PROCESSING, 

206 self.clear_spectral_processing, 

207 ) 

208 self.map_command( 

209 SpectralProcessingCommands.STOP_SPECTRAL_PROCESSING, 

210 self.stop_spectral_processing, 

211 ) 

212 self.environment_name = environment_name 

213 self.data_in_queue = data_in_queue 

214 self.data_out_queue = data_out_queue 

215 self.environment_command_queue = environment_command_queue 

216 self.response_spectral_matrix = None 

217 self.reference_spectral_matrix = None 

218 self.response_reference_spectral_matrix = None 

219 self.reference_diagonal_matrix = None 

220 self.response_diagonal_matrix = None 

221 self.response_fft = None 

222 self.reference_fft = None 

223 self.spectral_processing_parameters = None 

224 self.frames_computed = 0 

225 

226 def initialize_parameters(self, data: SpectralProcessingMetadata): 

227 """Initializes the signal processing parameters from the environment. 

228 

229 Parameters 

230 ---------- 

231 data : 

232 Container containing the setting specific to the environment. 

233 

234 """ 

235 if self.spectral_processing_parameters is None: 

236 reshape_arrays = True 

237 elif ( 

238 self.spectral_processing_parameters.num_frequency_lines != data.num_frequency_lines 

239 or self.spectral_processing_parameters.num_response_channels 

240 != data.num_response_channels 

241 or self.spectral_processing_parameters.num_reference_channels 

242 != data.num_reference_channels 

243 or self.spectral_processing_parameters.averages != data.averages 

244 or self.spectral_processing_parameters.averaging_type != data.averaging_type 

245 ): 

246 reshape_arrays = True 

247 else: 

248 reshape_arrays = False 

249 self.spectral_processing_parameters = data 

250 if reshape_arrays: 

251 self.log("Initializing Empty Arrays") 

252 self.frames_computed = 0 

253 self.response_spectral_matrix = None 

254 self.reference_spectral_matrix = None 

255 self.response_reference_spectral_matrix = None 

256 self.reference_diagonal_matrix = None 

257 self.response_diagonal_matrix = None 

258 if self.spectral_processing_parameters.averaging_type == AveragingTypes.LINEAR: 

259 self.response_fft = np.nan * np.ones( 

260 ( 

261 self.spectral_processing_parameters.averages, 

262 self.spectral_processing_parameters.num_response_channels, 

263 self.spectral_processing_parameters.num_frequency_lines, 

264 ), 

265 dtype=complex, 

266 ) 

267 self.reference_fft = np.nan * np.ones( 

268 ( 

269 self.spectral_processing_parameters.averages, 

270 self.spectral_processing_parameters.num_reference_channels, 

271 self.spectral_processing_parameters.num_frequency_lines, 

272 ), 

273 dtype=complex, 

274 ) 

275 # print(self.response_fft.shape) 

276 else: 

277 self.response_fft = None 

278 self.reference_fft = None 

279 

280 def run_spectral_processing(self, data): 

281 """Continuously compute FRFs from time histories. 

282 

283 This function accepts data from the ``data_for_frf_queue`` and computes 

284 FRF matrices from the time data. It uses a rolling buffer to append 

285 data. The oldest data is pushed out of the buffer by the newest data. 

286 The test level is also passed with the response data and output 

287 data. The test level is used to ensure that no frame uses 

288 discontinuous data. 

289 

290 Parameters 

291 ---------- 

292 data : Ignored 

293 This parameter is not used by the function but must be present 

294 due to the calling signature of functions called through the 

295 ``command_map`` 

296 

297 """ 

298 data = flush_queue(self.data_in_queue, timeout=WAIT_TIME) 

299 if len(data) == 0: 

300 time.sleep(WAIT_TIME) 

301 self.command_queue.put( 

302 self.process_name, 

303 (SpectralProcessingCommands.RUN_SPECTRAL_PROCESSING, None), 

304 ) 

305 return 

306 frames_received = len(data) 

307 self.log(f"Received {frames_received} Frames") 

308 if self.spectral_processing_parameters.averaging_type == AveragingTypes.LINEAR: 

309 response_fft, reference_fft = [value for value in zip(*data)] 

310 self.response_fft = np.concatenate( 

311 ( 

312 self.response_fft[frames_received:], 

313 response_fft[-self.response_fft.shape[0] :], 

314 ), 

315 axis=0, 

316 ) 

317 self.reference_fft = np.concatenate( 

318 ( 

319 self.reference_fft[frames_received:], 

320 reference_fft[-self.reference_fft.shape[0] :], 

321 ), 

322 axis=0, 

323 ) 

324 self.log( 

325 f"Buffered Frames (Resp Shape: {self.response_fft.shape}, " 

326 f"Ref Shape: {self.reference_fft.shape})" 

327 ) 

328 # Exclude any with NaNs 

329 exclude_averages = np.any(np.isnan(self.response_fft), axis=(-1, -2)) 

330 self.log(f"Computed Number Averages {(~exclude_averages).sum()}") 

331 # Return if there is actually no data 

332 if np.all(exclude_averages): 

333 self.command_queue.put( 

334 self.process_name, 

335 (SpectralProcessingCommands.RUN_SPECTRAL_PROCESSING, None), 

336 ) 

337 return 

338 mean_fft = np.mean(np.abs(self.reference_fft[~exclude_averages]), axis=(-1, -2)) 

339 self.log(f"Mean FFT Value Over Averaged Frames: \n {mean_fft}") 

340 

341 # Now we compute the spectral matrices depending on what is required. 

342 # Compute the response power spectra 

343 response_spectral_time = time.time() 

344 if self.spectral_processing_parameters.requires_full_spectral_response: 

345 self.log("Computing Full Spectral Response Matrix") 

346 self.response_spectral_matrix = ( 

347 np.einsum( 

348 "aif,ajf->fij", 

349 self.response_fft[~exclude_averages], 

350 np.conj(self.response_fft[~exclude_averages]), 

351 ) 

352 / self.response_fft[~exclude_averages].shape[0] 

353 ) 

354 # Get the diagonal matrix as well 

355 self.response_diagonal_matrix = np.einsum("fii->fi", self.response_spectral_matrix) 

356 elif self.spectral_processing_parameters.requires_diagonal_spectral_response: 

357 self.log("Computing Diagonal of Spectral Response Matrix") 

358 # self.response_diagonal_matrix = np.einsum( 

359 # 'aif,aif->fi', 

360 # self.response_fft[~exclude_averages], 

361 # np.conj(self.response_fft[~exclude_averages]) 

362 # )/self.response_fft[~exclude_averages].shape[0] 

363 self.response_diagonal_matrix = np.mean( 

364 self.response_fft[~exclude_averages] 

365 * np.conj(self.response_fft[~exclude_averages]), 

366 axis=0, 

367 ).T 

368 if ( 

369 self.spectral_processing_parameters.requires_full_spectral_response 

370 or self.spectral_processing_parameters.requires_diagonal_spectral_response 

371 ): 

372 self.log( 

373 "Computed Response Spectral Matrix in " 

374 f"{time.time() - response_spectral_time:0.2f} seconds" 

375 ) 

376 

377 # Compute the reference power spectra 

378 reference_spectral_time = time.time() 

379 if self.spectral_processing_parameters.requires_full_spectral_reference: 

380 self.log("Computing Full Spectral Reference Matrix") 

381 self.reference_spectral_matrix = ( 

382 np.einsum( 

383 "aif,ajf->fij", 

384 self.reference_fft[~exclude_averages], 

385 np.conj(self.reference_fft[~exclude_averages]), 

386 ) 

387 / self.reference_fft[~exclude_averages].shape[0] 

388 ) 

389 # Get the diagonal matrix as well 

390 self.reference_diagonal_matrix = np.einsum( 

391 "fii->fi", self.reference_spectral_matrix 

392 ) 

393 elif self.spectral_processing_parameters.requires_diagonal_spectral_reference: 

394 self.log("Computing Diagonal of Spectral Reference Matrix") 

395 self.reference_diagonal_matrix = ( 

396 np.einsum( 

397 "aif,aif->fi", 

398 self.reference_fft[~exclude_averages], 

399 np.conj(self.reference_fft[~exclude_averages]), 

400 ) 

401 / self.reference_fft[~exclude_averages].shape[0] 

402 ) 

403 if ( 

404 self.spectral_processing_parameters.requires_full_spectral_reference 

405 or self.spectral_processing_parameters.requires_diagonal_spectral_reference 

406 ): 

407 self.log( 

408 f"Computed Reference Spectral Matrix in " 

409 f"{time.time() - reference_spectral_time:0.2f} seconds" 

410 ) 

411 

412 # Compute cross spectra between reference and response 

413 if self.spectral_processing_parameters.requires_spectral_reference_response: 

414 cross_spectral_time = time.time() 

415 self.log("Computing Full Cross Spectral Response/Reference Matrix") 

416 self.response_reference_spectral_matrix = ( 

417 np.einsum( 

418 "aif,ajf->fij", 

419 self.response_fft[~exclude_averages], 

420 np.conj(self.reference_fft[~exclude_averages]), 

421 ) 

422 / self.response_fft[~exclude_averages].shape[0] 

423 ) 

424 self.log( 

425 "Computed Crossspectral Matrix in " 

426 f"{time.time() - cross_spectral_time:0.2f} seconds" 

427 ) 

428 frames = self.spectral_processing_parameters.averages - np.sum(exclude_averages) 

429 

430 else: # For exponential averaging 

431 for frame in data: 

432 response_fft, reference_fft = frame 

433 

434 # Compute response spectra 

435 response_spectral_time = time.time() 

436 if self.spectral_processing_parameters.requires_full_spectral_response: 

437 self.log("Computing Full Spectral Response Matrix") 

438 if self.response_spectral_matrix is None: 

439 self.response_spectral_matrix = np.einsum( 

440 "if,jf->fij", response_fft, np.conj(response_fft) 

441 ) 

442 else: 

443 self.response_spectral_matrix = ( 

444 self.spectral_processing_parameters.exponential_averaging_coefficient 

445 * np.einsum("if,jf->fij", response_fft, np.conj(response_fft)) 

446 + ( 

447 1 

448 - self.spectral_processing_parameters.exponential_averaging_coefficient 

449 ) 

450 * self.response_spectral_matrix 

451 ) 

452 # Get the diagonal matrix as well 

453 self.response_diagonal_matrix = np.einsum( 

454 "fii->fi", self.response_spectral_matrix 

455 ) 

456 elif self.spectral_processing_parameters.requires_diagonal_spectral_response: 

457 self.log("Computing Diagonal of Spectral Response Matrix") 

458 if self.response_diagonal_matrix is None: 

459 self.response_diagonal_matrix = np.einsum( 

460 "if,if->fi", response_fft, np.conj(response_fft) 

461 ) 

462 else: 

463 self.response_diagonal_matrix = ( 

464 self.spectral_processing_parameters.exponential_averaging_coefficient 

465 * np.einsum("if,if->fi", response_fft, np.conj(response_fft)) 

466 + ( 

467 1 

468 - self.spectral_processing_parameters.exponential_averaging_coefficient 

469 ) 

470 * self.response_diagonal_matrix 

471 ) 

472 if ( 

473 self.spectral_processing_parameters.requires_full_spectral_response 

474 or self.spectral_processing_parameters.requires_diagonal_spectral_response 

475 ): 

476 self.log( 

477 "Computed Response Spectral Matrix in " 

478 f"{time.time() - response_spectral_time:0.2f} seconds" 

479 ) 

480 

481 # Compute the reference spectra 

482 reference_spectral_time = time.time() 

483 if self.spectral_processing_parameters.requires_full_spectral_reference: 

484 self.log("Computing Full Spectral Reference Matrix") 

485 if self.reference_spectral_matrix is None: 

486 self.reference_spectral_matrix = np.einsum( 

487 "if,jf->fij", reference_fft, np.conj(reference_fft) 

488 ) 

489 else: 

490 self.reference_spectral_matrix = ( 

491 self.spectral_processing_parameters.exponential_averaging_coefficient 

492 * np.einsum("if,jf->fij", reference_fft, np.conj(reference_fft)) 

493 + ( 

494 1 

495 - self.spectral_processing_parameters.exponential_averaging_coefficient 

496 ) 

497 * self.reference_spectral_matrix 

498 ) 

499 # Get the diagonal matrix as well 

500 self.reference_diagonal_matrix = np.einsum( 

501 "fii->fi", self.reference_spectral_matrix 

502 ) 

503 elif self.spectral_processing_parameters.requires_diagonal_spectral_reference: 

504 self.log("Computing Diagonal of Spectral Reference Matrix") 

505 if self.reference_diagonal_matrix is None: 

506 self.reference_diagonal_matrix = np.einsum( 

507 "if,if->fi", reference_fft, np.conj(reference_fft) 

508 ) 

509 else: 

510 self.reference_diagonal_matrix = ( 

511 self.spectral_processing_parameters.exponential_averaging_coefficient 

512 * np.einsum("if,if->fi", reference_fft, np.conj(reference_fft)) 

513 + ( 

514 1 

515 - self.spectral_processing_parameters.exponential_averaging_coefficient 

516 ) 

517 * self.reference_diagonal_matrix 

518 ) 

519 if ( 

520 self.spectral_processing_parameters.requires_full_spectral_reference 

521 or self.spectral_processing_parameters.requires_diagonal_spectral_reference 

522 ): 

523 self.log( 

524 "Computed Reference Spectral Matrix in " 

525 f"{time.time() - reference_spectral_time:0.2f} seconds" 

526 ) 

527 

528 # Compute reference and response cross spectra 

529 if self.spectral_processing_parameters.requires_spectral_reference_response: 

530 cross_spectral_time = time.time() 

531 self.log("Computing Full Cross Spectral Response/Reference Matrix") 

532 if self.response_reference_spectral_matrix is None: 

533 self.response_reference_spectral_matrix = np.einsum( 

534 "if,jf->fij", response_fft, np.conj(reference_fft) 

535 ) 

536 else: 

537 self.response_reference_spectral_matrix = ( 

538 self.spectral_processing_parameters.exponential_averaging_coefficient 

539 * np.einsum("if,jf->fij", response_fft, np.conj(reference_fft)) 

540 + ( 

541 1 

542 - self.spectral_processing_parameters.exponential_averaging_coefficient 

543 ) 

544 * self.response_reference_spectral_matrix 

545 ) 

546 self.log( 

547 "Computed Crossspectral Matrix in " 

548 f"{time.time() - cross_spectral_time:0.2f} seconds" 

549 ) 

550 self.frames_computed += 1 

551 

552 frames = self.frames_computed 

553 self.log( 

554 f"Computed Spectral Matrices for {frames} frames in " 

555 f"{time.time() - response_spectral_time:0.2f} seconds" 

556 ) 

557 gffpinv = None 

558 gfxpinv = None 

559 if self.spectral_processing_parameters.compute_frf: 

560 frf_time = time.time() 

561 if self.spectral_processing_parameters.frf_estimator == Estimator.H1: 

562 if gffpinv is None: 

563 gffpinv = np.linalg.pinv( 

564 self.reference_spectral_matrix, rcond=1e-12, hermitian=True 

565 ) 

566 frf = self.response_reference_spectral_matrix @ gffpinv 

567 elif self.spectral_processing_parameters.frf_estimator == Estimator.H2: 

568 gfx = self.response_reference_spectral_matrix.conj().transpose(0, 2, 1) 

569 gfxpinv = np.linalg.pinv(gfx, rcond=1e-12, hermitian=True) 

570 frf = self.response_spectral_matrix @ gfxpinv 

571 elif self.spectral_processing_parameters.frf_estimator == Estimator.H3: 

572 if gffpinv is None: 

573 gffpinv = np.linalg.pinv( 

574 self.reference_spectral_matrix, rcond=1e-12, hermitian=True 

575 ) 

576 gfx = self.response_reference_spectral_matrix.conj().transpose(0, 2, 1) 

577 if gfxpinv is None: 

578 gfxpinv = np.linalg.pinv(gfx, rcond=1e-12, hermitian=True) 

579 frf = ( 

580 self.response_spectral_matrix @ gfxpinv 

581 + self.response_reference_spectral_matrix @ gffpinv 

582 ) / 2 

583 elif self.spectral_processing_parameters.frf_estimator == Estimator.HV: 

584 gxx = self.response_diagonal_matrix.T[..., np.newaxis, np.newaxis] 

585 gxf = np.einsum("fij->ifj", self.response_reference_spectral_matrix)[ 

586 ..., np.newaxis, : 

587 ] 

588 gff = self.reference_spectral_matrix 

589 gff = np.broadcast_to(gff, gxx.shape[:-2] + gff.shape[-2:]) 

590 gffx = np.block([[gff, np.conj(np.moveaxis(gxf, -2, -1))], [gxf, gxx]]) 

591 # Compute eigenvalues 

592 _, evect = np.linalg.eigh(np.moveaxis(gffx, -2, -1)) 

593 # Get the evect corresponding to the minimum eigenvalue 

594 evect = evect[..., 0] # Assumes evals are sorted ascending 

595 frf = np.moveaxis( 

596 -evect[..., :-1] / evect[..., -1:], # Scale so last value is -1 

597 -3, 

598 -2, 

599 ) 

600 else: 

601 raise ValueError(f"Invalid frf_estimator {Estimator.H1}") 

602 self.log(f"Computed FRF in {time.time() - frf_time:0.2f} seconds") 

603 cond_time = time.time() 

604 frf_condition = np.linalg.cond(frf) 

605 self.log(f"Computed FRF Condition Number in {time.time() - cond_time:0.2f} seconds") 

606 else: 

607 frf = None 

608 frf_condition = None 

609 if self.spectral_processing_parameters.compute_coherence: 

610 coh_time = time.time() 

611 if gffpinv is None: 

612 gffpinv = np.linalg.pinv( 

613 self.reference_spectral_matrix, rcond=1e-12, hermitian=True 

614 ) 

615 coherence = ( 

616 np.einsum( 

617 "fij,fjk,fik->fi", 

618 self.response_reference_spectral_matrix, 

619 gffpinv, 

620 self.response_reference_spectral_matrix.conj(), 

621 ) 

622 / self.response_diagonal_matrix 

623 ).real 

624 self.log(f"Computed Coherence in {time.time() - coh_time:0.2f} seconds") 

625 else: 

626 coherence = None 

627 if self.spectral_processing_parameters.compute_cpsd: 

628 cpsd_time = time.time() 

629 reference_spectral_matrix = self.reference_spectral_matrix.copy() 

630 response_spectral_matrix = self.response_spectral_matrix.copy() 

631 # Normalize 

632 response_spectral_matrix *= ( 

633 # Window correction was done in the data collector 

634 self.spectral_processing_parameters.frequency_spacing 

635 / self.spectral_processing_parameters.sample_rate**2 

636 ) 

637 response_spectral_matrix[1:-1] *= 2 

638 reference_spectral_matrix *= ( 

639 # Window correction was done in the data collector 

640 self.spectral_processing_parameters.frequency_spacing 

641 / self.spectral_processing_parameters.sample_rate**2 

642 ) 

643 reference_spectral_matrix[1:-1] *= 2 

644 self.log(f"Computed CPSDs in {time.time() - cpsd_time:0.2f} seconds") 

645 elif self.spectral_processing_parameters.compute_apsd: 

646 apsd_time = time.time() 

647 reference_spectral_matrix = self.reference_diagonal_matrix.copy() 

648 response_spectral_matrix = self.response_diagonal_matrix.copy() 

649 # Normalize 

650 response_spectral_matrix *= ( 

651 # Window correction was done in the data collector 

652 self.spectral_processing_parameters.frequency_spacing 

653 / self.spectral_processing_parameters.sample_rate**2 

654 ) 

655 response_spectral_matrix[1:-1] *= 2 

656 reference_spectral_matrix *= ( 

657 # Window correction was done in the data collector 

658 self.spectral_processing_parameters.frequency_spacing 

659 / self.spectral_processing_parameters.sample_rate**2 

660 ) 

661 reference_spectral_matrix[1:-1] *= 2 

662 self.log(f"Computed APSDs in {time.time() - apsd_time:0.2f} seconds") 

663 else: 

664 response_spectral_matrix = None 

665 reference_spectral_matrix = None 

666 frequencies = ( 

667 np.arange(self.spectral_processing_parameters.num_frequency_lines) 

668 * self.spectral_processing_parameters.frequency_spacing 

669 ) 

670 self.log("Sending Updated Spectral Data") 

671 self.data_out_queue.put( 

672 ( 

673 frames, 

674 frequencies, 

675 frf, 

676 coherence, 

677 response_spectral_matrix, 

678 reference_spectral_matrix, 

679 frf_condition, 

680 ) 

681 ) 

682 # Keep running 

683 self.command_queue.put( 

684 self.process_name, 

685 (SpectralProcessingCommands.RUN_SPECTRAL_PROCESSING, None), 

686 ) 

687 

688 def clear_spectral_processing(self, data): # pylint: disable=unused-argument 

689 """Clears all data in the buffer so the FRF starts fresh from new data 

690 

691 Parameters 

692 ---------- 

693 data : Ignored 

694 This parameter is not used by the function but must be present 

695 due to the calling signature of functions called through the 

696 ``command_map`` 

697 

698 """ 

699 self.frames_computed = 0 

700 self.response_spectral_matrix = None 

701 self.reference_spectral_matrix = None 

702 self.response_reference_spectral_matrix = None 

703 if self.spectral_processing_parameters.averaging_type == AveragingTypes.LINEAR: 

704 self.response_fft[:] = np.nan 

705 self.reference_fft[:] = np.nan 

706 else: 

707 self.response_fft = None 

708 self.reference_fft = None 

709 

710 def stop_spectral_processing(self, data): 

711 """Stops computing FRFs from time data. 

712 

713 Parameters 

714 ---------- 

715 data : Ignored 

716 This parameter is not used by the function but must be present 

717 due to the calling signature of functions called through the 

718 ``command_map`` 

719 

720 """ 

721 time.sleep(WAIT_TIME) 

722 flushed_data = self.command_queue.flush(self.process_name) 

723 # Put back any quit message that may have been pulled off 

724 for message, data in flushed_data: 

725 if message == GlobalCommands.QUIT: 

726 self.command_queue.put(self.process_name, (message, data)) 

727 flush_queue(self.data_out_queue) 

728 self.environment_command_queue.put( 

729 self.process_name, (SpectralProcessingCommands.SHUTDOWN_ACHIEVED, None) 

730 ) 

731 

732 

733def spectral_processing_process( 

734 environment_name: str, 

735 command_queue: VerboseMessageQueue, 

736 data_in_queue: mp.queues.Queue, 

737 data_out_queue: mp.queues.Queue, 

738 environment_command_queue: VerboseMessageQueue, 

739 gui_update_queue: mp.queues.Queue, 

740 log_file_queue: mp.queues.Queue, 

741 process_name=None, 

742): 

743 """Function passed to multiprocessing as the FRF computation process 

744 

745 This process creates the ``FRFComputationProcess`` object and calls the 

746 ``run`` function. 

747 

748 

749 Parameters 

750 ---------- 

751 environment_name : str : 

752 Name of the environment that this subprocess belongs to. 

753 command_queue : VerboseMessageQueue : 

754 The queue containing instructions for the FRF process 

755 data_for_frf_queue : mp.queues.Queue : 

756 Queue containing input data for the FRF computation 

757 updated_frf_queue : mp.queues.Queue : 

758 Queue where frf process will put computed frfs 

759 gui_update_queue : mp.queues.Queue : 

760 Queue for gui updates 

761 log_file_queue : mp.queues.Queue : 

762 Queue for writing to the log file 

763 

764 """ 

765 

766 spectral_processing_instance = SpectralProcessingProcess( 

767 ( 

768 environment_name + " Spectral Processing Computation" 

769 if process_name is None 

770 else process_name 

771 ), 

772 command_queue, 

773 data_in_queue, 

774 data_out_queue, 

775 environment_command_queue, 

776 gui_update_queue, 

777 log_file_queue, 

778 environment_name, 

779 ) 

780 spectral_processing_instance.run()