@@ -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
7497def 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