Coverage for src/sdynpy/fileio/sdynpy_escdf.py: 9%

1081 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-18 21:32 +0000

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

2""" 

3Created on Fri Feb 7 10:47:58 2025 

4 

5@author: dprohe 

6""" 

7 

8from ..core.sdynpy_geometry import (Geometry,NodeArray,CoordinateSystemArray, 

9 coordinate_system_array,node_array, 

10 global_coord) 

11from ..core.sdynpy_shape import ShapeArray,shape_array 

12from ..core.sdynpy_data import data_array,FunctionTypes,NDDataArray,GUIPlot,join 

13from ..core.sdynpy_colors import color_list 

14from ..core.sdynpy_coordinate import coordinate_array 

15from .sdynpy_rattlesnake import (read_rattlesnake_output,read_system_id_nc4, 

16 read_modal_data,read_random_spectral_data, 

17 read_transient_control_data) 

18 

19from qtpy.QtWidgets import (QMainWindow) 

20from qtpy import QtWidgets, uic, QtGui, QtCore 

21import netCDF4 as nc4 

22import os 

23import numpy as np 

24 

25try: 

26 import escdf 

27except ImportError: 

28 escdf = None 

29 

30_element_types = { 

31 11: 'bar2',# 'Rod', 

32 21: 'bar2',# 'Linear beam', 

33 22: 'bar2',# 'Tapered beam', 

34 23: 'bar2',# 'Curved beam', 

35 24: 'bar3',# 'Parabolic beam', 

36 31: 'bar2',# 'Straight pipe', 

37 32: 'bar2',# 'Curved pipe', 

38 41: 'tri3',#'Plane Stress Linear Triangle', 

39 42: 'tri6',#'Plane Stress Parabolic Triangle', 

40 43: None,#Plane Stress Cubic Triangle', 

41 44: 'quad4',#'Plane Stress Linear Quadrilateral', 

42 45: 'quad8',#'Plane Stress Parabolic Quadrilateral', 

43 46: None,#'Plane Strain Cubic Quadrilateral', 

44 51: 'tri3',#'Plane Strain Linear Triangle', 

45 52: 'tri6',#'Plane Strain Parabolic Triangle', 

46 53: None,#'Plane Strain Cubic Triangle', 

47 54: 'quad4',#'Plane Strain Linear Quadrilateral', 

48 55: 'quad8',#'Plane Strain Parabolic Quadrilateral', 

49 56: None,#'Plane Strain Cubic Quadrilateral', 

50 61: 'tri3',#'Plate Linear Triangle', 

51 62: 'tri6',#'Plate Parabolic Triangle', 

52 63: None,#'Plate Cubic Triangle', 

53 64: 'quad4',#'Plate Linear Quadrilateral', 

54 65: 'quad8',#'Plate Parabolic Quadrilateral', 

55 66: None,#'Plate Cubic Quadrilateral', 

56 71: 'quad4',#'Membrane Linear Quadrilateral', 

57 72: 'quad8',#'Membrane Parabolic Triangle', 

58 73: None,#'Membrane Cubic Triangle', 

59 74: 'tri3',#'Membrane Linear Triangle', 

60 75: 'quad8',#'Membrane Parabolic Quadrilateral', 

61 76: None,#'Membrane Cubic Quadrilateral', 

62 81: 'tri3',#'Axisymetric Solid Linear Triangle', 

63 82: 'tri6',#'Axisymetric Solid Parabolic Triangle', 

64 84: None,#'Axisymetric Solid Linear Quadrilateral', 

65 85: None,#'Axisymetric Solid Parabolic Quadrilateral', 

66 91: 'tri3',#'Thin Shell Linear Triangle', 

67 92: 'tri6',#'Thin Shell Parabolic Triangle', 

68 93: None,#'Thin Shell Cubic Triangle', 

69 94: 'quad4',#'Thin Shell Linear Quadrilateral', 

70 95: 'quad8',#'Thin Shell Parabolic Quadrilateral', 

71 96: None,#'Thin Shell Cubic Quadrilateral', 

72 101: None,#'Thick Shell Linear Wedge', 

73 102: None,#'Thick Shell Parabolic Wedge', 

74 103: None,#'Thick Shell Cubic Wedge', 

75 104: 'quad4',#'Thick Shell Linear Brick', 

76 105: 'quad8',#'Thick Shell Parabolic Brick', 

77 106: None,#'Thick Shell Cubic Brick', 

78 111: 'tet4',#'Solid Linear Tetrahedron', 

79 112: 'wedge6',#'Solid Linear Wedge', 

80 113: 'wedge15',#'Solid Parabolic Wedge', 

81 114: None,#'Solid Cubic Wedge', 

82 115: 'hex8',#'Solid Linear Brick', 

83 116: 'hex20',#'Solid Parabolic Brick', 

84 117: None,#'Solid Cubic Brick', 

85 118: 'tet10',#'Solid Parabolic Tetrahedron', 

86 121: 'bar2',#'Rigid Bar', 

87 122: None,#'Rigid Element', 

88 136: None,#'Node To Node Translational Spring', 

89 137: None,#'Node To Node Rotational Spring', 

90 138: None,#'Node To Ground Translational Spring', 

91 139: None,#'Node To Ground Rotational Spring', 

92 141: None,#'Node To Node Damper', 

93 142: None,#'Node To Gound Damper', 

94 151: None,#'Node To Node Gap', 

95 152: None,#'Node To Ground Gap', 

96 161: 'sphere1',#'Lumped Mass', 

97 171: None,#'Axisymetric Linear Shell', 

98 172: None,#'Axisymetric Parabolic Shell', 

99 181: None,#'Constraint', 

100 191: None,#'Plastic Cold Runner', 

101 192: None,#'Plastic Hot Runner', 

102 193: None,#'Plastic Water Line', 

103 194: None,#'Plastic Fountain', 

104 195: None,#'Plastic Baffle', 

105 196: None,#'Plastic Rod Heater', 

106 201: None,#'Linear node-to-node interface', 

107 202: None,#'Linear edge-to-edge interface', 

108 203: None,#'Parabolic edge-to-edge interface', 

109 204: None,#'Linear face-to-face interface', 

110 208: None,#'Parabolic face-to-face interface', 

111 212: None,#'Linear axisymmetric interface', 

112 213: None,#'Parabolic axisymmetric interface', 

113 221: None,#'Linear rigid surface', 

114 222: None,#'Parabolic rigid surface', 

115 231: None,#'Axisymetric linear rigid surface', 

116 232: None,#'Axisymentric parabolic rigid surface' 

117 } 

118 

119_inverse_element_types = { 

120 'sphere1':161, 

121 'bar2':21, 

122 'bar3':24, 

123 'tri3':41, 

124 'tri6':42, 

125 'quad4':44, 

126 'quad8':45, 

127 'tet4':111, 

128 'tet10':118, 

129 'hex8':115, 

130 'hex20':116, 

131 'wedge6':112, 

132 'wedge15':113 

133 } 

134 

135def to_geometry(geometry_dataset): 

136 if not (geometry_dataset.istype("geometry") or geometry_dataset.istype("point_cloud")): 

137 raise ValueError( 

138 "geometry_dataset must have be an ESCDF `geometry` or `point_cloud` dataset." 

139 ) 

140 if geometry_dataset.istype("geometry"): 

141 element_type_string = None 

142 nodes = [] 

143 css = [coordinate_system_array()] 

144 for id, position, x, y, z in zip( 

145 geometry_dataset.node_id, 

146 geometry_dataset.node_position, 

147 geometry_dataset.node_x_direction, 

148 geometry_dataset.node_y_direction, 

149 geometry_dataset.node_z_direction, 

150 ): 

151 nodes.append(node_array(id, position, def_cs=1, disp_cs=id + 1, color=0)) 

152 css.append( 

153 coordinate_system_array( 

154 id + 1, "Node {:} Disp CS".format(id), matrix=np.array((x, y, z, np.zeros(3))) 

155 ) 

156 ) 

157 nodes = np.array(nodes).view(NodeArray) 

158 css = np.array(css).view(CoordinateSystemArray) 

159 geometry = Geometry(nodes, css) 

160 try: 

161 for i, connection in enumerate(geometry_dataset.line_connection): 

162 try: 

163 color = geometry_dataset.line_color[i, :] 

164 color_index = np.argmin(np.linalg.norm(color / 255 - color_list, axis=-1)) 

165 except TypeError: 

166 color_index = 0 

167 geometry.add_traceline(connection, color=color_index) 

168 except TypeError: 

169 # No tracelines 

170 pass 

171 

172 try: 

173 for i, connection in enumerate(geometry_dataset.element_connection): 

174 try: 

175 color = geometry_dataset.element_color[i, :] 

176 color_index = np.argmin(np.linalg.norm(color / 255 - color_list, axis=-1)) 

177 except TypeError: 

178 color_index = 0 

179 type_name = geometry_dataset.element_type[i] 

180 # try: 

181 # type_name = geometry_dataset.element_type[i][()] 

182 # except TypeError as e: 

183 # try: 

184 # type_name = geometry_dataset.element_type[i] 

185 # except Exception: 

186 # raise ValueError('Could not parse type_name') from e 

187 try: 

188 element_type = _inverse_element_types[type_name.lower()] 

189 except KeyError: 

190 if element_type_string is None: 

191 element_type_string = { 

192 val.lower(): key for key, val in _element_types.items() 

193 } 

194 try: 

195 element_type = element_type_string[type_name.lower()] 

196 except KeyError: 

197 print( 

198 "Unknown Element Type at index {:}, {:}. Skipping...".format( 

199 i, type_name 

200 ) 

201 ) 

202 continue 

203 geometry.add_element(element_type, connection, color=color_index) 

204 except TypeError as e: 

205 # No elements 

206 print(f'there was an error {e}') 

207 pass 

208 return geometry 

209 elif geometry_dataset.istype("point_cloud"): 

210 element_type_string = None 

211 nodes = [] 

212 css = [coordinate_system_array()] 

213 if geometry_dataset.node_id is None: 

214 node_id = np.arange(1, geometry_dataset.node_position.shape[0] + 1) 

215 else: 

216 node_id = geometry_dataset.node_id[...] 

217 for id, position in zip(node_id, geometry_dataset.node_position): 

218 nodes.append(node_array(id, position, def_cs=1, disp_cs=1, color=0)) 

219 nodes = np.array(nodes).view(NodeArray) 

220 css = np.array(css).view(CoordinateSystemArray) 

221 

222 geometry = Geometry(nodes, css) 

223 return geometry 

224 else: 

225 raise ValueError( 

226 "geometry_dataset must have be an ESCDF `geometry` or `point_cloud` dataset." 

227 ) 

228 

229def from_geometry(dataset_name, geometry : Geometry, descriptive_name = ''): 

230 if escdf is None: 

231 raise ImportError('Could not import module `escdf`, unable to create dataset.') 

232 esgeo = escdf.classes.geometry(dataset_name,descriptive_name = descriptive_name) 

233 esgeo.node_id = geometry.node.id 

234 esgeo.node_position = geometry.global_node_coordinate(geometry.node.id) 

235 esgeo.node_x_direction = geometry.global_deflection( 

236 coordinate_array(geometry.node.id,'X+')) 

237 esgeo.node_y_direction = geometry.global_deflection( 

238 coordinate_array(geometry.node.id,'Y+')) 

239 esgeo.node_z_direction = geometry.global_deflection( 

240 coordinate_array(geometry.node.id,'Z+')) 

241 if geometry.traceline.size > 0: 

242 esgeo.line_color = [np.array(color_list[i])*255 for i in geometry.traceline.color] 

243 esgeo.line_connection = geometry.traceline.connectivity 

244 if geometry.element.size > 0: 

245 esgeo.element_connection = geometry.element.connectivity 

246 esgeo.element_color = [np.array(color_list[i])*255 for i in geometry.element.color] 

247 esgeo.element_type = [_element_types[t] for t in geometry.element.type] 

248 return esgeo 

249 

250def to_shape(shape_dataset): 

251 if not shape_dataset.istype('mode'): 

252 raise ValueError('shape_dataset must be an ESCDF `mode` dataset.') 

253 coordinate = coordinate_array(string_array=shape_dataset.dof_name[...]) 

254 shape_matrix = shape_dataset.shape[...].T 

255 frequency = shape_dataset.frequency[...] 

256 damping = shape_dataset.damping_ratio[...] 

257 try: 

258 modal_mass = shape_dataset.modal_mass[...] 

259 except TypeError: 

260 modal_mass = 1 

261 try: 

262 description = shape_dataset.description[...] 

263 except TypeError: 

264 description = '' 

265 return shape_array(coordinate,shape_matrix,frequency,damping,modal_mass, 

266 description) 

267 

268def from_shape(dataset_name, shape : ShapeArray, descriptive_name = ''): 

269 if escdf is None: 

270 raise ImportError('Could not import module `escdf`, unable to create dataset.') 

271 shape = shape.flatten() 

272 modes = escdf.classes.mode(dataset_name,descriptive_name = descriptive_name) 

273 modes.damping_ratio = shape.damping 

274 modes.frequency = shape.frequency 

275 dofs = np.unique(shape.coordinate) 

276 modes.shape = shape[dofs].T 

277 modes.dof_name = dofs.string_array() 

278 modes.modal_mass = shape.modal_mass 

279 if not np.all(shape.comment1 == ''): 

280 modes.description = shape.comment1.astype(object) 

281 return modes 

282 

283datatype_enum_names = { 

284 FunctionTypes.TIME_RESPONSE:'time response', 

285 FunctionTypes.CROSSSPECTRUM:'power spectrum', 

286 FunctionTypes.FREQUENCY_RESPONSE_FUNCTION:'frequency response function', 

287 FunctionTypes.TRANSMISIBILITY:'transmissibility', 

288 FunctionTypes.COHERENCE:'coherence', 

289 FunctionTypes.CROSSCORRELATION:'correlation', 

290 FunctionTypes.POWER_SPECTRAL_DENSITY:'power spectral density', 

291 FunctionTypes.SPECTRUM:'spectrum', 

292 FunctionTypes.MODE_INDICATOR_FUNCTION:'mode indicator function', 

293 FunctionTypes.PARTIAL_COHERENCE:'partial coherence', 

294 FunctionTypes.SHOCK_RESPONSE_SPECTRUM:'shock response spectrum', 

295 FunctionTypes.IMPULSE_RESPONSE_FUNCTION:'impulse response function', 

296 FunctionTypes.MULTIPLE_COHERENCE:'multiple coherence'} 

297 

298datatype_names = { 

299 FunctionTypes.GENERAL: 

300 ['general','none'], 

301 FunctionTypes.TIME_RESPONSE: 

302 ['time history','time response', 'th', 'time data', 'time', 

303 'time histories','time responses', 'ths', 'times',], 

304 FunctionTypes.AUTOSPECTRUM: 

305 ['autospectrum','as','autospectra'], 

306 FunctionTypes.CROSSSPECTRUM: 

307 ['crossspectrum','cs','crossspectra'], 

308 FunctionTypes.FREQUENCY_RESPONSE_FUNCTION: 

309 ['frf','frequency response','frequency response function', 

310 'frfs','frequency responses', 'frequency response functions', 

311 'transfer function','transfer functions', 'tf'], 

312 FunctionTypes.TRANSMISIBILITY: 

313 ['transmisibility','transmisibility function', 

314 'transmisibilities','transmisibility functions'], 

315 FunctionTypes.COHERENCE:['coherence','coh'], 

316 FunctionTypes.AUTOCORRELATION: 

317 ['corr','correlation','autocorrelation','corrs','correlations', 

318 'autocorrelations'], 

319 FunctionTypes.CROSSCORRELATION: 

320 ['ccorr','crosscorrelation''ccorrs', 

321 'crosscorrelations'], 

322 FunctionTypes.POWER_SPECTRAL_DENSITY: 

323 ['power spectral density','power spectral densities','psd','psds', 

324 'apsd','apsds','cpsd','cpsds','autopower spectral density', 

325 'autopower spectral densities','crosspower spectral density', 

326 'crosspower spectral densities'], 

327 FunctionTypes.ENERGY_SPECTRAL_DENSITY: 

328 ['esd','energy spectral density','energy spectral densities','esds'], 

329 FunctionTypes.PROBABILITY_DENSITY_FUNCTION: 

330 ['pdf','pdfs','probability density','probability densities', 

331 'probability density function','probability density functions'], 

332 FunctionTypes.SPECTRUM: 

333 ['fft','ffts','spectrum','spectra'], 

334 FunctionTypes.CUMULATIVE_FREQUENCY_DISTRIBUTION: 

335 ['cfd','cfds','cumulative frequency distribution', 

336 'cumulative frequency distributions'], 

337 FunctionTypes.PEAKS_VALLEY: 

338 ['peaks valley'], 

339 FunctionTypes.STRESS_PER_CYCLE: 

340 ['stress per cycle','stress/cycle'], 

341 FunctionTypes.STRAIN_PER_CYCLE: 

342 ['strain per cycle','strain/cycle'], 

343 FunctionTypes.ORBIT:['orbit','orbits'], 

344 FunctionTypes.MODE_INDICATOR_FUNCTION: 

345 ['mif','cmif','mmif','nmif','mode indicator function','mode indicator functions', 

346 'complex mode indicator function','complex mode indicator functions', 

347 'multimode indicator function','multimode indicator functions', 

348 'normal mode indicator function normal mode indicator functions'], 

349 FunctionTypes.FORCE_PATTERN:['force_pattern'], 

350 FunctionTypes.PARTIAL_POWER:['partial power'], 

351 FunctionTypes.PARTIAL_COHERENCE:['partial coherence','partial coh','pcoh'], 

352 FunctionTypes.EIGENVALUE:['eigenvalue','eigenvalues'], 

353 FunctionTypes.EIGENVECTOR:['eigenvector','eigenvectors'], 

354 FunctionTypes.SHOCK_RESPONSE_SPECTRUM: 

355 ['shock response spectrum','shock response spectra','srs','srss', 'response spectrum'], 

356 FunctionTypes.FINITE_IMPULSE_RESPONSE_FILTER: 

357 ['finite impulse response function','firf'], 

358 FunctionTypes.MULTIPLE_COHERENCE: 

359 ['mcoh','multiple coherence'], 

360 FunctionTypes.ORDER_FUNCTION:['order','order function','order functions'], 

361 FunctionTypes.PHASE_COMPENSATION:['phase comp','phase compensation'], 

362 FunctionTypes.IMPULSE_RESPONSE_FUNCTION: 

363 ['irf','irfs','impulse response function','impulse response functions', 

364 'impuse response','impulse responses'] 

365 } 

366 

367datatype_map = {} 

368for key,name_list in datatype_names.items(): 

369 for name in name_list: 

370 datatype_map[name.replace(' ','').replace('_','').replace('-','').lower()] = key 

371 

372def to_data(data_dataset): 

373 if not data_dataset.istype('data'): 

374 raise ValueError('data_dataset must be an ESCDF `data` dataset.') 

375 try: 

376 function_type = datatype_map[ 

377 data_dataset.data_type[...].replace(' ','').replace('_','').replace('-','').lower()] 

378 except KeyError: 

379 raise ValueError('Unknown data type {:}'.format(data_dataset.data_type[...])) 

380 coordinate = coordinate_array(string_array=data_dataset.channel[...]) 

381 ordinate_unit = np.char.add('Ordinate Unit: ', data_dataset.ordinate_unit[...]) 

382 abscissa_unit = np.char.add(' -- Abscissa Unit: ', data_dataset.abscissa_unit[...]) 

383 comment1 = np.char.add(ordinate_unit,abscissa_unit) 

384 try: 

385 abscissa = np.arange(data_dataset.ordinate.shape[-1])*data_dataset.abscissa_step[...] + data_dataset.abscissa_start[...] 

386 except TypeError: 

387 abscissa = data_dataset.abscissa[...] 

388 data = data_array(function_type, abscissa, data_dataset.ordinate[...], 

389 coordinate, comment1) 

390 return data 

391 

392def from_data(dataset_name, data : NDDataArray,descriptive_name=''): 

393 if escdf is None: 

394 raise ImportError('Could not import module `escdf`, unable to create dataset.') 

395 data = data.flatten() 

396 try: 

397 abscissa_spacing = data.abscissa_spacing 

398 except ValueError: 

399 abscissa_spacing = None 

400 starting_abscissa = data.abscissa[...,0] 

401 if not np.all(starting_abscissa == starting_abscissa[0]): 

402 starting_abscissa = None 

403 else: 

404 starting_abscissa = starting_abscissa[0] 

405 dataset = escdf.classes.data(dataset_name,descriptive_name = descriptive_name) 

406 if starting_abscissa is None or abscissa_spacing is None: 

407 if np.all(data.abscissa == data.abscissa[0]): 

408 abscissa = data.abscissa[0] 

409 else: 

410 abscissa = data.abscissa 

411 dataset.abscissa = abscissa 

412 else: 

413 dataset.abscissa_step = abscissa_spacing 

414 dataset.abscissa_start = starting_abscissa 

415 dataset.ordinate = data.ordinate 

416 dataset.data_type = datatype_enum_names[data.function_type] 

417 dataset.channel = data.coordinate.string_array().astype(object) 

418 return dataset 

419 

420def from_rattlesnake_channel_info(dataset_name, channel_info, descriptive_name=''): 

421 """Generates a "vibration_channel_table" dataset from the channel information 

422 in a Rattlesnake netcdf4 file. 

423 

424 Operates on channel tables like the one from  

425 

426 Parameters 

427 ---------- 

428 dataset_name : str 

429 The name of the dataset to be generated 

430 channel_info : pandas.DataFrame 

431 A dataframe returned from reading Rattlesnake netcdf4 output into SDynPy 

432 descriptive_name : str 

433 A description of the dataset to be generated 

434 

435 Returns 

436 ------- 

437 escdf.Dataset 

438 A dataset with the type 'vibration_channel_table' containing channel 

439 table information 

440 """ 

441 if escdf is None: 

442 raise ImportError('Could not import module `escdf`, unable to create dataset.') 

443 

444 escdf_channels = escdf.Dataset(dataset_name,'vibration_channel_table', 

445 descriptive_name) 

446 

447 escdf_channels.coupling = [v.lower() for v in channel_info['coupling']] 

448 escdf_channels.daq = [daq+'/'+channel for daq,channel in zip(channel_info['physical_device'],channel_info['physical_channel'])] 

449 escdf_channels.data_type = [v.lower() for v in channel_info['channel_type']] 

450 escdf_channels.node_direction = channel_info['node_direction'] 

451 escdf_channels.node_id = channel_info['node_number'] 

452 escdf_channels.sensitivity = channel_info['sensitivity'] 

453 escdf_channels.sensitivity_unit = channel_info['unit'] 

454 escdf_channels.make = channel_info['make'] 

455 escdf_channels.model = channel_info['model'] 

456 escdf_channels.serial_number = [serial_number+triax_dof for serial_number,triax_dof in zip(channel_info['serial_number'],channel_info['triax_dof'])] 

457 

458 return escdf_channels 

459 

460def from_rattlesnake_modal_parameters(dataset_name, modal_dataset, environment_name = None, descriptive_name=''): 

461 """Generates a "modal_test_parameters" object from modal output 

462 

463 Parameters 

464 ---------- 

465 dataset_name : str 

466 The name of the dataset to be generated 

467 modal_dataset : netCDF4.Dataset 

468 A dataset produced by loading the netcdf4 output file. 

469 environment_name : str, optional 

470 Name of the environment. Must be specified if more than one environment 

471 exists in the file 

472 descriptive_name : str, optional 

473 A description of the generated dataset, by default ''. 

474 

475 Returns 

476 ------- 

477 escdf.Dataset 

478 A dataset with the type "modal_test_parameters" containing 

479 modal parameters 

480 """ 

481 if escdf is None: 

482 raise ImportError('Could not import module `escdf`, unable to create dataset.') 

483 

484 environment_names = [name for name in modal_dataset.groups if name != 'channels'] 

485 if environment_name is None: 

486 environment_names = [name for name in modal_dataset.groups if name != 'channels'] 

487 if len(environment_names) != 1: 

488 raise ValueError('Found {:} environments in the file. Could not select correctly.'.format(len(environment_names))) 

489 environment_name = environment_names[0] 

490 modal_parameters = escdf.Dataset(dataset_name, 'modal_test_parameters', descriptive_name) 

491 modal_parameters.sample_rate = modal_dataset.sample_rate 

492 modal_parameters.samples_per_frame = modal_dataset[environment_name].samples_per_frame 

493 modal_parameters.averaging_type = modal_dataset[environment_name].averaging_type.lower() 

494 modal_parameters.averages = modal_dataset[environment_name].num_averages 

495 if modal_dataset[environment_name].averaging_type.lower() == 'exponential': 

496 modal_parameters.averaging_coefficient = modal_dataset[environment_name].averaging_coefficient 

497 modal_parameters.overlap = modal_dataset[environment_name].overlap 

498 modal_parameters.window = modal_dataset[environment_name].frf_window.lower() 

499 if modal_dataset[environment_name].frf_window.lower() == 'exponential': 

500 modal_parameters.window_parameters = [modal_dataset[environment_name].exponential_window_value_at_frame_end] 

501 modal_parameters.window_parameter_names = ['Window Value at Frame End'] 

502 modal_parameters.frf_technique = modal_dataset[environment_name].frf_technique.lower() 

503 trigger = modal_dataset[environment_name].trigger_type.lower() 

504 modal_parameters.trigger_type = trigger 

505 if trigger != 'free run': 

506 modal_parameters.trigger_slope = 'positive' if modal_dataset[environment_name].trigger_slope_positive else 'negative' 

507 modal_parameters.trigger_level = modal_dataset[environment_name].trigger_level 

508 modal_parameters.pretrigger = modal_dataset[environment_name].pretrigger 

509 signal_generator_type = modal_dataset[environment_name].signal_generator_type 

510 modal_parameters.signal_generator_type = signal_generator_type 

511 if signal_generator_type != 'none': 

512 modal_parameters.signal_generator_level = modal_dataset[environment_name].signal_generator_level 

513 modal_parameters.signal_generator_min_freq = modal_dataset[environment_name].signal_generator_min_frequency 

514 modal_parameters.signal_generator_max_freq = modal_dataset[environment_name].signal_generator_max_frequency 

515 if signal_generator_type == 'burst': 

516 modal_parameters.signal_generator_on_fraction = modal_dataset[environment_name].signal_generator_on_fraction 

517 environment_index = modal_dataset['environment_names'][...]==environment_name 

518 active_channel_indices = modal_dataset['environment_active_channels'][...,environment_index][:,0].astype(bool) 

519 channel_names = np.array([str(modal_dataset['channels']['node_number'][i])+str(modal_dataset['channels']['node_direction'][i]) for i in range(modal_dataset.dimensions['response_channels'].size)])[active_channel_indices] 

520 reference_channels = channel_names[modal_dataset[environment_name]['reference_channel_indices'][:]] 

521 modal_parameters.reference_channel = reference_channels 

522 excitation_indices = np.array([modal_dataset['channels']['feedback_device'][i] != '' for i in range(modal_dataset.dimensions['response_channels'].size)]) 

523 excitation_channels = channel_names[excitation_indices] 

524 modal_parameters.excitation_channel = excitation_channels 

525 return modal_parameters 

526 

527def from_rattlesnake_system_id_parameters(dataset_name, system_id_dataset, environment_name=None, descriptive_name=''): 

528 """Gets system identification parameters from a NetCDF4 streaming file 

529 

530 Parameters 

531 ---------- 

532 dataset_name : str 

533 The name of the dataset to be generated 

534 system_id_dataset : netCDF4.Dataset 

535 A dataset created by loading a system identification streaming file 

536 environment_name : str, optional 

537 Name of the environment. Must be specified if more than one environment 

538 exists in the file 

539 descriptive_name : str, optional 

540 A description for the dataset that will be generated, by default ''. 

541 

542 Returns 

543 ------- 

544 escdf.Dataset 

545 A dataset with the type "rattlesnake_sysid_parameters" containing 

546 system identification parameters. 

547 """ 

548 if escdf is None: 

549 raise ImportError('Could not import module `escdf`, unable to create dataset.') 

550 if environment_name is None: 

551 environment_names = [name for name in system_id_dataset.groups if name != 'channels'] 

552 if len(environment_names) != 1: 

553 raise ValueError('Found {:} environments in the file. Could not select correctly.'.format(len(environment_names))) 

554 environment_name = environment_names[0] 

555 sysid_params = escdf.classes.rattlesnake_sysid_parameters(dataset_name, 

556 descriptive_name) 

557 sysid_params.sample_rate = system_id_dataset.sample_rate 

558 sysid_params.frame_size = system_id_dataset[environment_name].sysid_frame_size 

559 sysid_params.averaging_type = system_id_dataset[environment_name].sysid_averaging_type 

560 sysid_params.noise_averages = system_id_dataset[environment_name].sysid_noise_averages 

561 sysid_params.averages = system_id_dataset[environment_name].sysid_averages 

562 if system_id_dataset[environment_name].sysid_averaging_type != 'Linear': 

563 sysid_params.exponential_averaging_coefficient = system_id_dataset[environment_name].sysid_exponential_averaging_coefficient 

564 sysid_params.estimator = system_id_dataset[environment_name].sysid_estimator 

565 sysid_params.level = system_id_dataset[environment_name].sysid_level 

566 sysid_params.level_ramp_time = system_id_dataset[environment_name].sysid_level_ramp_time 

567 sysid_params.signal_type = system_id_dataset[environment_name].sysid_signal_type 

568 if system_id_dataset[environment_name].sysid_signal_type == 'Burst Random': 

569 sysid_params.burst_on = system_id_dataset[environment_name].sysid_burst_on 

570 sysid_params.pretrigger = system_id_dataset[environment_name].sysid_pretrigger 

571 sysid_params.burst_ramp_fraction = system_id_dataset[environment_name].burst_ramp_fraction 

572 sysid_params.window = system_id_dataset[environment_name].sysid_window 

573 sysid_params.overlap = system_id_dataset[environment_name].sysid_overlap 

574 environment_index = system_id_dataset['environment_names'][...]==environment_name 

575 active_channel_indices = system_id_dataset['environment_active_channels'][...,environment_index][:,0].astype(bool) 

576 channel_names = np.array([str(system_id_dataset['channels']['node_number'][i])+str(system_id_dataset['channels']['node_direction'][i]) for i in range(system_id_dataset.dimensions['response_channels'].size)])[active_channel_indices] 

577 control_channels = channel_names[system_id_dataset[environment_name]['control_channel_indices'][:]] 

578 sysid_params.control_channel = control_channels 

579 excitation_indices = np.array([system_id_dataset['channels']['feedback_device'][i] != '' for i in range(system_id_dataset.dimensions['response_channels'].size)]) 

580 excitation_channels = channel_names[excitation_indices] 

581 sysid_params.excitation_channel = excitation_channels 

582 if 'response_transformation_matrix' in system_id_dataset[environment_name].variables: 

583 sysid_params.control_transformation_matrix = system_id_dataset[environment_name]['response_transformation_matrix'][...] 

584 if 'reference_transformation_matrix' in system_id_dataset[environment_name].variables: 

585 sysid_params.excitation_transformation_matrix = system_id_dataset[environment_name]['reference_transformation_matrix'][...] 

586 return sysid_params 

587 

588def from_rattlesnake_random_parameters(dataset_name, random_dataset, environment_name=None, descriptive_name=''): 

589 """Gets random vibration parameters from a NetCDF4 streaming file 

590 

591 Parameters 

592 ---------- 

593 dataset_name : str 

594 The name of the dataset to be generated 

595 random_dataset : netCDF4.Dataset 

596 A dataset created by loading the streaming file 

597 environment_name : str, optional 

598 Name of the environment. Must be specified if more than one environment 

599 exists in the file 

600 descriptive_name : str, optional 

601 A description for the dataset that will be generated, by default ''. 

602 

603 Returns 

604 ------- 

605 escdf.Dataset 

606 A dataset with the type "rattlesnake_random_control_parameters" containing 

607 random vibration control parameters. 

608 """ 

609 if escdf is None: 

610 raise ImportError('Could not import module `escdf`, unable to create dataset.') 

611 if environment_name is None: 

612 environment_names = [name for name in random_dataset.groups if name != 'channels'] 

613 if len(environment_names) != 1: 

614 raise ValueError('Found {:} environments in the file. Could not select correctly.'.format(len(environment_names))) 

615 environment_name = environment_names[0] 

616 params = escdf.classes.rattlesnake_random_control_parameters(dataset_name, 

617 descriptive_name) 

618 params.sample_rate = random_dataset.sample_rate 

619 environment_index = random_dataset['environment_names'][...]==environment_name 

620 active_channel_indices = random_dataset['environment_active_channels'][...,environment_index][:,0].astype(bool) 

621 channel_names = np.array([str(random_dataset['channels']['node_number'][i])+str(random_dataset['channels']['node_direction'][i]) for i in range(random_dataset.dimensions['response_channels'].size)])[active_channel_indices] 

622 control_channels = channel_names[random_dataset[environment_name]['control_channel_indices'][:]] 

623 params.control_channel = control_channels 

624 excitation_indices = np.array([random_dataset['channels']['feedback_device'][i] != '' for i in range(random_dataset.dimensions['response_channels'].size)]) 

625 excitation_channels = channel_names[excitation_indices] 

626 params.excitation_channel = excitation_channels 

627 params.samples_per_frame = random_dataset[environment_name].samples_per_frame 

628 params.control_frequency_lines = random_dataset[environment_name]['specification_frequency_lines'][...] 

629 params.test_level_ramp_time = random_dataset[environment_name].test_level_ramp_time 

630 params.cpsd_overlap = random_dataset[environment_name].cpsd_overlap 

631 params.cola_window = random_dataset[environment_name].cola_window 

632 params.update_tf_during_control = random_dataset[environment_name].update_tf_during_control 

633 params.cola_overlap = random_dataset[environment_name].cola_overlap 

634 params.cola_window_exponent = random_dataset[environment_name].cola_window_exponent 

635 params.frames_in_cpsd = random_dataset[environment_name].frames_in_cpsd 

636 params.cpsd_window = random_dataset[environment_name].cpsd_window 

637 params.control_python_script = random_dataset[environment_name].control_python_script 

638 params.control_python_function = random_dataset[environment_name].control_python_function 

639 params.control_python_function_type = random_dataset[environment_name].control_python_function_type 

640 params.control_python_function_parameters = random_dataset[environment_name].control_python_function_parameters 

641 params.allow_automatic_aborts = random_dataset[environment_name].allow_automatic_aborts 

642 units = [f'({unit})^2/Hz' for unit in np.array( 

643 [random_dataset['channels']['unit'][i] != '' for i in range(random_dataset.dimensions['response_channels'].size)] 

644 )[active_channel_indices][random_dataset[environment_name]['control_channel_indices'][:]]] 

645 params.specification_unit = units 

646 if not np.all(np.isnan(random_dataset[environment_name]['specification_warning_matrix'][...])): 

647 params.specification_warning_matrix = random_dataset[environment_name]['specification_warning_matrix'][...] 

648 if not np.all(np.isnan(random_dataset[environment_name]['specification_abort_matrix'][...])): 

649 params.specification_abort_matrix = random_dataset[environment_name]['specification_abort_matrix'][...] 

650 if 'response_transformation_matrix' in random_dataset[environment_name].variables: 

651 params.control_transformation_matrix = random_dataset[environment_name]['response_transformation_matrix'][...] 

652 if 'reference_transformation_matrix' in random_dataset[environment_name].variables: 

653 params.excitation_transformation_matrix = random_dataset[environment_name]['reference_transformation_matrix'][...] 

654 return params 

655 

656def from_rattlesnake_transient_parameters(dataset_name, transient_dataset, environment_name=None, descriptive_name=''): 

657 """Gets transient vibration parameters from a NetCDF4 streaming file 

658 

659 Parameters 

660 ---------- 

661 dataset_name : str 

662 The name of the dataset to be generated 

663 transient_dataset : netCDF4.Dataset 

664 A dataset created by loading the streaming file 

665 environment_name : str, optional 

666 Name of the environment. Must be specified if more than one environment 

667 exists in the file 

668 descriptive_name : str, optional 

669 A description for the dataset that will be generated, by default ''. 

670 

671 Returns 

672 ------- 

673 escdf.Dataset 

674 A dataset with the type "rattlesnake_transient_control_parameters" containing 

675 random vibration control parameters. 

676 """ 

677 if escdf is None: 

678 raise ImportError('Could not import module `escdf`, unable to create dataset.') 

679 if environment_name is None: 

680 environment_names = [name for name in transient_dataset.groups if name != 'channels'] 

681 if len(environment_names) != 1: 

682 raise ValueError('Found {:} environments in the file. Could not select correctly.'.format(len(environment_names))) 

683 environment_name = environment_names[0] 

684 params = escdf.classes.rattlesnake_transient_control_parameters(dataset_name, 

685 descriptive_name) 

686 params.sample_rate = transient_dataset.sample_rate 

687 environment_index = transient_dataset['environment_names'][...]==environment_name 

688 active_channel_indices = transient_dataset['environment_active_channels'][...,environment_index][:,0].astype(bool) 

689 channel_names = np.array([str(transient_dataset['channels']['node_number'][i])+str(transient_dataset['channels']['node_direction'][i]) for i in range(transient_dataset.dimensions['response_channels'].size)])[active_channel_indices] 

690 control_channels = channel_names[transient_dataset[environment_name]['control_channel_indices'][:]] 

691 params.control_channel = control_channels 

692 excitation_indices = np.array([transient_dataset['channels']['feedback_device'][i] != '' for i in range(transient_dataset.dimensions['response_channels'].size)]) 

693 excitation_channels = channel_names[excitation_indices] 

694 params.excitation_channel = excitation_channels 

695 params.test_level_ramp_time = transient_dataset[environment_name].test_level_ramp_time 

696 params.control_python_script = transient_dataset[environment_name].control_python_script 

697 params.control_python_function = transient_dataset[environment_name].control_python_function 

698 params.control_python_function_type = transient_dataset[environment_name].control_python_function_type 

699 params.control_python_function_parameters = transient_dataset[environment_name].control_python_function_parameters 

700 if 'response_transformation_matrix' in transient_dataset[environment_name].variables: 

701 params.control_transformation_matrix = transient_dataset[environment_name]['response_transformation_matrix'][...] 

702 if 'reference_transformation_matrix' in transient_dataset[environment_name].variables: 

703 params.excitation_transformation_matrix = transient_dataset[environment_name]['reference_transformation_matrix'][...] 

704 return params 

705 

706def datasets_from_rattlesnake_system_identification( 

707 test_name_slug, 

708 sysid_streaming_data, 

709 sysid_results_data, 

710 verbose=False): 

711 """ 

712 Converts system identification data from Rattlesnake into ESCDF Datasets 

713 

714 Parameters 

715 ---------- 

716 test_name_slug : str 

717 A string identifier that will be prepended to the dataset names 

718 sysid_streaming_data : str or netCDF4.Dataset 

719 A string pointing to a netCDF4 file or the netCDF4 file loaded as a 

720 netCDF4.Dataset object. The file should contain streaming data from 

721 a System Identification phase of the Rattlesnake controller. 

722 sysid_results_data : str or netCDF4.Dataset 

723 A string pointing to a netCDF4 file or the netCDF4 file loaded as a 

724 netCDF4.Dataset object. The file should contain spectral data from 

725 a System Identification phase of the Rattlesnake controller. 

726 verbose : bool, optional 

727 If True, progress will be printed. The default is False. 

728 

729 Raises 

730 ------ 

731 ImportError 

732 If the escdf package is not available. 

733 

734 Returns 

735 ------- 

736 ESCDF Datasets 

737 A nested Tuple of ESCDF datasets. The first index is the metadata and 

738 the second index is the data. Metadata includes system identification 

739 parameters and channel table. Data includes FRFs, Noise and System ID 

740 Response CPSDs, Noise and System ID Drive CPSDs, and Multiple Coherence. 

741 

742 """ 

743 if escdf is None: 

744 raise ImportError('Could not import module `escdf`, unable to create dataset.') 

745 if isinstance(sysid_streaming_data,str): 

746 sysid_streaming_data = nc4.Dataset(sysid_streaming_data) 

747 if isinstance(sysid_results_data,str): 

748 sysid_results_data = nc4.dataset(sysid_results_data) 

749 

750 # Extract the data 

751 if verbose: 

752 print('Reading Time Data') 

753 noise_time_history, channel_table = read_rattlesnake_output( 

754 sysid_streaming_data,read_variable = 'time_data') 

755 sysid_time_history, _ = read_rattlesnake_output( 

756 sysid_streaming_data,read_variable = 'time_data_1') 

757 if verbose: 

758 print('Reading Spectral Data') 

759 (sysid_frfs, sysid_response_cpsd, sysid_drive_cpsd, 

760 sysid_response_noise_cpsd, sysid_drive_noise_cpsd, sysid_coherence) = ( 

761 read_system_id_nc4(sysid_results_data)) 

762 

763 if verbose: 

764 print('Creating ESCDF Data Objects') 

765 # Create escdf data objects 

766 esnoise_time_history = from_data( 

767 f'{test_name_slug}_Noise_Time_History',noise_time_history) 

768 esnoise_time_history.ordinate_unit = [comment.split('::')[1].strip() for comment in noise_time_history.flatten().comment1] 

769 esnoise_time_history.abscissa_unit = 's' 

770 

771 essysid_time_history = from_data( 

772 f'{test_name_slug}_SysID_Time_History',sysid_time_history) 

773 essysid_time_history.ordinate_unit = [comment.split('::')[1].strip() for comment in noise_time_history.flatten().comment1] 

774 essysid_time_history.abscissa_unit = 's' 

775 

776 esspectral_data = [] 

777 for data,joiner,appender,label in zip([sysid_frfs,sysid_response_cpsd,sysid_response_noise_cpsd, 

778 sysid_drive_cpsd,sysid_drive_noise_cpsd], 

779 ['/','*','*','*','*'], 

780 ['','/Hz','/Hz','/Hz','/Hz'], 

781 ['SysID_FRFs','SysID_Response_CPSD', 

782 'SysID_Response_Noise_CPSD', 

783 'SysID_Drive_CPSD', 

784 'SysID_Drive_Noise_CPSD']): 

785 data = data.flatten() 

786 esobj = from_data(f'{test_name_slug}_{label}',data) 

787 try: 

788 if np.all(data.comment1[0] == data.comment1): # All units are the same 

789 comment = data.comment1[0] 

790 esobj.ordinate_unit = joiner.join(['('+value.split('::')[1].strip()+')' for value in comment.split('//')])+appender 

791 else: 

792 esobj.ordinate_unit = [joiner.join(['('+value.split('::')[1].strip()+')' for value in comment.split('//')])+appender for comment in data.comment1] 

793 except IndexError: 

794 print(f'Could not automatically assign units to {esobj.name}') 

795 esobj.abscissa_unit = 'Hz' 

796 esspectral_data.append(esobj) 

797 

798 essysid_coherence = from_data( 

799 f'{test_name_slug}_SysID_Coherence',sysid_coherence) 

800 essysid_coherence.ordinate_unit = '-' 

801 essysid_coherence.abscissa_unit = 'Hz' 

802 

803 if verbose: 

804 print('Creating ESCDF Metadata Objects') 

805 # Channel table 

806 eschan = from_rattlesnake_channel_info( 

807 f'{test_name_slug}_Channel_Table', channel_table) 

808 

809 # System Identification Parameters 

810 essysid = from_rattlesnake_system_id_parameters( 

811 f'{test_name_slug}_Params', sysid_streaming_data) 

812 

813 return ((essysid, eschan), 

814 tuple([esnoise_time_history, essysid_time_history] + esspectral_data 

815 + [essysid_coherence])) 

816 

817def datasets_from_rattlesnake_random_vibration(test_name_slug, 

818 streaming_data, 

819 spectral_data, 

820 verbose=False): 

821 """ 

822 Converts random vibration data from Rattlesnake into ESCDF Datasets 

823 

824 Parameters 

825 ---------- 

826 test_name_slug : str 

827 A string identifier that will be prepended to the dataset names 

828 streaming_data : str or netCDF4.Dataset 

829 A string pointing to a netCDF4 file or the netCDF4 file loaded as a 

830 netCDF4.Dataset object. The file should contain streaming data from 

831 a random vibration phase of the Rattlesnake controller. 

832 spectral_data : str or netCDF4.Dataset 

833 A string pointing to a netCDF4 file or the netCDF4 file loaded as a 

834 netCDF4.Dataset object. The file should contain spectral data from 

835 a random vibration phase of the Rattlesnake controller. 

836 verbose : bool, optional 

837 If True, progress will be printed. The default is False. 

838 

839 Raises 

840 ------ 

841 ImportError 

842 If the escdf package is not available. 

843 

844 Returns 

845 ------- 

846 ESCDF Datasets 

847 A nested tuple of ESCDF datasets. The first index is the metadata and 

848 the second index is the data. Metadata includes specification, random 

849 vibration parameters, and channel table. Data includes Response CPSD 

850 and Drive CPSD. 

851 """ 

852 

853 if escdf is None: 

854 raise ImportError('Could not import module `escdf`, unable to create dataset.') 

855 if isinstance(streaming_data,str): 

856 streaming_data = nc4.Dataset(streaming_data) 

857 if isinstance(spectral_data,str): 

858 spectral_data = nc4.dataset(spectral_data) 

859 

860 # Extract the data 

861 time_dataset_names = [variable for variable in streaming_data.variables if 'time_data' in variable] 

862 if verbose: 

863 print('Reading Time Data') 

864 time_datasets = [] 

865 for name in time_dataset_names: 

866 time_history, channel_table = read_rattlesnake_output( 

867 streaming_data,read_variable = name) 

868 time_datasets.append(time_history) 

869 

870 if verbose: 

871 print('Reading Spectral Data') 

872 response_cpsd,spec_cpsd,drive_cpsd = read_random_spectral_data(spectral_data) 

873 

874 if verbose: 

875 print('Creating ESCDF Data Objects') 

876 esdata = [] 

877 for i,data in enumerate(time_datasets): 

878 this_esdata = from_data( 

879 f'{test_name_slug}_Time_History{"_"+str(i) if len(time_datasets)>1 else ""}', 

880 data) 

881 this_esdata.ordinate_unit = [comment.split('::')[1].strip() for comment in data.flatten().comment1] 

882 this_esdata.abscissa_unit = 's' 

883 esdata.append(this_esdata) 

884 

885 esspec = [] 

886 for i,(data, label) in enumerate(zip([spec_cpsd, response_cpsd, drive_cpsd], 

887 ['Specification_CPSD','Response_CPSD','Drive_CPSD'])): 

888 data = data.flatten() 

889 this_esdata = from_data(f'{test_name_slug}_{label}',data) 

890 try: 

891 if np.all(data.comment1[0] == data.comment1): # All units are the same 

892 comment = data.comment1[0] 

893 this_esdata.ordinate_unit = '*'.join(['('+value.split('::')[1].strip()+')' for value in comment.split('//')])+'/Hz' 

894 else: 

895 this_esdata.ordinate_unit = ['*'.join(['('+value.split('::')[1].strip()+')' for value in comment.split('//')])+'/Hz' for comment in data.comment1] 

896 except IndexError: 

897 print(f'Could not automatically assign units to {this_esdata.name}') 

898 this_esdata.abscissa_unit = 'Hz' 

899 if i == 0: 

900 esspec.append(this_esdata) 

901 else: 

902 esdata.append(this_esdata) 

903 

904 if verbose: 

905 print('Creating ESCDF Metadata Objects') 

906 

907 # Channel table 

908 eschan = from_rattlesnake_channel_info( 

909 f'{test_name_slug}_Channel_Table', channel_table) 

910 # Random Vibration Parameters 

911 esrand = from_rattlesnake_random_parameters( 

912 f'{test_name_slug}_Params', streaming_data) 

913 

914 # Pull the spec into metadata 

915 return ((esrand, eschan, esspec[0]), tuple(esdata)) 

916 

917def datasets_from_rattlesnake_transient_vibration(test_name_slug, 

918 streaming_data, 

919 control_data, 

920 verbose=False): 

921 """ 

922 Converts transient vibration data from Rattlesnake into ESCDF Datasets 

923 

924 Parameters 

925 ---------- 

926 test_name_slug : str 

927 A string identifier that will be prepended to the dataset names 

928 streaming_data : str or netCDF4.Dataset 

929 A string pointing to a netCDF4 file or the netCDF4 file loaded as a 

930 netCDF4.Dataset object. The file should contain streaming data from 

931 a random vibration phase of the Rattlesnake controller. 

932 control_data : str or netCDF4.Dataset 

933 A string pointing to a netCDF4 file or the netCDF4 file loaded as a 

934 netCDF4.Dataset object. The file should contain control data from 

935 a transient vibration phase of the Rattlesnake controller. 

936 verbose : bool, optional 

937 If True, progress will be printed. The default is False. 

938 

939 Raises 

940 ------ 

941 ImportError 

942 If the escdf package is not available. 

943 

944 Returns 

945 ------- 

946 ESCDF Datasets 

947 A nested tuple of ESCDF datasets. The first index is the metadata and 

948 the second index is the data. Metadata includes specification, transient 

949 vibration parameters, and channel table. Data includes Response 

950 and Drive time histories. 

951 """ 

952 

953 if escdf is None: 

954 raise ImportError('Could not import module `escdf`, unable to create dataset.') 

955 if isinstance(streaming_data,str): 

956 streaming_data = nc4.Dataset(streaming_data) 

957 if isinstance(control_data,str): 

958 control_data = nc4.dataset(control_data) 

959 

960 # Extract the data 

961 time_dataset_names = [variable for variable in streaming_data.variables if 'time_data' in variable] 

962 if verbose: 

963 print('Reading Time Data') 

964 time_datasets = [] 

965 for name in time_dataset_names: 

966 time_history, channel_table = read_rattlesnake_output( 

967 streaming_data,read_variable = name) 

968 time_datasets.append(time_history) 

969 

970 if verbose: 

971 print('Reading Control Data') 

972 response_th,spec_th,drive_th = read_transient_control_data(control_data) 

973 

974 if verbose: 

975 print('Creating ESCDF Data Objects') 

976 esdata = [] 

977 for i,data in enumerate(time_datasets): 

978 this_esdata = from_data( 

979 f'{test_name_slug}_Time_History{"_"+str(i) if len(time_datasets)>1 else ""}', 

980 data) 

981 this_esdata.ordinate_unit = [comment.split('::')[1].strip() for comment in data.flatten().comment1] 

982 this_esdata.abscissa_unit = 's' 

983 esdata.append(this_esdata) 

984 

985 esspec = [] 

986 for i,(data, label) in enumerate(zip([spec_th, response_th, drive_th], 

987 ['Specification_Time_History','Control_Time_History','Drive_Time_History'])): 

988 data = data.flatten() 

989 this_esdata = from_data(f'{test_name_slug}_{label}',data) 

990 try: 

991 if np.all(data.comment1[0] == data.comment1): # All units are the same 

992 comment = data.comment1[0] 

993 this_esdata.ordinate_unit = '('+comment.split('::')[1].strip()+')' 

994 else: 

995 this_esdata.ordinate_unit = ['('+comment.split('::')[1].strip()+')' for comment in data.comment1] 

996 except IndexError: 

997 print(f'Could not automatically assign units to {this_esdata.name}') 

998 this_esdata.abscissa_unit = 's' 

999 if i == 0: 

1000 esspec.append(this_esdata) 

1001 else: 

1002 esdata.append(this_esdata) 

1003 

1004 if verbose: 

1005 print('Creating ESCDF Metadata Objects') 

1006 

1007 # Channel table 

1008 eschan = from_rattlesnake_channel_info( 

1009 f'{test_name_slug}_Channel_Table', channel_table) 

1010 # Transient Vibration Parameters 

1011 estrans = from_rattlesnake_transient_parameters( 

1012 f'{test_name_slug}_Params', streaming_data) 

1013 

1014 # Pull the spec into metadata 

1015 return ((estrans, eschan, esspec[0]), tuple(esdata)) 

1016 

1017def datasets_from_rattlesnake_modal( 

1018 test_name_slug, 

1019 modal_results_data, 

1020 mode_fits = None, 

1021 resynthesized_frfs = None, 

1022 verbose=False): 

1023 """ 

1024 Converts modal data from Rattlesnake into ESCDF Datasets 

1025 

1026 Parameters 

1027 ---------- 

1028 test_name_slug : str 

1029 A string identifier that will be prepended to the dataset names 

1030 modal_results_data : str or netCDF4.Dataset 

1031 A string pointing to a netCDF4 file or the netCDF4 file loaded as a 

1032 netCDF4.Dataset object. The file should contain modal results from a 

1033 Rattlesnake modal environment. 

1034 mode_fits : ShapeArray 

1035 A ShapeArray object containing fit modes. If not supplied, no mode 

1036 dataset will be created. 

1037 resynthesized_frfs : TransferFunctionArray 

1038 A TransferFunctionArray object containing resynthesized FRFs. If not 

1039 supplied, no resynthesized frf dataset will be created. 

1040 verbose : bool, optional 

1041 If True, progress will be printed. The default is False. 

1042 

1043 Raises 

1044 ------ 

1045 ImportError 

1046 If the escdf package is not available. 

1047 

1048 Returns 

1049 ------- 

1050 ESCDF Datasets 

1051 A nested Tuple of ESCDF datasets. The first index is the metadata and 

1052 the second index is the data. Metadata includes modal 

1053 parameters and channel table. Data includes FRFs, Coherence, Fit Modes 

1054 (if supplied) and resynthesized FRFs (if supplied). 

1055 

1056 """ 

1057 if escdf is None: 

1058 raise ImportError('Could not import module `escdf`, unable to create dataset.') 

1059 if isinstance(modal_results_data,str): 

1060 modal_results_data = nc4.Dataset(modal_results_data) 

1061 

1062 th, frf, mcoh, channel_table = read_modal_data(modal_results_data) 

1063 th_joined = join(th) 

1064 th_joined.comment1 = th[0].comment1 

1065 frf = frf.flatten() 

1066 

1067 

1068 if verbose: 

1069 print('Creating ESCDF Data Objects') 

1070 

1071 es_th = from_data( 

1072 f'{test_name_slug}_Time_History',th) 

1073 es_th.ordinate_unit = [comment.split('::')[1].strip() for comment in th.flatten().comment1] 

1074 es_th.abscissa_unit = 's' 

1075 

1076 es_frf = from_data(f'{test_name_slug}_FRF',frf) 

1077 es_frf.abscissa_unit = 'Hz' 

1078 try: 

1079 if np.all(frf.comment1[0] == frf.comment1): # All units are the same 

1080 comment = frf.comment1[0] 

1081 es_frf.ordinate_unit = '/'.join(['('+value.split('::')[1].strip()+')' for value in comment.split('//')]) 

1082 else: 

1083 es_frf.ordinate_unit = ['/'.join(['('+value.split('::')[1].strip()+')' for value in comment.split('//')]) for comment in frf.comment1] 

1084 except IndexError: 

1085 print(f'Could not automatically assign units to {es_frf.name}') 

1086 

1087 es_coh = from_data( 

1088 f'{test_name_slug}_Coherence',mcoh) 

1089 es_coh.ordinate_unit = '-' 

1090 es_coh.abscissa_unit = 'Hz' 

1091 

1092 additional_data = [] 

1093 if mode_fits is not None: 

1094 es_mode = from_shape(f'{test_name_slug}_Fit_Modes',mode_fits) 

1095 additional_data.append(es_mode) 

1096 

1097 if resynthesized_frfs is not None: 

1098 resynthesized_frfs = resynthesized_frfs.flatten() 

1099 es_frf_res = from_data(f'{test_name_slug}_Resynthesized_FRFs',resynthesized_frfs) 

1100 es_frf_res.abscissa_unit = 'Hz' 

1101 try: 

1102 frf_match = frf[resynthesized_frfs.coordinate] 

1103 if np.all(frf_match.comment1[0] == frf_match.comment1): # All units are the same 

1104 comment = frf_match.comment1[0] 

1105 es_frf_res.ordinate_unit = '/'.join(['('+value.split('::')[1].strip()+')' for value in comment.split('//')]) 

1106 else: 

1107 es_frf_res.ordinate_unit = ['/'.join(['('+value.split('::')[1].strip()+')' for value in comment.split('//')]) for comment in frf_match.comment1] 

1108 except (ValueError,IndexError): 

1109 print('Could not automatically assign units to {es_frf_res.name}') 

1110 additional_data.append(es_frf_res) 

1111 

1112 if verbose: 

1113 print('Creating ESCDF Metadata Objects') 

1114 # Channel table 

1115 eschan = from_rattlesnake_channel_info( 

1116 f'{test_name_slug}_Channel_Table', channel_table) 

1117 

1118 # System Identification Parameters 

1119 esmod = from_rattlesnake_modal_parameters( 

1120 f'{test_name_slug}_Params', modal_results_data) 

1121 

1122 return ((esmod, eschan), 

1123 tuple([es_th, es_frf, es_coh] + additional_data)) 

1124 

1125class ESCDFSettingDialog(QtWidgets.QDialog): 

1126 def __init__(self, data_type, initial_value, message, parent=None): 

1127 super().__init__(parent) 

1128 self.setWindowTitle("Set Settings") 

1129 self.data_type = data_type 

1130 self.value = None # To store the final value if OK is pressed 

1131 

1132 # Create layout 

1133 layout = QtWidgets.QVBoxLayout() 

1134 

1135 # Add message label 

1136 self.message_label = QtWidgets.QLabel(message) 

1137 layout.addWidget(self.message_label) 

1138 

1139 # Create the appropriate input widget based on data type 

1140 if data_type == int: 

1141 self.input_widget = QtWidgets.QSpinBox() 

1142 self.input_widget.setMaximum(999999999) 

1143 self.input_widget.setMinimum(-999999999) 

1144 self.input_widget.setValue(initial_value) 

1145 elif data_type == float: 

1146 self.input_widget = QtWidgets.QDoubleSpinBox() 

1147 self.input_widget.setMaximum(999999999.0) 

1148 self.input_widget.setMinimum(-999999999.0) 

1149 self.input_widget.setValue(initial_value) 

1150 elif data_type == str: 

1151 self.input_widget = QtWidgets.QTextEdit() 

1152 self.input_widget.setText(initial_value) 

1153 elif data_type == list: 

1154 self.input_widget = QtWidgets.QComboBox() 

1155 self.input_widget.addItems(initial_value) 

1156 self.input_widget.setCurrentIndex(0) # Default to first item 

1157 else: 

1158 raise ValueError("Unsupported data type") 

1159 

1160 layout.addWidget(self.input_widget) 

1161 

1162 # Add OK and Cancel buttons 

1163 button_layout = QtWidgets.QHBoxLayout() 

1164 self.ok_button = QtWidgets.QPushButton("OK") 

1165 self.cancel_button = QtWidgets.QPushButton("Cancel") 

1166 button_layout.addWidget(self.ok_button) 

1167 button_layout.addWidget(self.cancel_button) 

1168 layout.addLayout(button_layout) 

1169 

1170 # Connect buttons to their respective slots 

1171 self.ok_button.clicked.connect(self.accept) 

1172 self.cancel_button.clicked.connect(self.reject) 

1173 

1174 self.setLayout(layout) 

1175 

1176 def get_input_value(self): 

1177 """Retrieve the value from the input widget.""" 

1178 if self.data_type == int: 

1179 return self.input_widget.value() 

1180 elif self.data_type == float: 

1181 return self.input_widget.value() 

1182 elif self.data_type == str: 

1183 return self.input_widget.toPlainText() 

1184 elif self.data_type == list: 

1185 return self.input_widget.currentIndex() 

1186 else: 

1187 return None 

1188 

1189 @staticmethod 

1190 def get_value(data_type, initial_value, message, parent=None): 

1191 """ 

1192 Static method to create the dialog, show it, and return the value if OK is pressed. 

1193 Returns None if Cancel is pressed. 

1194 """ 

1195 dialog = ESCDFSettingDialog(data_type, initial_value, message, parent) 

1196 result = dialog.exec_() # Show the dialog and wait for user interaction 

1197 if result == QtWidgets.QDialog.Accepted: 

1198 return dialog.get_input_value() 

1199 else: 

1200 return None 

1201 

1202# def trace_callback(func): 

1203# """ 

1204# A decorator to trace the execution of callback functions. 

1205# Prints the function name before calling it. 

1206# """ 

1207# def wrapper(*args, **kwargs): 

1208# print(f"Callback triggered: {func.__name__}") 

1209# try: 

1210# return func(*args, **kwargs) 

1211# except Exception as e: 

1212# print(f"Caught: {e}") 

1213# return wrapper 

1214 

1215class ESCDFTableModel(QtCore.QAbstractTableModel): 

1216 

1217 def __init__(self, escdf_property, indices, parent=None): 

1218 super().__init__(parent) 

1219 self.prop = escdf_property 

1220 self.indices = indices 

1221 self.table_dimensions = [dimension_size for index, dimension_size in zip(self.indices, self.prop.shape) if index < 0] 

1222 if len(self.table_dimensions) > 2: 

1223 raise ValueError('Only two dimensions can be scrollable') 

1224 while len(self.table_dimensions) < 2: 

1225 self.table_dimensions.append(1) 

1226 

1227 def rowCount(self, parent=QtCore.QModelIndex()): 

1228 return self.table_dimensions[0] 

1229 

1230 def columnCount(self, parent=QtCore.QModelIndex()): 

1231 return self.table_dimensions[1] 

1232 

1233 def get_slice(self, row, column): 

1234 dynamic_variables = [row, column] 

1235 output_slice = [] 

1236 for index in self.indices: 

1237 if index < 0: 

1238 output_slice.append(dynamic_variables.pop(0)) 

1239 else: 

1240 output_slice.append(index) 

1241 return tuple(output_slice) 

1242 

1243 def data(self, index, role=QtCore.Qt.DisplayRole): 

1244 if role == QtCore.Qt.DisplayRole: 

1245 row = index.row() 

1246 col = index.column() 

1247 

1248 output_slice = self.get_slice(row, col) 

1249 

1250 # Fetch the data lazily 

1251 value = self.prop[output_slice] 

1252 return str(value) 

1253 return None 

1254 

1255 def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole): 

1256 if role == QtCore.Qt.DisplayRole: 

1257 # Return 0-based counting for headers 

1258 return str(section) 

1259 return None 

1260 

1261class DimensionSpinBox(QtWidgets.QSpinBox): 

1262 def textFromValue(self, value): 

1263 # Override to display ":" for -1 

1264 if value == -1: 

1265 return ":" 

1266 return str(value) 

1267 

1268 def valueFromText(self, text): 

1269 # Override to interpret ":" as -1 

1270 if text == ":": 

1271 return -1 

1272 return int(text) 

1273 

1274 def validate(self, text, pos): 

1275 # Allow ":" as valid input, along with numeric values 

1276 if text == ":": 

1277 return QtGui.QValidator.Acceptable, text, pos 

1278 try: 

1279 int(text) # Check if the text can be converted to an integer 

1280 return QtGui.QValidator.Acceptable, text, pos 

1281 except ValueError: 

1282 return QtGui.QValidator.Invalid, text, pos 

1283 

1284class ESCDFVisualizer(QMainWindow): 

1285 """An interactive window allowing users to explore an ESCDF file""" 

1286 

1287 monofont = QtGui.QFont("monospace") 

1288 monofont.setStyleHint(QtGui.QFont.TypeWriter) 

1289 

1290 def __init__(self, escdf_file=None): 

1291 """Create an ESCDF Visualizer Window to explore an ESCDF file. 

1292  

1293 A filename or ESCDF object can be passed as an argment, or it can be 

1294 loaded at a later point. 

1295  

1296 Parameters 

1297 ---------- 

1298 escdf_file : str or ESCDF, optional 

1299 The file or ESCDF object to explore. If not passed, one can be 

1300 loaded through the graphical user interface. 

1301 

1302 Returns 

1303 ------- 

1304 None. 

1305 

1306 """ 

1307 if escdf is None: 

1308 raise ImportError('Could not import module `escdf`, unable to use Visualizer.') 

1309 super().__init__() 

1310 uic.loadUi(os.path.join(os.path.abspath(os.path.dirname( 

1311 os.path.abspath(__file__))), 'escdf_visualizer.ui'), self) 

1312 self.activity_selector : QtWidgets.QListWidget 

1313 self.data_selector : QtWidgets.QListWidget 

1314 self.metadata_selector : QtWidgets.QListWidget 

1315 self.alldata_selector : QtWidgets.QListWidget 

1316 self.allmetadata_selector : QtWidgets.QListWidget 

1317 self.data_property_selector : QtWidgets.QListWidget 

1318 self.metadata_property_selector : QtWidgets.QListWidget 

1319 self.activity_property_string : QtWidgets.QLabel 

1320 self.data_property_string : QtWidgets.QLabel 

1321 self.export_mat_button : QtWidgets.QPushButton 

1322 self.export_npz_button : QtWidgets.QPushButton 

1323 self.plot_coordinate_button : QtWidgets.QPushButton 

1324 self.plot_data_against_metadata_button : QtWidgets.QPushButton 

1325 self.plot_data_button : QtWidgets.QPushButton 

1326 self.plot_data_on_geometry_button : QtWidgets.QPushButton 

1327 self.plot_geometry_button : QtWidgets.QPushButton 

1328 self.plot_shape_button : QtWidgets.QPushButton 

1329 self.tab_widget : QtWidgets.QTabWidget 

1330 for widget in [self.activity_property_string, self.data_property_string]: 

1331 widget.setFont(ESCDFVisualizer.monofont) 

1332 self.setWindowTitle('ESCDF Visualizer') 

1333 self.escdf : escdf.ESCDF 

1334 self.escdf = None 

1335 self.node_size : int 

1336 self.node_size = 5 

1337 self.line_width : int 

1338 self.line_width = 2 

1339 self.label_text_size : int 

1340 self.label_text_size = 10 

1341 self.arrow_size : float 

1342 self.arrow_size = 0.1 

1343 self.opacity : float 

1344 self.opacity = 1.0 

1345 self.undeformed_opacity : float 

1346 self.undeformed_opacity = 0.1 

1347 self.transient_start_time : float 

1348 self.transient_start_time = -10000000.0 

1349 self.transient_end_time : float 

1350 self.transient_end_time = 10000000.0 

1351 self.data_array = [] 

1352 self.metadata_array = [] 

1353 self.data_dimension_spinboxes = [] 

1354 self.metadata_dimension_spinboxes = [] 

1355 if escdf_file is not None: 

1356 self.load(escdf_file) 

1357 self.guiplots = [] 

1358 self.connect_callbacks() 

1359 self.update_viz_buttons() 

1360 self.show() 

1361 

1362 # @trace_callback 

1363 def connect_callbacks(self): 

1364 self.actionLoad_File.triggered.connect(self.select_file) 

1365 self.actionSet_Node_Size.triggered.connect(self.set_node_size) 

1366 self.actionSet_Line_Width.triggered.connect(self.set_line_width) 

1367 self.actionSet_Label_Text_Size.triggered.connect(self.set_label_text_size) 

1368 self.actionSet_Arrow_Size.triggered.connect(self.set_arrow_size) 

1369 self.actionSet_Opacity.triggered.connect(self.set_opacity) 

1370 self.actionSet_Undeformed_Opacity.triggered.connect(self.set_undeformed_opacity) 

1371 self.actionSet_Transient_Start_Time.triggered.connect(self.set_transient_start_time) 

1372 self.actionSet_Transient_End_Time.triggered.connect(self.set_transient_end_time) 

1373 self.activity_selector.currentItemChanged.connect(self.update_activity_data) 

1374 self.data_selector.currentItemChanged.connect(self.update_data_data) 

1375 self.data_selector.itemDoubleClicked.connect(self.go_to_data) 

1376 self.metadata_selector.itemDoubleClicked.connect(self.go_to_metadata) 

1377 self.metadata_selector.currentItemChanged.connect(self.update_metadata_data) 

1378 self.data_property_selector.currentItemChanged.connect(self.update_data_property) 

1379 self.metadata_property_selector.currentItemChanged.connect(self.update_metadata_property) 

1380 self.plot_coordinate_button.clicked.connect(self.plot_coordinate) 

1381 self.plot_data_against_metadata_button.clicked.connect(self.plot_data_against_metadata) 

1382 self.plot_data_button.clicked.connect(self.plot_data) 

1383 self.plot_data_on_geometry_button.clicked.connect(self.plot_data_on_geometry) 

1384 self.plot_geometry_button.clicked.connect(self.plot_geometry) 

1385 self.plot_shape_button.clicked.connect(self.plot_shape) 

1386 self.tab_widget.currentChanged.connect(self.update_tab) 

1387 self.alldata_selector.currentItemChanged.connect(self.update_data_properties) 

1388 self.allmetadata_selector.currentItemChanged.connect(self.update_metadata_properties) 

1389 

1390 # @trace_callback 

1391 def update_tab(self, argument=None): 

1392 self.update_viz_buttons() 

1393 

1394 # @trace_callback 

1395 def update_activity_data(self, current=None, previous = None): 

1396 index = self.activity_selector.currentRow() 

1397 if index < 0: 

1398 self.activity_property_string.setText('Select Activity to See Properties') 

1399 self.update_viz_buttons() 

1400 return 

1401 activity = self.escdf.activities[index] 

1402 self.activity_property_string.setText('\n'.join(activity.repr().split('\n')[:-2])) 

1403 self.data_selector.clear() 

1404 for data in activity.data: 

1405 name = data.name 

1406 type = data.dataset_type 

1407 self.data_selector.addItem(f'{name} ({type})') 

1408 self.metadata_selector.clear() 

1409 for name in activity.metadata_links: 

1410 metadata = self.escdf.metadata[name] 

1411 type = metadata.dataset_type 

1412 self.metadata_selector.addItem(f'{name} ({type})') 

1413 self.update_viz_buttons() 

1414 

1415 # @trace_callback 

1416 def go_to_data(self, item=None): 

1417 found = False 

1418 dataset = self.get_active_data() 

1419 for index, compare_dataset in enumerate(self.data_array): 

1420 if dataset is compare_dataset: 

1421 # print('Found Dataset at {:}'.format(index)) 

1422 found = True 

1423 break 

1424 if found: 

1425 self.tab_widget.setCurrentIndex(1) 

1426 self.alldata_selector.setCurrentRow(index) 

1427 else: 

1428 # print('Did not find dataset!') 

1429 raise ValueError("Did not find dataset!") 

1430 

1431 # @trace_callback 

1432 def go_to_metadata(self, item=None): 

1433 found = False 

1434 metadata_index = self.metadata_selector.currentRow() 

1435 metadata_name = self.escdf.activities[self.activity_selector.currentRow()].metadata_links[metadata_index] 

1436 for index, compare_dataset in enumerate(self.metadata_array): 

1437 if compare_dataset.name == metadata_name: 

1438 # print('Found Dataset at {:}'.format(index)) 

1439 found = True 

1440 break 

1441 if found: 

1442 self.tab_widget.setCurrentIndex(2) 

1443 self.allmetadata_selector.setCurrentRow(index) 

1444 else: 

1445 # print('Did not find dataset!') 

1446 raise ValueError("Did not find dataset!") 

1447 

1448 # @trace_callback 

1449 def update_data_properties(self, current=None, previous=None): 

1450 # print(self.alldata_selector.currentRow()) 

1451 data_index = self.alldata_selector.currentRow() 

1452 if data_index < 0: 

1453 self.data_property_selector.clear() 

1454 return 

1455 dataset = self.data_array[data_index] 

1456 widget = self.data_property_selector 

1457 self.populate_properties(dataset, widget) 

1458 

1459 # @trace_callback 

1460 def update_data_property(self, current=None, previous=None): 

1461 data_index = self.alldata_selector.currentRow() 

1462 data = self.data_array[data_index] 

1463 property_index = self.data_property_selector.currentRow() 

1464 if property_index < 0: 

1465 return 

1466 property_name = data.property_names[property_index] 

1467 prop = getattr(data, property_name) 

1468 # Set up spinboxes for the dimensions 

1469 self.set_up_data_dimension_spinboxes(prop) 

1470 self.update_data_dimension(prop=prop) 

1471 

1472 # @trace_callback 

1473 def set_up_data_dimension_spinboxes(self, prop): 

1474 for spinbox in self.data_dimension_spinboxes: 

1475 self.data_dimension_selector_layout.removeWidget(spinbox) 

1476 self.data_dimension_spinboxes.clear() 

1477 if prop is not None: 

1478 for i, dimension in enumerate(prop.shape): 

1479 spinbox = DimensionSpinBox() 

1480 spinbox.blockSignals(True) 

1481 spinbox.setMinimum(-1) 

1482 spinbox.setMaximum(dimension-1) 

1483 spinbox.valueChanged.connect(self.update_data_dimension) 

1484 self.data_dimension_selector_layout.addWidget(spinbox) 

1485 self.data_dimension_spinboxes.append(spinbox) 

1486 if i < 2: 

1487 spinbox.setValue(-1) 

1488 else: 

1489 spinbox.setValue(0) 

1490 for spinbox in self.data_dimension_spinboxes: 

1491 spinbox.blockSignals(False) 

1492 

1493 # @trace_callback 

1494 def update_data_dimension(self, ind=None, prop=None): 

1495 if prop is None: 

1496 data_index = self.alldata_selector.currentRow() 

1497 data = self.data_array[data_index] 

1498 property_index = self.data_property_selector.currentRow() 

1499 if property_index < 0: 

1500 return 

1501 property_name = data.property_names[property_index] 

1502 prop = getattr(data, property_name) 

1503 if prop is None: 

1504 self.data_table_view.setModel(None) 

1505 return 

1506 indices = [spinbox.value() for spinbox in self.data_dimension_spinboxes] 

1507 model = ESCDFTableModel(prop, indices) 

1508 self.data_table_view.setModel(model) 

1509 

1510 # @trace_callback 

1511 def update_metadata_properties(self, current = None, previous = None): 

1512 # print(self.allmetadata_selector.currentRow()) 

1513 data_index = self.allmetadata_selector.currentRow() 

1514 if data_index < 0: 

1515 self.metadata_property_selector.clear() 

1516 return 

1517 dataset = self.metadata_array[data_index] 

1518 widget = self.metadata_property_selector 

1519 self.populate_properties(dataset, widget) 

1520 

1521 # @trace_callback 

1522 def update_metadata_property(self, current=None, previous=None): 

1523 metadata_index = self.allmetadata_selector.currentRow() 

1524 metadata = self.metadata_array[metadata_index] 

1525 property_index = self.metadata_property_selector.currentRow() 

1526 if property_index < 0: 

1527 return 

1528 property_name = metadata.property_names[property_index] 

1529 prop = getattr(metadata, property_name) 

1530 # Set up spinboxes for the dimensions 

1531 self.set_up_metadata_dimension_spinboxes(prop) 

1532 self.update_metadata_dimension(prop=prop) 

1533 

1534 # @trace_callback 

1535 def set_up_metadata_dimension_spinboxes(self, prop): 

1536 for spinbox in self.metadata_dimension_spinboxes: 

1537 self.metadata_dimension_selector_layout.removeWidget(spinbox) 

1538 self.metadata_dimension_spinboxes.clear() 

1539 if prop is not None: 

1540 for i, dimension in enumerate(prop.shape): 

1541 spinbox = DimensionSpinBox() 

1542 spinbox.blockSignals(True) 

1543 spinbox.setMinimum(-1) 

1544 spinbox.setMaximum(dimension-1) 

1545 spinbox.valueChanged.connect(self.update_metadata_dimension) 

1546 self.metadata_dimension_selector_layout.addWidget(spinbox) 

1547 self.metadata_dimension_spinboxes.append(spinbox) 

1548 if i < 2: 

1549 spinbox.setValue(-1) 

1550 else: 

1551 spinbox.setValue(0) 

1552 for spinbox in self.metadata_dimension_spinboxes: 

1553 spinbox.blockSignals(False) 

1554 

1555 # @trace_callback 

1556 def update_metadata_dimension(self, ind=None, prop=None): 

1557 if prop is None: 

1558 metadata_index = self.allmetadata_selector.currentRow() 

1559 metadata = self.metadata_array[metadata_index] 

1560 property_index = self.metadata_property_selector.currentRow() 

1561 if property_index < 0: 

1562 return 

1563 property_name = metadata.property_names[property_index] 

1564 prop = getattr(metadata, property_name) 

1565 if prop is None: 

1566 self.metadata_table_view.setModel(None) 

1567 return 

1568 indices = [spinbox.value() for spinbox in self.metadata_dimension_spinboxes] 

1569 model = ESCDFTableModel(prop, indices) 

1570 self.metadata_table_view.setModel(model) 

1571 

1572 # @trace_callback 

1573 def populate_properties(self, dataset, widget : QtWidgets.QListWidget): 

1574 widget.clear() 

1575 for property_name in dataset.property_names: 

1576 prop = getattr(dataset, property_name) 

1577 if prop is None: 

1578 widget.addItem(f'{property_name}: []') 

1579 continue 

1580 if np.prod(prop.shape) == 1 and not prop.ragged: 

1581 widget.addItem(f'{property_name}: {prop[...]}') 

1582 continue 

1583 widget.addItem(f'{property_name}: {prop.datatype}, {prop.shape}') 

1584 

1585 # @trace_callback 

1586 def get_active_data(self): 

1587 activity_index = self.activity_selector.currentRow() 

1588 if activity_index < 0: 

1589 return 

1590 index = self.data_selector.currentRow() 

1591 if index < 0: 

1592 return 

1593 activity = self.escdf.activities[activity_index] 

1594 data = activity.data[index] 

1595 return data 

1596 

1597 # @trace_callback 

1598 def update_data_data(self, current = None, previous = None): 

1599 data = self.get_active_data() 

1600 if data is None: 

1601 self.data_property_string.setText('Select Data to See Properties') 

1602 self.update_viz_buttons() 

1603 return 

1604 self.data_property_string.setText(data.repr()) 

1605 self.update_viz_buttons() 

1606 

1607 # @trace_callback 

1608 def update_metadata_data(self, current = None, previous = None): 

1609 self.update_viz_buttons() 

1610 

1611 # @trace_callback 

1612 def select_file(self, checked=False): 

1613 filename, file_filter = QtWidgets.QFileDialog.getOpenFileName( 

1614 self, 'Select ESCDF File', filter='ESCDF (*.h5 *.esf)') 

1615 if filename == '': 

1616 return 

1617 self.load(filename) 

1618 

1619 # @trace_callback 

1620 def load(self,escdf_file): 

1621 # Load the file 

1622 self.escdf = escdf.ESCDF.load(escdf_file) 

1623 

1624 # Update the activity selector 

1625 self.activity_selector.clear() 

1626 for name in self.escdf.activities.names: 

1627 self.activity_selector.addItem(name) 

1628 

1629 self.data_array = [] 

1630 self.metadata_array = [] 

1631 

1632 # Update the data selector 

1633 self.alldata_selector.clear() 

1634 for activity in self.escdf.activities: 

1635 for data in activity.data: 

1636 self.alldata_selector.addItem(f'{activity.name}: {data.name} ({data.dataset_type})') 

1637 self.data_array.append(data) 

1638 self.alldata_selector.setCurrentRow(0) 

1639 self.update_data_properties() 

1640 

1641 # Update the metadata selector 

1642 self.allmetadata_selector.clear() 

1643 for metadata in self.escdf.metadata: 

1644 self.allmetadata_selector.addItem(f'{metadata.name} ({metadata.dataset_type})') 

1645 self.metadata_array.append(metadata) 

1646 self.allmetadata_selector.setCurrentRow(0) 

1647 self.update_metadata_properties() 

1648 

1649 # @trace_callback 

1650 def get_activity_geometry(self): 

1651 activity_index = self.activity_selector.currentRow() 

1652 if activity_index < 0: 

1653 return 

1654 activity_metadata = [self.escdf.metadata[name] for name in self.escdf.activities[activity_index].metadata_links] 

1655 geometry = [ 

1656 metadata 

1657 for metadata in activity_metadata 

1658 if metadata.istype("geometry") or metadata.istype("point_cloud") 

1659 ] 

1660 return geometry 

1661 

1662 # @trace_callback 

1663 def get_activity_metadata_data(self): 

1664 activity_index = self.activity_selector.currentRow() 

1665 activity_metadata = [self.escdf.metadata[name] for name in self.escdf.activities[activity_index].metadata_links] 

1666 data = [metadata for metadata in activity_metadata if metadata.istype('data')] 

1667 return data 

1668 

1669 # @trace_callback 

1670 def find_comparable_data(self, active_data): 

1671 data_comparisions = self.get_activity_metadata_data() 

1672 comparable_data = [] 

1673 for data in data_comparisions: 

1674 if data.data_type[...] != active_data.data_type[...]: 

1675 continue 

1676 if data.channel.shape != active_data.channel.shape: 

1677 continue 

1678 if np.any(data.channel[...] != active_data.channel[...]): 

1679 continue 

1680 comparable_data.append(data) 

1681 return comparable_data 

1682 

1683 # @trace_callback 

1684 def update_viz_buttons(self): 

1685 if self.tab_widget.currentIndex() != 0: 

1686 self.plot_shape_button.setVisible(False) 

1687 self.plot_data_on_geometry_button.setVisible(False) 

1688 self.plot_data_button.setVisible(False) 

1689 self.plot_data_against_metadata_button.setVisible(False) 

1690 self.plot_geometry_button.setVisible(False) 

1691 self.plot_coordinate_button.setVisible(False) 

1692 return 

1693 geometries = self.get_activity_geometry() 

1694 if geometries is None: 

1695 self.plot_shape_button.setVisible(False) 

1696 self.plot_data_on_geometry_button.setVisible(False) 

1697 self.plot_data_button.setVisible(False) 

1698 self.plot_data_against_metadata_button.setVisible(False) 

1699 self.plot_geometry_button.setVisible(False) 

1700 self.plot_coordinate_button.setVisible(False) 

1701 return 

1702 geom_active = len(geometries) > 0 

1703 self.plot_geometry_button.setVisible(geom_active) 

1704 self.plot_coordinate_button.setVisible(geom_active) 

1705 active_data = self.get_active_data() 

1706 if active_data is None: 

1707 self.plot_shape_button.setVisible(False) 

1708 self.plot_data_on_geometry_button.setVisible(False) 

1709 self.plot_data_button.setVisible(False) 

1710 self.plot_data_against_metadata_button.setVisible(False) 

1711 return 

1712 shape_active = geom_active and active_data.istype('mode') 

1713 self.plot_shape_button.setVisible(shape_active) 

1714 data_on_geom_active = geom_active and active_data.istype('data') and active_data.data_type[...] in [ 

1715 'time response', 'frequency response function', 'transmissibility', 'spectrum', 

1716 'impulse_response_function'] 

1717 self.plot_data_on_geometry_button.setVisible(data_on_geom_active) 

1718 data_active = active_data.istype('data') 

1719 self.plot_data_button.setVisible(data_active) 

1720 if data_active: 

1721 comparable_data = self.find_comparable_data(active_data) 

1722 data_comparable = len(comparable_data) > 0 

1723 else: 

1724 data_comparable = False 

1725 self.plot_data_against_metadata_button.setVisible(data_comparable) 

1726 

1727 # @trace_callback 

1728 def set_node_size(self, checked=False): 

1729 out = ESCDFSettingDialog.get_value(int, self.node_size, 'Set the Node Size', self) 

1730 if out is not None: 

1731 self.node_size = out 

1732 

1733 # @trace_callback 

1734 def set_line_width(self, checked=False): 

1735 out = ESCDFSettingDialog.get_value(int, self.line_width, 'Set the Line Width', self) 

1736 if out is not None: 

1737 self.line_width = out 

1738 

1739 # @trace_callback 

1740 def set_label_text_size(self, checked=False): 

1741 out = ESCDFSettingDialog.get_value(int, self.label_text_size, 'Set the Text Size for Labels', self) 

1742 if out is not None: 

1743 self.label_text_size = out 

1744 

1745 # @trace_callback 

1746 def set_arrow_size(self, checked=False): 

1747 out = ESCDFSettingDialog.get_value(float, self.arrow_size, 'Set the Arrow Size for Degrees of Freedom', self) 

1748 if out is not None: 

1749 self.arrow_size = out 

1750 

1751 # @trace_callback 

1752 def set_opacity(self, checked=False): 

1753 out = ESCDFSettingDialog.get_value(float, self.opacity, 'Set the Geometry Opacity', self) 

1754 if out is not None: 

1755 self.opacity = out 

1756 

1757 # @trace_callback 

1758 def set_undeformed_opacity(self, checked=False): 

1759 out = ESCDFSettingDialog.get_value(float, self.undeformed_opacity, 'Set the Undeformed Geometry Opacity', self) 

1760 if out is not None: 

1761 self.undeformed_opacity = out 

1762 

1763 # @trace_callback 

1764 def set_transient_start_time(self, checked=False): 

1765 out = ESCDFSettingDialog.get_value(float, self.transient_start_time, 'Set the Starting Time for Transient Plots', self) 

1766 if out is not None: 

1767 self.transient_start_time = out 

1768 

1769 # @trace_callback 

1770 def set_transient_end_time(self, checked=False): 

1771 out = ESCDFSettingDialog.get_value(float, self.transient_end_time, 'Set the Ending Time for Transient Plots', self) 

1772 if out is not None: 

1773 self.transient_end_time = out 

1774 

1775 # @trace_callback 

1776 def plot_coordinate(self): 

1777 geometries = self.get_activity_geometry() 

1778 if len(geometries) > 1: 

1779 index = ESCDFSettingDialog.get_value(list,[geo.name for geo in geometries],'Select Geometry') 

1780 if index is None: 

1781 return 

1782 else: 

1783 index = 0 

1784 geometry = to_geometry(geometries[index]) 

1785 coordinates = coordinate_array(geometry.node.id, [1, 2, 3], force_broadcast=True) 

1786 plot_kwargs = {'node_size': self.node_size, 

1787 'line_width': self.line_width, 

1788 'show_edges': self.actionPlot_Edges.isChecked(), 

1789 'label_nodes': self.actionLabel_Nodes.isChecked(), 

1790 'label_tracelines': self.actionLabel_Tracelines.isChecked(), 

1791 'label_elements': self.actionLabel_Elements.isChecked(), 

1792 'label_font_size': self.label_text_size} 

1793 geometry.plot_coordinate(coordinates, 

1794 arrow_scale=self.arrow_size, 

1795 label_dofs=self.actionLabel_Degrees_of_Freedom.isChecked(), 

1796 label_font_size=self.label_text_size, 

1797 opacity=self.opacity, 

1798 plot_kwargs=plot_kwargs 

1799 ) 

1800 

1801 # @trace_callback 

1802 def plot_data_against_metadata(self): 

1803 data = self.get_active_data() 

1804 compare_data = self.find_comparable_data(data) 

1805 data = to_data(data) 

1806 if len(compare_data) > 1: 

1807 index = ESCDFSettingDialog.get_value(list, [d.name for d in compare_data], 'Select Comparison Data') 

1808 if index is None: 

1809 return 

1810 else: 

1811 index = 0 

1812 compare_data = to_data(compare_data[index]) 

1813 if data.function_type == FunctionTypes.POWER_SPECTRAL_DENSITY and self.actionPlot_APSDs.isChecked(): 

1814 data = data.get_asd() 

1815 compare_data = compare_data.get_asd() 

1816 self.guiplots.append(GUIPlot(Active_Data=data, Comparison_Data=compare_data)) 

1817 

1818 # @trace_callback 

1819 def plot_data(self): 

1820 data = to_data(self.get_active_data()) 

1821 if data.function_type == FunctionTypes.POWER_SPECTRAL_DENSITY and self.actionPlot_APSDs.isChecked(): 

1822 data = data.get_asd() 

1823 self.guiplots.append(data.gui_plot()) 

1824 

1825 # @trace_callback 

1826 def plot_data_on_geometry(self): 

1827 geometries = self.get_activity_geometry() 

1828 if len(geometries) > 1: 

1829 index = ESCDFSettingDialog.get_value(list,[geo.name for geo in geometries],'Select Geometry') 

1830 if index is None: 

1831 return 

1832 else: 

1833 index = 0 

1834 geometry = to_geometry(geometries[index]) 

1835 data = to_data(self.get_active_data()) 

1836 # We need to reshape it to response x reference in order to plot it 

1837 if data.coordinate.shape[-1] > 1: 

1838 data = data.reshape_to_matrix() 

1839 # Get the reference coordinates 

1840 refs = data[0].reference_coordinate 

1841 if len(refs) > 1: 

1842 index = ESCDFSettingDialog.get_value(list, refs.string_array().tolist(), 'Select Reference to Plot') 

1843 if index is None: 

1844 return 

1845 else: 

1846 index = 0 

1847 data = data[:, index] 

1848 plot_kwargs = {'node_size': self.node_size, 

1849 'line_width': self.line_width, 

1850 'show_edges': self.actionPlot_Edges.isChecked(), 

1851 'label_nodes': self.actionLabel_Nodes.isChecked(), 

1852 'label_tracelines': self.actionLabel_Tracelines.isChecked(), 

1853 'label_elements': self.actionLabel_Elements.isChecked(), 

1854 'label_font_size': self.label_text_size} 

1855 if data.function_type in [FunctionTypes.TIME_RESPONSE, FunctionTypes.IMPULSE_RESPONSE_FUNCTION]: 

1856 geometry.plot_transient(data.extract_elements_by_abscissa( 

1857 self.transient_start_time, 

1858 self.transient_end_time 

1859 ), plot_kwargs=plot_kwargs, 

1860 undeformed_opacity=self.undeformed_opacity, 

1861 deformed_opacity=self.opacity) 

1862 else: 

1863 geometry.plot_deflection_shape(data, plot_kwargs=plot_kwargs, 

1864 undeformed_opacity=self.undeformed_opacity, 

1865 deformed_opacity=self.opacity) 

1866 

1867 # @trace_callback 

1868 def plot_geometry(self): 

1869 geometries = self.get_activity_geometry() 

1870 if len(geometries) > 1: 

1871 index = ESCDFSettingDialog.get_value(list,[geo.name for geo in geometries],'Select Geometry') 

1872 if index is None: 

1873 return 

1874 else: 

1875 index = 0 

1876 geometry = to_geometry(geometries[index]) 

1877 plot_kwargs = {'node_size': self.node_size, 

1878 'line_width': self.line_width, 

1879 'show_edges': self.actionPlot_Edges.isChecked(), 

1880 'label_nodes': self.actionLabel_Nodes.isChecked(), 

1881 'label_tracelines': self.actionLabel_Tracelines.isChecked(), 

1882 'label_elements': self.actionLabel_Elements.isChecked(), 

1883 'label_font_size': self.label_text_size, 

1884 'opacity': self.opacity} 

1885 geometry.plot(**plot_kwargs) 

1886 

1887 # @trace_callback 

1888 def plot_shape(self): 

1889 geometries = self.get_activity_geometry() 

1890 if len(geometries) > 1: 

1891 index = ESCDFSettingDialog.get_value(list,[geo.name for geo in geometries],'Select Geometry') 

1892 if index is None: 

1893 return 

1894 else: 

1895 index = 0 

1896 geometry = to_geometry(geometries[index]) 

1897 shapes = to_shape(self.get_active_data()) 

1898 plot_kwargs = {'node_size': self.node_size, 

1899 'line_width': self.line_width, 

1900 'show_edges': self.actionPlot_Edges.isChecked(), 

1901 'label_nodes': self.actionLabel_Nodes.isChecked(), 

1902 'label_tracelines': self.actionLabel_Tracelines.isChecked(), 

1903 'label_elements': self.actionLabel_Elements.isChecked(), 

1904 'label_font_size': self.label_text_size} 

1905 geometry.plot_shape(shapes, plot_kwargs=plot_kwargs, 

1906 undeformed_opacity=self.undeformed_opacity, 

1907 deformed_opacity=self.opacity)