Skip to content

Commit c86e926

Browse files
committed
Bug Fixes and Documentation Improvements
1 parent 3aca53a commit c86e926

File tree

7 files changed

+73
-73
lines changed

7 files changed

+73
-73
lines changed

components/data_physics_interface.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,11 @@
2222
along with this program. If not, see <https://www.gnu.org/licenses/>.
2323
"""
2424

25-
2625
import ctypes
2726
from enum import Enum
2827
import numpy as np
2928
from numpy.ctypeslib import ndpointer
3029
from time import sleep,time
31-
import matplotlib.pyplot as plt
32-
33-
plt.close('all')
3430

3531
DEBUG = False
3632

components/nidaqmx_hardware_multitask.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ def set_parameters(self,test_data : DataAcquisitionParameters):
114114
self.reader = ni_read.AnalogMultiChannelReader(self.task.in_stream)
115115
self.read_data = np.zeros((len(self.task.ai_channels),test_data.samples_per_read))
116116
self.acquisition_delay = BUFFER_SIZE_FACTOR*test_data.samples_per_write
117+
print('Actual Acquisition Sample Rate: {:}'.format(self.task.timing.samp_clk_rate))
117118

118119
def start(self):
119120
"""Start acquiring data"""
@@ -339,12 +340,24 @@ def create_sources(self,channel_data : List[Channel]):
339340
channel_data : List[Channel] :
340341
A list of ``Channel`` objects defining the channels in the test
341342
"""
342-
self.write_trigger = '/'+channel_data[0].physical_device+'/ai/StartTrigger'
343343
# Get the physical devices
344344
physical_devices = list(set([ni.system.device.Device(channel.feedback_device).product_type
345345
for channel in channel_data
346346
if not (channel.feedback_device is None)
347347
and not (channel.feedback_device.strip() == '')]))
348+
# Check if it's a CDAQ device
349+
try:
350+
devices = [ni.system.device.Device(channel.feedback_device)
351+
for channel in channel_data
352+
if not (channel.feedback_device is None)
353+
and not (channel.feedback_device.strip() == '')]
354+
if len(devices) == 0:
355+
self.write_trigger = None # No output device
356+
else:
357+
chassis_device = devices[0].compact_daq_chassis_device
358+
self.write_trigger = [trigger for trigger in chassis_device.terminals if 'ai/StartTrigger' in trigger][0]
359+
except ni.DaqError:
360+
self.write_trigger = '/'+channel_data[0].physical_device+'/ai/StartTrigger'
348361
print('Output Devices: {:}'.format(physical_devices))
349362
self.tasks = [ni.Task() for device in physical_devices]
350363
index = 0
@@ -383,6 +396,7 @@ def set_parameters(self,test_data : DataAcquisitionParameters):
383396
task.triggers.start_trigger.trig_type = ni.constants.TriggerType.DIGITAL_EDGE
384397
task.out_stream.output_buf_size = self.buffer_size_factor*test_data.samples_per_write
385398
self.writers.append(ni_write.AnalogMultiChannelWriter(task.out_stream,auto_start=False))
399+
print('Actual Output Sample Rate: {:}'.format(task.timing.samp_clk_rate))
386400

387401
def start(self):
388402
"""Method to start acquiring data"""

components/output.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@ def output_signal(self,data):
252252
self.queue_container.input_output_sync_queue.put((environment,write_data[...,::self.output_oversample].copy()))
253253
self.environment_first_data[environment] = False
254254
self.hardware.write(write_data)
255+
else:
256+
if self.environment_first_data[environment]:
257+
self.queue_container.input_output_sync_queue.put((environment,0))
258+
self.environment_first_data[environment] = False
255259
# np.savez('test_data/output_data_check.npz',output_data = write_data)
256260
# Now check and see if we are starting up and start the hardare if so
257261
if self.startup:

components/random_vibration_sys_id_environment.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,6 +1009,8 @@ def enable_control(self,enabled):
10091009
widget.setEnabled(enabled)
10101010
for widget in [self.run_widget.stop_test_button]:
10111011
widget.setEnabled(not enabled)
1012+
if enabled:
1013+
self.run_timer.stop()
10121014

10131015
def update_run_time(self):
10141016
"""Updates the time that the control has been running on the GUI"""
@@ -1229,11 +1231,11 @@ def save_spectral_data(self):
12291231
self.environment_parameters.store_to_netcdf(group_handle)
12301232
# Create Variables for Spectral Data
12311233
group_handle.createDimension('drive_channels',self.last_transfer_function.shape[2])
1232-
var = group_handle.createVariable('frf_data_real','f8',('fft_lines','control_channels','drive_channels'))
1234+
var = group_handle.createVariable('frf_data_real','f8',('fft_lines','specification_channels','drive_channels'))
12331235
var[...] = self.last_transfer_function.real
1234-
var = group_handle.createVariable('frf_data_imag','f8',('fft_lines','control_channels','drive_channels'))
1236+
var = group_handle.createVariable('frf_data_imag','f8',('fft_lines','specification_channels','drive_channels'))
12351237
var[...] = self.last_transfer_function.imag
1236-
var = group_handle.createVariable('frf_coherence','f8',('fft_lines','control_channels'))
1238+
var = group_handle.createVariable('frf_coherence','f8',('fft_lines','specification_channels'))
12371239
var[...] = self.last_coherence.real
12381240
var = group_handle.createVariable('response_cpsd_real','f8',('fft_lines','specification_channels','specification_channels'))
12391241
var[...] = self.last_response_cpsd.real

components/random_vibration_sys_id_utilities.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
along with this program. If not, see <https://www.gnu.org/licenses/>.
2222
"""
2323

24-
2524
import os
2625
import numpy as np
2726
from scipy.io import loadmat

components/transient_sys_id_environment.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,9 @@ def store_to_netcdf(self,netcdf_group_handle : nc4._netCDF4.Group):
199199
netcdf_group_handle.control_python_function_parameters = self.control_python_function_parameters
200200
# Save the output signal
201201
netcdf_group_handle.createDimension('control_channels',len(self.control_channel_indices))
202+
netcdf_group_handle.createDimension('specification_channels',len(self.control_channel_indices))
202203
netcdf_group_handle.createDimension('signal_samples',self.signal_samples)
203-
var = netcdf_group_handle.createVariable('control_signal','f8',('control_channels','signal_samples'))
204+
var = netcdf_group_handle.createVariable('control_signal','f8',('specification_channels','signal_samples'))
204205
var[...] = self.control_signal
205206
# Control Channels
206207
var = netcdf_group_handle.createVariable('control_channel_indices','i4',('control_channels'))
@@ -887,29 +888,29 @@ def save_control_data(self):
887888
# Create Variables for Spectral Data
888889
group_handle.createDimension('drive_channels',self.last_transfer_function.shape[2])
889890
group_handle.createDimension('fft_lines',self.environment_parameters.sysid_frame_size//2 + 1)
890-
var = group_handle.createVariable('frf_data_real','f8',('fft_lines','control_channels','drive_channels'))
891+
var = group_handle.createVariable('frf_data_real','f8',('fft_lines','specification_channels','drive_channels'))
891892
var[...] = self.last_transfer_function.real
892-
var = group_handle.createVariable('frf_data_imag','f8',('fft_lines','control_channels','drive_channels'))
893+
var = group_handle.createVariable('frf_data_imag','f8',('fft_lines','specification_channels','drive_channels'))
893894
var[...] = self.last_transfer_function.imag
894-
var = group_handle.createVariable('frf_coherence','f8',('fft_lines','control_channels'))
895+
var = group_handle.createVariable('frf_coherence','f8',('fft_lines','specification_channels'))
895896
var[...] = self.last_coherence.real
896-
var = group_handle.createVariable('response_cpsd_real','f8',('fft_lines','control_channels','control_channels'))
897+
var = group_handle.createVariable('response_cpsd_real','f8',('fft_lines','specification_channels','specification_channels'))
897898
var[...] = self.last_response_cpsd.real
898-
var = group_handle.createVariable('response_cpsd_imag','f8',('fft_lines','control_channels','control_channels'))
899+
var = group_handle.createVariable('response_cpsd_imag','f8',('fft_lines','specification_channels','specification_channels'))
899900
var[...] = self.last_response_cpsd.imag
900901
var = group_handle.createVariable('drive_cpsd_real','f8',('fft_lines','drive_channels','drive_channels'))
901902
var[...] = self.last_reference_cpsd.real
902903
var = group_handle.createVariable('drive_cpsd_imag','f8',('fft_lines','drive_channels','drive_channels'))
903904
var[...] = self.last_reference_cpsd.imag
904-
var = group_handle.createVariable('response_noise_cpsd_real','f8',('fft_lines','control_channels','control_channels'))
905+
var = group_handle.createVariable('response_noise_cpsd_real','f8',('fft_lines','specification_channels','specification_channels'))
905906
var[...] = self.last_response_noise.real
906-
var = group_handle.createVariable('response_noise_cpsd_imag','f8',('fft_lines','control_channels','control_channels'))
907+
var = group_handle.createVariable('response_noise_cpsd_imag','f8',('fft_lines','specification_channels','specification_channels'))
907908
var[...] = self.last_response_noise.imag
908909
var = group_handle.createVariable('drive_noise_cpsd_real','f8',('fft_lines','drive_channels','drive_channels'))
909910
var[...] = self.last_reference_noise.real
910911
var = group_handle.createVariable('drive_noise_cpsd_imag','f8',('fft_lines','drive_channels','drive_channels'))
911912
var[...] = self.last_reference_noise.imag
912-
var = group_handle.createVariable('control_response','f8',('control_channels','signal_samples'))
913+
var = group_handle.createVariable('control_response','f8',('specification_channels','signal_samples'))
913914
var[...] = self.last_control_data
914915
var = group_handle.createVariable('control_drives','f8',('drive_channels','signal_samples'))
915916
var[...] = self.last_output_data

control_laws/transient_control_laws.py

Lines changed: 39 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -27,48 +27,71 @@ def pseudoinverse_control(
2727
last_excitation_signals = None, # Last excitation signal for drive-based control
2828
last_response_signals = None, # Last response signal for error correction
2929
):
30-
# Get a tolerance if specified
30+
# Parse the input arguments in extra_parameters
3131
rcond = 1e-15
3232
zero_impulse_after = None
33+
# Split it up into lines
3334
for entry in extra_parameters.split('\n'):
3435
try:
36+
# For each entry, split the key from the value using the colon
3537
field,value = entry.split(':')
38+
# Strip any whitespace
3639
field = field.strip()
40+
# Check the field to figure out which value to assign
3741
if field == 'rcond':
3842
rcond = float(value)
3943
elif field == 'zero_impulse_after':
4044
zero_impulse_after = float(value)
4145
else:
46+
# Report if we cannot understand the parameter
4247
print('Unrecognized Parameter: {:}, skipping...'.format(field))
4348
except ValueError:
49+
# Report if we cannot parse the line
4450
print('Unable to Parse Line {:}, skipping...'.format(entry))
4551

46-
# Compute impulse responses
52+
# Compute impulse responses using the IFFT of the transfer function
53+
# We will zero pad the IFFT to do interpolation in the frequency domain
54+
# to match the length of the required signal
4755
impulse_response = np.fft.irfft(transfer_function,axis=0)
4856

57+
# The impulse response should be going to zero at the end of the frame,
58+
# but practically there may be some gibbs phenomenon effects that make the
59+
# impulse response noncausal. If we zero pad, this might be wrong. We
60+
# therefore give the use the ability to zero out this non-causal poriton of
61+
# the impulse response.
4962
if zero_impulse_after is not None:
5063
# Remove noncausal portion
5164
impulse_response_abscissa = np.arange(impulse_response.shape[0])/sample_rate
5265
zero_indices = impulse_response_abscissa > zero_impulse_after
5366
impulse_response[zero_indices] = 0
5467

55-
# Zero pad the impulse response to create a signal that is long enough
56-
added_zeros = np.zeros((specification_signals.shape[-1]-impulse_response.shape[0],) + impulse_response.shape[1:])
68+
# Zero pad the impulse response to create a signal that is long enough for
69+
# the specification signal
70+
added_zeros = np.zeros((specification_signals.shape[-1]-impulse_response.shape[0],)
71+
+ impulse_response.shape[1:])
5772
full_impulse_response = np.concatenate((impulse_response,added_zeros),axis=0)
5873

59-
# Compute FRFs
74+
# Compute FRFs using the FFT from the impulse response. This is now
75+
# interpolated such that it matches the frequency spacing of the specification
76+
# signal
6077
interpolated_transfer_function = np.fft.rfft(full_impulse_response,axis=0)
6178

62-
# Perform convolution in frequency domain
79+
# Perform convolution by frequency domain multiplication
6380
signal_fft = np.fft.rfft(specification_signals,axis=-1)
81+
# Invert the FRF matrix using the specified rcond parameter
6482
inverted_frf = np.linalg.pinv(interpolated_transfer_function,rcond=rcond)
83+
# Multiply the inverted FRFs by the response spectra to get the drive spectra
6584
drive_signals_fft = np.einsum('ijk,ki->ij',inverted_frf,signal_fft)
6685

67-
# Zero pad the FFT to oversample
86+
# Zero pad the drive FFT to oversample to the output_oversample_factor
6887
drive_signals_fft_zero_padded = np.concatenate((drive_signals_fft[:-1],
69-
np.zeros((drive_signals_fft[:-1].shape[0]*(output_oversample_factor-1)+1,)+drive_signals_fft.shape[1:])),axis=0)
88+
np.zeros((drive_signals_fft[:-1].shape[0]*(output_oversample_factor-1)+1,)
89+
+drive_signals_fft.shape[1:])),axis=0)
7090

71-
drive_signals_oversampled = np.fft.irfft(drive_signals_fft_zero_padded.T,axis=-1)*output_oversample_factor
91+
# Finally, take the IFFT to get the time domain signal. We need to scale
92+
# by the output_oversample_factor due to how the IFFT is normalized.
93+
drive_signals_oversampled = np.fft.irfft(
94+
drive_signals_fft_zero_padded.T,axis=-1)*output_oversample_factor
7295
return drive_signals_oversampled
7396

7497
def pseudoinverse_control_generator():
@@ -153,17 +176,6 @@ def __init__(self,
153176
last_excitation_signals = None, # Last excitation signal for drive-based control
154177
last_response_signals = None, # Last response signal for error correction
155178
):
156-
"""
157-
Initializes the control law
158-
159-
Parameters
160-
----------
161-
162-
Returns
163-
-------
164-
None.
165-
166-
"""
167179
self.rcond = 1e-15
168180
self.zero_impulse_after = None
169181
for entry in extra_parameters.split('\n'):
@@ -202,17 +214,6 @@ def system_id_update(self,
202214
frames, # Number of frames in the CPSD and FRF matrices
203215
total_frames, # Total frames that could be in the CPSD and FRF matrices
204216
):
205-
"""
206-
Updates the control law with the data from the system identification
207-
208-
Parameters
209-
----------
210-
transfer_function : np.ndarray
211-
A complex 3d numpy ndarray with dimensions frequency lines x control
212-
channels x excitation sources representing the FRF matrix measured
213-
by the system identification process between drive voltages and
214-
control response
215-
"""
216217
# Compute impulse responses
217218
impulse_response = np.fft.irfft(transfer_function,axis=0)
218219

@@ -223,7 +224,8 @@ def system_id_update(self,
223224
impulse_response[zero_indices] = 0
224225

225226
# Zero pad the impulse response to create a signal that is long enough
226-
added_zeros = np.zeros((self.specification_signals.shape[-1]-impulse_response.shape[0],) + impulse_response.shape[1:])
227+
added_zeros = np.zeros((self.specification_signals.shape[-1]-impulse_response.shape[0],)
228+
+ impulse_response.shape[1:])
227229
full_impulse_response = np.concatenate((impulse_response,added_zeros),axis=0)
228230

229231
# Compute FRFs
@@ -236,34 +238,16 @@ def system_id_update(self,
236238

237239
# Zero pad the FFT to oversample
238240
drive_signals_fft_zero_padded = np.concatenate((drive_signals_fft[:-1],
239-
np.zeros((drive_signals_fft[:-1].shape[0]*(self.output_oversample_factor-1)+1,)+drive_signals_fft.shape[1:])),axis=0)
241+
np.zeros((drive_signals_fft[:-1].shape[0]*(self.output_oversample_factor-1)+1,)
242+
+drive_signals_fft.shape[1:])),axis=0)
240243

241-
self.drive_signals_oversampled = np.fft.irfft(drive_signals_fft_zero_padded.T,axis=-1)*self.output_oversample_factor
244+
self.drive_signals_oversampled = np.fft.irfft(
245+
drive_signals_fft_zero_padded.T,axis=-1)*self.output_oversample_factor
242246

243247
def control(self,
244248
last_excitation_signals = None, # Last excitation signal for drive-based control
245249
last_response_signals = None, # Last response signal for error correction
246-
) -> np.ndarray:
247-
"""
248-
Perform the control operations
249-
250-
Parameters
251-
----------
252-
last_excitation_signals : np.ndarray, optional
253-
The most recent output signal, which can be used for error-based
254-
control. The default is None.
255-
last_response_signals : np.ndarray, optional
256-
The most recent responses to the last output signals, which can be
257-
used for error-based control. The default is None.
258-
259-
Returns
260-
-------
261-
output_signal : np.ndarray
262-
A 2D numpy array consisting of number of outputs x signal samples *
263-
output_oversample_factor. This signal will be played directly to the
264-
shakers.
265-
"""
266-
250+
) -> np.ndarray:
267251
# We could modify the output signal based on new data that we obtained
268252
# Otherwise just output the same
269253

0 commit comments

Comments
 (0)