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
« 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
5@author: dprohe
6"""
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)
19from qtpy.QtWidgets import (QMainWindow)
20from qtpy import QtWidgets, uic, QtGui, QtCore
21import netCDF4 as nc4
22import os
23import numpy as np
25try:
26 import escdf
27except ImportError:
28 escdf = None
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 }
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 }
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
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)
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 )
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
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)
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
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'}
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 }
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
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
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
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.
424 Operates on channel tables like the one from
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
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.')
444 escdf_channels = escdf.Dataset(dataset_name,'vibration_channel_table',
445 descriptive_name)
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'])]
458 return escdf_channels
460def from_rattlesnake_modal_parameters(dataset_name, modal_dataset, environment_name = None, descriptive_name=''):
461 """Generates a "modal_test_parameters" object from modal output
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 ''.
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.')
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
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
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 ''.
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
588def from_rattlesnake_random_parameters(dataset_name, random_dataset, environment_name=None, descriptive_name=''):
589 """Gets random vibration parameters from a NetCDF4 streaming file
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 ''.
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
656def from_rattlesnake_transient_parameters(dataset_name, transient_dataset, environment_name=None, descriptive_name=''):
657 """Gets transient vibration parameters from a NetCDF4 streaming file
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 ''.
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
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
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.
729 Raises
730 ------
731 ImportError
732 If the escdf package is not available.
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.
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)
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))
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'
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'
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)
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'
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)
809 # System Identification Parameters
810 essysid = from_rattlesnake_system_id_parameters(
811 f'{test_name_slug}_Params', sysid_streaming_data)
813 return ((essysid, eschan),
814 tuple([esnoise_time_history, essysid_time_history] + esspectral_data
815 + [essysid_coherence]))
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
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.
839 Raises
840 ------
841 ImportError
842 If the escdf package is not available.
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 """
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)
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)
870 if verbose:
871 print('Reading Spectral Data')
872 response_cpsd,spec_cpsd,drive_cpsd = read_random_spectral_data(spectral_data)
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)
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)
904 if verbose:
905 print('Creating ESCDF Metadata Objects')
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)
914 # Pull the spec into metadata
915 return ((esrand, eschan, esspec[0]), tuple(esdata))
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
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.
939 Raises
940 ------
941 ImportError
942 If the escdf package is not available.
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 """
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)
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)
970 if verbose:
971 print('Reading Control Data')
972 response_th,spec_th,drive_th = read_transient_control_data(control_data)
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)
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)
1004 if verbose:
1005 print('Creating ESCDF Metadata Objects')
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)
1014 # Pull the spec into metadata
1015 return ((estrans, eschan, esspec[0]), tuple(esdata))
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
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.
1043 Raises
1044 ------
1045 ImportError
1046 If the escdf package is not available.
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).
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)
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()
1068 if verbose:
1069 print('Creating ESCDF Data Objects')
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'
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}')
1087 es_coh = from_data(
1088 f'{test_name_slug}_Coherence',mcoh)
1089 es_coh.ordinate_unit = '-'
1090 es_coh.abscissa_unit = 'Hz'
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)
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)
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)
1118 # System Identification Parameters
1119 esmod = from_rattlesnake_modal_parameters(
1120 f'{test_name_slug}_Params', modal_results_data)
1122 return ((esmod, eschan),
1123 tuple([es_th, es_frf, es_coh] + additional_data))
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
1132 # Create layout
1133 layout = QtWidgets.QVBoxLayout()
1135 # Add message label
1136 self.message_label = QtWidgets.QLabel(message)
1137 layout.addWidget(self.message_label)
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")
1160 layout.addWidget(self.input_widget)
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)
1170 # Connect buttons to their respective slots
1171 self.ok_button.clicked.connect(self.accept)
1172 self.cancel_button.clicked.connect(self.reject)
1174 self.setLayout(layout)
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
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
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
1215class ESCDFTableModel(QtCore.QAbstractTableModel):
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)
1227 def rowCount(self, parent=QtCore.QModelIndex()):
1228 return self.table_dimensions[0]
1230 def columnCount(self, parent=QtCore.QModelIndex()):
1231 return self.table_dimensions[1]
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)
1243 def data(self, index, role=QtCore.Qt.DisplayRole):
1244 if role == QtCore.Qt.DisplayRole:
1245 row = index.row()
1246 col = index.column()
1248 output_slice = self.get_slice(row, col)
1250 # Fetch the data lazily
1251 value = self.prop[output_slice]
1252 return str(value)
1253 return None
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
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)
1268 def valueFromText(self, text):
1269 # Override to interpret ":" as -1
1270 if text == ":":
1271 return -1
1272 return int(text)
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
1284class ESCDFVisualizer(QMainWindow):
1285 """An interactive window allowing users to explore an ESCDF file"""
1287 monofont = QtGui.QFont("monospace")
1288 monofont.setStyleHint(QtGui.QFont.TypeWriter)
1290 def __init__(self, escdf_file=None):
1291 """Create an ESCDF Visualizer Window to explore an ESCDF file.
1293 A filename or ESCDF object can be passed as an argment, or it can be
1294 loaded at a later point.
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.
1302 Returns
1303 -------
1304 None.
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()
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)
1390 # @trace_callback
1391 def update_tab(self, argument=None):
1392 self.update_viz_buttons()
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()
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!")
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!")
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)
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)
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)
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)
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)
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)
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)
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)
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}')
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
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()
1607 # @trace_callback
1608 def update_metadata_data(self, current = None, previous = None):
1609 self.update_viz_buttons()
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)
1619 # @trace_callback
1620 def load(self,escdf_file):
1621 # Load the file
1622 self.escdf = escdf.ESCDF.load(escdf_file)
1624 # Update the activity selector
1625 self.activity_selector.clear()
1626 for name in self.escdf.activities.names:
1627 self.activity_selector.addItem(name)
1629 self.data_array = []
1630 self.metadata_array = []
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()
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()
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
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
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
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)
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
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
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
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
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
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
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
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
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 )
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))
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())
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)
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)
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)