Coverage for src / sdynpy / signal_processing / sdynpy_srs.py: 3%

610 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-11 16:22 +0000

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

2""" 

3Created on Fri Nov 3 09:23:59 2023 

4 

5@author: dprohe 

6""" 

7 

8import numpy as np 

9from scipy.signal import lfilter 

10import matplotlib.pyplot as plt 

11from scipy.signal import oaconvolve 

12from scipy.optimize import minimize, NonlinearConstraint, nnls 

13 

14 

15def srs(signal, dt, frequencies=None, damping=0.05, spectrum_type=9, 

16 b_filter_weights=None, a_filter_weights=None): 

17 """ 

18 Computes shock response spectrum of a signal 

19 

20 Computes the shock response spectrum using a ramp invariant digital filter 

21 simulation of the single degree-of-freedom system. 

22 

23 Parameters 

24 ---------- 

25 signal : np.ndarray 

26 A shape (..., num_samples) ndarray containing the sampled signals to 

27 compute SRSs from. 

28 dt : float 

29 The time between samples (1/sample rate) 

30 frequencies : np.ndarray 

31 An iterable of frequency lines at which the shock response spectrum 

32 will be calculated. If not specified, it will be computed from the 

33 sample rate/10000 to the sample rate/4 over 50 lines. 

34 damping : float 

35 Fraction of critical damping to use in the SRS calculation (e.g. you 

36 should specify 0.03 to represent 3%, not 3). The default is 0.03. 

37 spectrum_type : int 

38 The type of spectrum desired: 

39 If `spectrum_type` > 0 (pos) then the SRS will be a base 

40 acceleration-absolute acceleration model 

41 If `spectrum_type` < 0 (neg) then the SRS will be a base acceleration-relative 

42 displacement model (expressed in equivalent static acceleration units). 

43 If abs(`spectrum_type`) is: 

44 1--positive primary, 2--negative primary, 3--absolute maximum primary 

45 4--positive residual, 5--negative residual, 6--absolute maximum residual 

46 7--largest of 1&4, maximum positive, 8--largest of 2&5, maximum negative 

47 9 -- maximax, the largest absolute value of 1-8 

48 10 -- returns a matrix s(9,length(fn)) with all the types 1-9. 

49 b_filter_weights : np.ndarray, optional 

50 Optional filter weights with shape (frequencies.size,3). 

51 The default is to automatically compute filter weights using 

52 `sdof_ramp_invariant_filter_weights`. 

53 a_filter_weights : np.ndarray, optional 

54 Optional filter weights with shape (frequencies.size,3). 

55 The default is to automatically compute filter weights using 

56 `sdof_ramp_invariant_filter_weights`. 

57 

58 Returns 

59 ------- 

60 srs : np.ndarray 

61 The shock response spectrum at the frequencies specified. If 

62 `spectrum_type` == 10 or -10, then this will have shape 

63 (... x 9 x len(frequencies)) where each row is a different type of SRS 

64 frequencies : np.ndarray 

65 The frequencies at which the SRSs were computed. 

66 """ 

67 # Compute default parameters 

68 sample_rate = 1/dt 

69 if frequencies is None: 

70 frequencies = np.logspace(np.log10(sample_rate/1e4), 

71 np.log10(sample_rate/4), 

72 50) 

73 else: 

74 frequencies = np.array(frequencies).flatten() 

75 if frequencies.size == 0: 

76 raise ValueError('Frequencies must have nonzero size') 

77 signal = np.array(signal) 

78 num_samples = signal.shape[-1] 

79 

80 # Compute bad parameters 

81 abs_type = abs(spectrum_type) 

82 if abs_type > 10: 

83 raise ValueError("`spectrum_type` must be in the range [-10,10]") 

84 if abs_type == 0: 

85 raise ValueError('`spectrum_type` must not be 0') 

86 if sample_rate <= 0: 

87 raise ValueError('`dt` must be positive') 

88 if frequencies.min() < 0: 

89 raise ValueError('`frequencies` must all be positive') 

90 if damping < 0: 

91 raise ValueError('`damping` must be positive') 

92 if damping == 0: 

93 print('Warning: Damping is Zero in SRS calculation!') 

94 

95 # Get the filter coefficients if not specified 

96 if b_filter_weights is None or a_filter_weights is None: 

97 b_filter_weights, a_filter_weights = sdof_ramp_invariant_filter_weights( 

98 frequencies, sample_rate, damping, spectrum_type) 

99 else: 

100 b_filter_weights = np.atleast_2d(np.array(b_filter_weights)) 

101 a_filter_weights = np.atleast_2d(np.array(a_filter_weights)) 

102 

103 if b_filter_weights.shape != (frequencies.size, 3): 

104 raise ValueError('`b_filter_weights` and `a_filter_weights` must have shape (frequencies.size,3)') 

105 

106 srs_output = np.zeros(signal.shape[:-1] 

107 + ((9,) if np.abs(spectrum_type) == 10 else ()) 

108 + (frequencies.size,)) 

109 for i_freq, (frequency, b, a) in enumerate(zip( 

110 frequencies, b_filter_weights, a_filter_weights)): 

111 # Append enough zeros to get 1/100 of the residual response 

112 zeros_to_append = int(np.max([2, np.ceil(0.01*sample_rate/frequency)])) 

113 signals_extended = np.concatenate(( 

114 signal, np.zeros(signal.shape[:-1] + (zeros_to_append,))), axis=-1) 

115 

116 # Filter the signal 

117 response = sdof_filter(b, a, signals_extended) 

118 

119 # Now compute the SRS parameters 

120 # Primary response 

121 primary_maximum = np.array(np.max(response[..., :num_samples], axis=-1)) 

122 primary_maximum[primary_maximum < 0] = 0 

123 primary_minimum = np.array(np.abs(np.min(response[..., :num_samples], axis=-1))) 

124 primary_minimum[primary_minimum < 0] = 0 

125 primary_abs = np.array(np.max([primary_maximum, primary_minimum], axis=0)) 

126 

127 # Now compute the residual response. we need time steps and 

128 # responses at two points 

129 residual_times = [0, (zeros_to_append-1)/sample_rate] 

130 residual_responses = np.concatenate([response[..., num_samples, np.newaxis], 

131 response[..., num_samples+zeros_to_append-1, np.newaxis]], 

132 axis=-1) 

133 

134 # Compute the peak response after the structure has stopped responding 

135 peak_residual_responses = sdof_free_decay_peak_response( 

136 residual_responses, residual_times, frequency, damping) 

137 

138 # Pull out the residual peaks 

139 residual_maximum = np.array(np.max(peak_residual_responses, axis=-1)) 

140 residual_maximum[residual_maximum < 0] = 0 

141 residual_minimum = np.array(np.abs(np.min(peak_residual_responses, axis=-1))) 

142 residual_minimum[residual_minimum < 0] = 0 

143 residual_abs = np.array(np.max([residual_maximum, residual_minimum], axis=0)) 

144 

145 all_srs_this_frequency_line = np.array([ 

146 primary_maximum, primary_minimum, primary_abs, 

147 residual_maximum, residual_minimum, residual_abs, 

148 np.max([primary_maximum, residual_maximum], axis=0), 

149 np.max([primary_minimum, residual_minimum], axis=0), 

150 np.max([primary_abs, residual_abs], axis=0)]) 

151 

152 if abs_type < 10: 

153 srs_output[..., i_freq] = all_srs_this_frequency_line[abs_type-1] 

154 else: 

155 srs_output[..., i_freq] = np.moveaxis(all_srs_this_frequency_line, 0, -1) 

156 return srs_output, frequencies 

157 

158 

159def sdof_ramp_invariant_filter_weights(frequencies, sample_rate, damping, 

160 spectrum_type): 

161 """ 

162 Computes filter weights for SDOF resonators using a ramp-invariant filter. 

163 

164 The weights are used in conjunction with the function 

165 `sdof_filter` and `srs` to calculate the shock response 

166 spectrum of an acceleration time history using a ramp 

167 invarient simulation. 

168 

169 Parameters 

170 ---------- 

171 frequencies : np.ndarray 

172 The frequencies at which filters should be computed 

173 sample_rate : float 

174 The sample rate of the measurement 

175 damping : float 

176 The fraction of critical damping for the system (e.g. 0.03, not 3 for 3%) 

177 spectrum_type : int 

178 See `spectrum_type` argument for compute_srs 

179 

180 Returns 

181 ------- 

182 b : np.ndarray 

183 Filter coefficients with shape (frequencies.shape,3) 

184 a : np.ndarray 

185 Filter coefficients with shape (frequencies.shape,3) 

186 """ 

187 # Make sure it's a numpy array 

188 frequencies = np.array(frequencies) 

189 # Preallocate arrays 

190 a = np.ones(frequencies.shape+(3,)) 

191 b = np.ones(frequencies.shape+(3,)) 

192 

193 normalized_frequencies = 2*np.pi*frequencies/sample_rate 

194 

195 small_freq_indices = normalized_frequencies < 0.001 

196 

197 # Small frequencies 

198 small_freqs = normalized_frequencies[small_freq_indices] 

199 

200 # 2*z*w + w*w*(1-2*z*z) where z is damping and w is normalized frequencies 

201 a[small_freq_indices, 1] = ( 

202 2*damping*small_freqs 

203 + small_freqs**2*(1-2*damping**2) 

204 ) 

205 # -2*z*w + 2*z*z*w*w where z is damping and w is normalized frequencies 

206 a[small_freq_indices, 2] = ( 

207 -2*damping*small_freqs 

208 + 2*damping**2*small_freqs**2 

209 ) 

210 

211 if spectrum_type > 0: 

212 # Absolute Acceleration Model 

213 # z*w + (w*w)*( (1/6) - 2*z*z/3 ); 

214 b[small_freq_indices, 0] = ( 

215 damping*small_freqs 

216 + small_freqs**2 

217 * (1/6 - 2*damping**2/3) 

218 ) 

219 # 2*w*w*(1-z*z)/3 

220 b[small_freq_indices, 1] = ( 

221 2*small_freqs**2*(1-damping**2)/3 

222 ) 

223 # -z*w + w*w*( (1/6) - 4*z*z/3 ); 

224 b[small_freq_indices, 2] = ( 

225 - damping*small_freqs 

226 + small_freqs**2 

227 * (1/6 - 4*damping**2/3) 

228 ) 

229 else: 

230 # Relative Displacement Model 

231 # -w*w/6; 

232 b[small_freq_indices, 0] = -small_freqs**2/6 

233 # -2*w*w/3; 

234 b[small_freq_indices, 1] = -2*small_freqs**2/3 

235 # -w*w/6; 

236 b[small_freq_indices, 2] = -small_freqs**2/6 

237 

238 # Large frequencies 

239 large_freqs = normalized_frequencies[~small_freq_indices] 

240 

241 # Define some helper variables 

242 sq = np.sqrt(1-damping**2) 

243 e = np.exp(-damping*large_freqs) 

244 wd = large_freqs*sq 

245 sp = e*np.sin(wd) 

246 fact = (2*damping**2 - 1)*sp/sq 

247 c = e*np.cos(wd) 

248 

249 a[~small_freq_indices, 1] = 2*(1-c) 

250 a[~small_freq_indices, 2] = -1 + e**2 

251 

252 if spectrum_type > 0: 

253 # Absolute Acceleration Model 

254 spwd = sp/wd 

255 b[~small_freq_indices, 0] = 1-spwd 

256 b[~small_freq_indices, 1] = 2*(spwd - c) 

257 b[~small_freq_indices, 2] = e**2 - spwd 

258 else: 

259 # Relative Displacement Model 

260 b[~small_freq_indices, 0] = -(2*damping*(c-1) + fact + large_freqs)/large_freqs 

261 b[~small_freq_indices, 1] = -(-2*c*large_freqs + 2*damping*(1-e**2) - 2*fact)/large_freqs 

262 b[~small_freq_indices, 2] = -(e**2*(large_freqs+2*damping) - 2*damping*c + fact)/large_freqs 

263 

264 return b, a 

265 

266 

267def sdof_filter(b, a, signal, zi=None): 

268 """ 

269 Applies a filter to simulate a single degree of freedom system 

270 

271 Parameters 

272 ---------- 

273 b : np.ndarray 

274 Size 3 array representing filter coefficients used by scipy.signal.lfilter 

275 a : np.array 

276 Size 3 array representing filter coefficients used by scipy.signal.lfilter 

277 signal : np.ndarray 

278 The signals that are to be filtered 

279 zi : np.ndarray, optional 

280 Optional initial state for the filters, having length 

281 `max(len(a), len(b)) - 1`. If not specified, zero initial conditions 

282 are assumed. 

283 

284 Returns 

285 ------- 

286 filtered_signal : np.ndarray 

287 The filtered signal 

288 

289 """ 

290 aa = np.array([a[0], a[1]-2, a[2] + 1]) 

291 return lfilter(b, aa, signal, axis=-1, zi=zi) 

292 

293 

294def sdof_free_decay_peak_response(responses, times_at_responses, frequency, damping): 

295 """ 

296 Calculates peak response of a freely-decaying sdof system. 

297 

298 The residual response is the peak response of the sdof system 

299 as it decays after the input has ended, i.e., the input is zero. 

300 The first two peaks in the decaying response will be the largest. 

301 One peak will be negative and one positive. We don't know before 

302 the calculation if the first and largest peak in amplitude will be 

303 positive or negative. 

304 

305 Parameters 

306 ---------- 

307 responses : np.array 

308 A (...,2) shape array giving the response at two time steps 

309 times_at_responses : np.array 

310 A length-2 array giving the time values that the responses occur at 

311 frequency : float 

312 The frequency at which the computation is performed 

313 damping : float 

314 The fraction of critical damping for the system (e.g. 0.03, not 3 for 3%) 

315 

316 Returns 

317 ------- 

318 response_peaks : np.ndarray 

319 A shape (...,2) array giving the amplitude at the two peaks after the 

320 response has decayed 

321 

322 Notes 

323 ----- 

324 The response has the form 

325 a(t) = exp(-zeta wn t)[z[0]sin(wd t) + z[1]cos(wd t)]. 

326 If I know a(t) at two values of time, t, I can calculate the constants 

327 z[0] and z[1]. The general form of the response can then be solved for 

328 the maximum by finding the time of the first maximum by setting the 

329 derivative to zero. Then substituting the time of the maximum response 

330 back into the general equation to find the maximum response. 

331 The second peak will occur half a cycle later. 

332 

333 """ 

334 responses = np.array(responses) 

335 times_at_responses = np.array(times_at_responses) 

336 # Set up some initial helper variables 

337 fd = frequency*np.sqrt(1-damping*damping) 

338 wd = 2*np.pi*fd 

339 wn = 2*np.pi*frequency 

340 wdt = wd*times_at_responses 

341 e = np.exp(-wn*damping*times_at_responses) 

342 sd = np.sin(wdt) 

343 cd = np.cos(wdt) 

344 c = e[:, np.newaxis]*np.array((sd, cd)).T 

345 z = np.linalg.solve(c, responses[..., np.newaxis])[..., 0] 

346 

347 # the response has the form 

348 # a(t) = exp(-zeta wn t)[z[0]sin(wd t) + z[1]cos(wd t)] 

349 # the first maximum response will occur at the time tmax 

350 tmax = np.array((1/wd) * np.arctan((wd*z[..., 0] - damping*wn*z[..., 1])/(wd*z[..., 1] - damping*wn*z[..., 0]))) 

351 tmax[tmax < 0] = tmax[tmax < 0] + np.pi/wd 

352 tmax[tmax > np.pi/wd] = tmax[tmax > np.pi/wd] - np.pi/wd 

353 

354 # We can now plug it into the equation to find the maximum 

355 response_peaks = [] 

356 response_peaks.append(np.exp(-damping*wn*tmax)*(z[..., 0]*np.sin(wd*tmax) + z[..., 1]*np.cos(wd*tmax))) 

357 tmin = tmax + np.pi/wd 

358 response_peaks.append(np.exp(-damping*wn*tmin)*(z[..., 0]*np.sin(wd*tmin) + z[..., 1]*np.cos(wd*tmin))) 

359 return np.moveaxis(response_peaks, 0, -1) 

360 

361 

362def octspace(low, high, points_per_octave): 

363 """ 

364 Constructs octave spacing between low and high values 

365 

366 Parameters 

367 ---------- 

368 low : float 

369 Starting value for the spacing 

370 high : float 

371 Upper value for the spacing 

372 points_per_octave : int 

373 Number of points per octave 

374 

375 Returns 

376 ------- 

377 octave_points : np.ndarray 

378 Octave-spaced points 

379 """ 

380 num_octaves = np.log2(high/low) 

381 num_steps = np.ceil(num_octaves*points_per_octave) 

382 point_indices = np.arange(num_steps+1) 

383 log_points = np.log2(low) + num_octaves/num_steps*point_indices 

384 points = 2**log_points 

385 return points 

386 

387 

388def sum_decayed_sines(sample_rate, block_size, 

389 sine_frequencies=None, sine_tone_range=None, sine_tone_per_octave=None, 

390 sine_amplitudes=None, sine_decays=None, sine_delays=None, 

391 required_srs=None, srs_breakpoints=None, 

392 srs_damping=0.05, srs_type=9, 

393 compensation_frequency=None, compensation_decay=0.95, 

394 # Paramters for the iteration 

395 number_of_iterations=3, convergence=0.8, 

396 error_tolerance=0.05, 

397 tau=None, num_time_constants=None, decay_resolution=None, 

398 scale_factor=1.02, 

399 acceleration_factor=1.0, 

400 plot_results=False, srs_frequencies=None, 

401 ignore_compensation_pulse = False, 

402 verbose=False 

403 ): 

404 """ 

405 Generate a Sum of Decayed Sines signal given an SRS. 

406 

407 Note that there are many approaches to do this, with many optional arguments 

408 so please read the documentation carefully to understand which arguments 

409 must be passed to the function. 

410 

411 Parameters 

412 ---------- 

413 sample_rate : float 

414 The sample rate of the generated signal. 

415 block_size : int 

416 The number of samples in the generated signal. 

417 sine_frequencies : np.ndarray, optional 

418 The frequencies of the sine tones. If this argument is not specified, 

419 then the `sine_tone_range` argument must be specified. 

420 sine_tone_range : np.ndarray, optional 

421 A length-2 array containing the minimum and maximum sine tone to 

422 generate. If this is argument is not specified, then the 

423 `sine_frequencies` argument must be specified instead. 

424 sine_tone_per_octave : int, optional 

425 The number of sine tones per octave. If not specified along with 

426 `sine_tone_range`, then a default value of 4 will be used if the 

427 `srs_damping` is >= 0.05. Otherwise, the formula of 

428 `sine_tone_per_octave = 9 - srs_damping*100` will be used. 

429 sine_amplitudes : np.ndarray, optional 

430 The initial amplitude of the sine tones used in the optimization. If 

431 not specified, they will be set to the value of the SRS at each frequency 

432 divided by the quality factor of the SRS. 

433 sine_decays : np.ndarray, optional 

434 An array of decay value time constants (often represented by variable 

435 tau). Tau is the time for the amplitude of motion to decay 63% defined 

436 by the equation `1/(2*np.pi*freq*zeta)` where `freq` is the frequency 

437 of the sine tone and `zeta` is the fraction of critical damping. 

438 If not specified, then either the `tau` or `num_time_constants` 

439 arguments must be specified instead. 

440 sine_delays : np.ndarray, optional 

441 An array of delay values for the sine components. If not specified, 

442 all tones will have zero delay. 

443 required_srs : np.ndarray, optional 

444 The SRS to match defined at each of the `sine_frequencies`. If this 

445 argument is not passed, then the `srs_breakpoints` argument must be 

446 passed instead. The default is None. 

447 srs_breakpoints : np.ndarray, optional 

448 A numpy array with shape `(n,2)` where the first column is the 

449 frequencies at which each breakpoint occurs, and the second column is 

450 the value of the SRS at that breakpoint. SRS values at the 

451 `sine_frequencies` will be interpolated in a log-log sense from this 

452 breakpoint array. If this argument is not specified, then the 

453 `required_srs` must be passed instead. 

454 srs_damping : float, optional 

455 Fraction of critical damping to use in the SRS calculation (e.g. you 

456 should specify 0.03 to represent 3%, not 3). The default is 0.03. 

457 srs_type : int 

458 The type of spectrum desired: 

459 If `srs_type` > 0 (pos) then the SRS will be a base 

460 acceleration-absolute acceleration model 

461 If `srs_type` < 0 (neg) then the SRS will be a base acceleration-relative 

462 displacement model (expressed in equivalent static acceleration units). 

463 If abs(`srs_type`) is: 

464 1--positive primary, 2--negative primary, 3--absolute maximum primary 

465 4--positive residual, 5--negative residual, 6--absolute maximum residual 

466 7--largest of 1&4, maximum positive, 8--largest of 2&5, maximum negative 

467 9 -- maximax, the largest absolute value of 1-8 

468 10 -- returns a matrix s(9,length(fn)) with all the types 1-9. 

469 compensation_frequency : float 

470 The frequency of the compensation pulse. If not specified, it will be 

471 set to 1/3 of the lowest sine tone 

472 compensation_decay : float 

473 The decay value for the compensation pulse. If not specified, it will 

474 be set to 0.95. 

475 number_of_iterations : int, optional 

476 The number of iterations to perform. At least two iterations should be 

477 performed. 3 iterations is preferred, and will be used if this argument 

478 is not specified. 

479 convergence : float, optional 

480 The fraction of the error corrected each iteration. The default is 0.8. 

481 error_tolerance : float, optional 

482 Allowable relative error in the SRS. The default is 0.05. 

483 tau : float, optional 

484 If a floating point number is passed, then this will be used for the 

485 `sine_decay` values. Alternatively, a dictionary can be passed with 

486 the keys containing a length-2 tuple specifying the minimum and maximum 

487 frequency range, and the value specifying the value of `tau` within that 

488 frequency range. If this latter approach is used, all `sine_frequencies` 

489 must be contained within a frequency range. If this argument is not 

490 specified, then either `sine_decays` or `num_time_constants` must be 

491 specified instead. 

492 num_time_constants : int, optional 

493 If an integer is passed, then this will be used to set the `sine_decay` 

494 values by ensuring the specified number of time constants occur in the 

495 `block_size`. Alternatively, a dictionary can be passed with the keys 

496 containing a length-2 tuple specifying the minimum and maximum 

497 frequency range, and the value specifying the value of 

498 `num_time_constants` over that frequency range. If this latter approach 

499 is used, all `sine_frequencies` must be contained within a frequency 

500 range. If this argument is not specified, then either `sine_decays` or 

501 `tau` must be specified instead. 

502 decay_resolution : float, optional 

503 A scalar identifying the resolution of the fractional decay rate 

504 (often known by the variable `zeta`). The decay parameters will be 

505 rounded to this value. The default is to not round. 

506 scale_factor : float, optional 

507 A scaling applied to the sine tone amplitudes so the achieved SRS better 

508 fits the specified SRS, rather than just touching it. The default is 1.02. 

509 acceleration_factor : float, optional 

510 Optional scale factor to convert acceleration into velocity and 

511 displacement. For example, if sine amplitudes are in G and displacement 

512 is desired in inches, the acceleration factor should be set to 386.089. 

513 If sine amplitudes are in G and displacement is desired in meters, the 

514 acceleration factor should be set to 9.80665. The default is 1, which 

515 assumes consistent units (e.g. acceleration in m/s^2, velocity in m/s, 

516 displacement in m). 

517 plot_results : bool, optional 

518 If True, a figure will be plotted showing the acceleration, velocity, 

519 and displacement signals, as well as the desired and achieved SRS. 

520 srs_frequencies : np.ndarray, optional 

521 If specified, these frequencies will be used to compute the SRS that 

522 will be plotted when the `plot_results` value is `True`. 

523 ignore_compensation_pulse : bool, optional 

524 If True, the compensation pulse will be ignored. 

525 verbose : True, optional 

526 If True, additional diagnostics will be printed to the console. 

527 

528 Returns 

529 ------- 

530 acceleration_signal : ndarray 

531 The acceleration signal that satisfies the SRS. 

532 velocity_signal : ndarray 

533 The velocity of the acceleration signal. 

534 displacement_signal : ndarray 

535 The displacement of the acceleration signal. 

536 sine_frequencies : ndarray 

537 An array of frequencies for each sine tone, including the compensation 

538 pulse 

539 sine_amplitudes : ndarray 

540 An array of amplitudes for each sine tone, including the compensation 

541 pulse 

542 sine_decays : ndarray 

543 An array of decay values for each sine tone, including the compensation 

544 pulse 

545 sine_delays : ndarray 

546 An array of delay values for each sine tone, including the compensation 

547 pulse 

548 fig : matplotlib.Figure 

549 A reference to the plotted figure. Only returned if `plot_results` is 

550 `True`. 

551 ax : matplotlib.Axes 

552 A reference to the plotted axes. Only returned if `plot_results` is 

553 `True`. 

554 

555 """ 

556 # Handle the sine tone frequencies 

557 if sine_frequencies is None and sine_tone_range is None: 

558 raise ValueError('Either `sine_frequencies` or `sine_tone_range` must be specified') 

559 if sine_frequencies is not None and sine_tone_range is not None: 

560 raise ValueError('`sine_frequencies` can not be specified simultaneously with `sine_tone_range`') 

561 if sine_frequencies is None: 

562 # Create sine tones 

563 if sine_tone_per_octave is None: 

564 sine_tone_per_octave = int(np.floor(9-srs_damping*100)) 

565 sine_frequencies = octspace(sine_tone_range[0], sine_tone_range[1], 

566 sine_tone_per_octave) 

567 # Now set up the SRS 

568 if required_srs is None and srs_breakpoints is None: 

569 raise ValueError('Either `required_srs` or `srs_breakpoints` must be specified') 

570 if required_srs is not None and srs_breakpoints is not None: 

571 raise ValueError('`required_srs` can not be specified simultaneously with `srs_breakpoints`') 

572 if required_srs is None: 

573 required_srs = loginterp(sine_frequencies, 

574 srs_breakpoints[:, 0], 

575 srs_breakpoints[:, 1]) 

576 if sine_amplitudes is None: 

577 srs_amplitudes = required_srs.copy() 

578 srs_amplitudes[np.arange(srs_amplitudes.size) % 2 == 0] *= -1 

579 quality = 1/(2*srs_damping) 

580 srs_amplitudes /= quality 

581 sine_amplitudes = srs_amplitudes 

582 

583 if sine_delays is None: 

584 sine_delays = np.zeros(sine_frequencies.size) 

585 

586 if compensation_frequency is None: 

587 compensation_frequency = np.min(sine_frequencies)/3 

588 

589 # Set up decay terms 

590 decay_terms_specified = 0 

591 if sine_decays is not None: 

592 decay_terms_specified += 1 

593 if tau is not None: 

594 decay_terms_specified += 1 

595 if num_time_constants is not None: 

596 decay_terms_specified += 1 

597 if decay_terms_specified == 0: 

598 raise ValueError('One of `sine_decays`, `tau`, or `num_time_constants` must be specified') 

599 if decay_terms_specified > 1: 

600 raise ValueError('Only one of `sine_decays`, `tau`, or `num_time_constants` can be specified') 

601 

602 # Now check and see which is defined 

603 if num_time_constants is not None: 

604 period = block_size / sample_rate 

605 if isinstance(num_time_constants, dict): 

606 tau = {} 

607 for freq_range, num_time_constant in num_time_constants.items(): 

608 tau[freq_range] = period / num_time_constant 

609 else: 

610 tau = period/num_time_constants 

611 if tau is not None: 

612 sine_decays = [] 

613 for freq in sine_frequencies: 

614 if isinstance(tau, dict): 

615 this_decay = None 

616 for freq_range, this_tau in tau.items(): 

617 if freq_range[0] <= freq <= freq_range[1]: 

618 this_decay = 1/(2*np.pi*freq*this_tau) 

619 break 

620 if this_decay is None: 

621 raise ValueError('No frequency range matching frequency {:} was found in the specified decay parameters.'.format(freq)) 

622 sine_decays.append(this_decay) 

623 else: 

624 sine_decays.append(1/(2*np.pi*freq*tau)) 

625 sine_decays = np.array(sine_decays) 

626 # Otherwise we just keep the specified sine_decays 

627 

628 # Now handle the minimum resolution on the decay 

629 if decay_resolution is not None: 

630 sine_decays = decay_resolution*np.round(sine_decays/decay_resolution) 

631 

632 if compensation_frequency is None: 

633 compensation_frequency = np.min(sine_frequencies)/3 

634 

635 # Now we can actually run the generation process 

636 (acceleration_signal, used_frequencies, used_amplitudes, used_decays, 

637 used_delays, used_comp_frequency, used_comp_amplitude, used_comp_decay, 

638 used_comp_delay) = _sum_decayed_sines( 

639 sine_frequencies, sine_amplitudes, sine_decays, sine_delays, 

640 scale_factor*required_srs, compensation_frequency, compensation_decay, 

641 sample_rate, block_size, srs_damping, srs_type, number_of_iterations, 

642 convergence, error_tolerance, ignore_compensation_pulse, verbose) 

643 

644 all_frequencies = np.concatenate((used_frequencies, 

645 [used_comp_frequency])) 

646 all_amplitudes = np.concatenate((used_amplitudes, 

647 [used_comp_amplitude])) 

648 all_decays = np.concatenate((used_decays, 

649 [used_comp_decay])) 

650 all_delays = np.concatenate((used_delays, 

651 [used_comp_delay])) 

652 # Now compute displacement and velocity 

653 velocity_signal, displacement_signal = sum_decayed_sines_displacement_velocity( 

654 all_frequencies, all_amplitudes, all_decays, all_delays, sample_rate, 

655 block_size, acceleration_factor) 

656 

657 return_vals = (acceleration_signal, velocity_signal, displacement_signal, 

658 all_frequencies, all_amplitudes, all_decays, all_delays) 

659 

660 # Plot the results 

661 if plot_results: 

662 fig, ax = plt.subplots(2, 2, figsize=(8, 6)) 

663 times = np.arange(block_size)/sample_rate 

664 ax[0, 0].plot(times, acceleration_signal) 

665 ax[0, 0].set_ylabel('Acceleration') 

666 ax[0, 0].set_xlabel('Time (s)') 

667 ax[0, 1].plot(times, velocity_signal) 

668 ax[0, 1].set_ylabel('Velocity') 

669 ax[0, 1].set_xlabel('Time (s)') 

670 ax[1, 0].plot(times, displacement_signal) 

671 ax[1, 0].set_ylabel('Displacement') 

672 ax[1, 0].set_xlabel('Time (s)') 

673 # Compute SRS 

674 if srs_frequencies is None: 

675 srs_frequencies = sine_frequencies 

676 this_srs, this_frequencies = srs( 

677 acceleration_signal, 1/sample_rate, srs_frequencies, 

678 srs_damping, srs_type) 

679 if srs_breakpoints is None: 

680 srs_abscissa = sine_frequencies 

681 srs_ordinate = required_srs 

682 else: 

683 srs_abscissa, srs_ordinate = srs_breakpoints.T 

684 ax[1, 1].plot(srs_abscissa, srs_ordinate, 'k--') 

685 ax[1, 1].plot(this_frequencies, this_srs) 

686 ax[1, 1].set_ylabel('SRS ({:0.2f}% damping)'.format(srs_damping*100)) 

687 ax[1, 1].set_xlabel('Frequency (Hz)') 

688 ax[1, 1].set_yscale('log') 

689 ax[1, 1].set_xscale('log') 

690 fig.tight_layout() 

691 return_vals += (fig, ax) 

692 ax[1, 1].legend(('Reference', 'Decayed Sine')) 

693 return return_vals 

694 

695 

696def _sum_decayed_sines(sine_frequencies, sine_amplitudes, 

697 sine_decays, sine_delays, 

698 required_srs, 

699 compensation_frequency, compensation_decay, 

700 sample_rate, block_size, 

701 srs_damping, srs_type, 

702 number_of_iterations=3, convergence=0.8, 

703 error_tolerance=0.05, ignore_compensation_pulse = False, 

704 verbose=False): 

705 """ 

706 Optimizes the amplitudes of sums of decayed sines 

707 

708 Parameters 

709 ---------- 

710 sine_frequencies : ndarray 

711 An array of frequencies for each sine tone 

712 sine_amplitudes : ndarray 

713 An array of amplitudes for each sine tone 

714 sine_decays : ndarray 

715 An array of decay values for each sine tone 

716 sine_delays : ndarray 

717 An array of delay values for each sine tone 

718 required_srs : ndarray 

719 An array of SRS values for each frequency in `sine_frequencies` 

720 compensation_frequency : float 

721 The frequency of the compensation pulse 

722 compensation_decay : float 

723 The decay value for the compensation pulse 

724 sample_rate : float 

725 The sample rate of the signal 

726 block_size : int 

727 The number of samples in the signal 

728 srs_damping : float 

729 Fraction of critical damping to use in the SRS calculation (e.g. you 

730 should specify 0.03 to represent 3%, not 3). The default is 0.03. 

731 srs_type : int 

732 The type of spectrum desired: 

733 If `spectrum_type` > 0 (pos) then the SRS will be a base 

734 acceleration-absolute acceleration model 

735 If `spectrum_type` < 0 (neg) then the SRS will be a base acceleration-relative 

736 displacement model (expressed in equivalent static acceleration units). 

737 If abs(`spectrum_type`) is: 

738 1--positive primary, 2--negative primary, 3--absolute maximum primary 

739 4--positive residual, 5--negative residual, 6--absolute maximum residual 

740 7--largest of 1&4, maximum positive, 8--largest of 2&5, maximum negative 

741 9 -- maximax, the largest absolute value of 1-8 

742 10 -- returns a matrix s(9,length(fn)) with all the types 1-9. 

743 number_of_iterations : int 

744 The number of iterations that will be performed. The default is 3. 

745 convergence : float, optional 

746 The fraction of the error corrected each iteration. The default is 0.8. 

747 error_tolerance : float, optional 

748 Allowable relative error in the SRS. The default is 0.05. 

749 ignore_compensation_pulse : bool, optional 

750 If True, ignores the compensation pulse. Default is false. 

751 verbose : bool, optional 

752 If True, information on the interations will be provided. The default 

753 is False. 

754 

755 Returns 

756 ------- 

757 this_signal : np.ndarray 

758 A numpy array containing the generated signal 

759 sine_frequencies : ndarray 

760 An array of frequencies for each sine tone 

761 sine_amplitudes : ndarray 

762 An array of amplitudes for each sine tone 

763 sine_decays : ndarray 

764 An array of decay values for each sine tone 

765 sine_delays : ndarray 

766 An array of delay values for each sine tone 

767 this_compensation_frequency : float 

768 The frequency of the compensation term. 

769 this_compensation_amplitude : float 

770 The amplitude of the compensation term. 

771 this_compensation_decay : float 

772 The decay constant of the compensation term 

773 this_compensation_delay : float 

774 The delay constant of the compenstation term 

775 """ 

776 for i in range(number_of_iterations): 

777 if verbose: 

778 print('Starting Iteration {:}'.format(i+1)) 

779 # Compute updated sine amplitudes and compensation terms 

780 this_sine_amplitudes, this_compensation_amplitude, this_compensation_delay = ( 

781 _sum_decayed_sines_single_iteration( 

782 sine_frequencies, sine_amplitudes, sine_decays, sine_delays, 

783 required_srs, compensation_frequency, compensation_decay, 

784 sample_rate, block_size, srs_damping, srs_type, 

785 number_of_iterations=10, convergence=convergence, 

786 error_tolerance=error_tolerance, ignore_compensation_pulse=ignore_compensation_pulse, 

787 verbose=verbose)) 

788 # get the SRS error at each frequency term by first computing the signal 

789 (this_signal, this_compensation_frequency, this_compensation_amplitude, 

790 this_compensation_decay, this_compensation_delay) = sum_decayed_sines_reconstruction_with_compensation( 

791 sine_frequencies, this_sine_amplitudes, sine_decays, sine_delays, 

792 compensation_frequency, compensation_decay, sample_rate, block_size, ignore_compensation_pulse=ignore_compensation_pulse) 

793 # Then computing the SRS of the signal 

794 this_srs = srs(this_signal, 1/sample_rate, sine_frequencies, srs_damping, 

795 srs_type)[0] 

796 srs_error = (this_srs - required_srs)/required_srs 

797 sine_amplitudes = this_sine_amplitudes 

798 if verbose: 

799 print('Sine Table after Iterations:') 

800 print('{:>8s}, {:>10s}, {:>10s}, {:>10s}, {:>10s}, {:>10s}'.format( 

801 'Sine', 'Frequency', 'Amplitude', 'SRS Val', 'SRS Req', 'Error')) 

802 for i, (freq, amp, srs_val, srs_req, srs_err) in enumerate(zip( 

803 sine_frequencies, sine_amplitudes, this_srs, required_srs, 

804 srs_error)): 

805 print('{:>8s}, {:>10.4f}, {:>10.4f}, {:>10.4f}, {:>10.4f}, {:>10.4f}'.format( 

806 str(i), freq, amp, srs_val, srs_req, srs_err)) 

807 print('Compensating Pulse:') 

808 print('{:>10s}, {:>10s}, {:>10s}, {:>10s}'.format( 

809 'Frequency', 'Amplitude', 'Decay', 'Delay')) 

810 print('{:>10.4f}, {:>10.4f}, {:>10.4f}, {:>10.4f}'.format( 

811 this_compensation_frequency, this_compensation_amplitude, 

812 this_compensation_decay, this_compensation_delay)) 

813 return (this_signal, sine_frequencies, sine_amplitudes, sine_decays, 

814 sine_delays, this_compensation_frequency, this_compensation_amplitude, 

815 this_compensation_decay, this_compensation_delay) 

816 

817 

818def _sum_decayed_sines_single_iteration(sine_frequencies, sine_amplitudes, 

819 sine_decays, sine_delays, 

820 required_srs, compensation_frequency, 

821 compensation_decay, 

822 sample_rate, block_size, 

823 damping_srs, srs_type, 

824 number_of_iterations=10, convergence=0.8, 

825 error_tolerance=0.05, 

826 ignore_compensation_pulse=False, 

827 verbose=False): 

828 """ 

829 Iterates on amplitudes of decayed sine waves to match a prescribed SRS 

830 

831 Parameters 

832 ---------- 

833 sine_frequencies : ndarray 

834 An array of frequencies for each sine tone 

835 sine_amplitudes : ndarray 

836 An array of amplitudes for each sine tone 

837 sine_decays : ndarray 

838 An array of decay values for each sine tone 

839 sine_delays : ndarray 

840 An array of delay values for each sine tone 

841 required_srs : ndarray 

842 An array of SRS values for each frequency in `sine_frequencies` 

843 compensation_frequency : float 

844 The frequency of the compensation pulse 

845 compensation_decay : float 

846 The decay value for the compensation pulse 

847 sample_rate : float 

848 The sample rate of the signal 

849 block_size : int 

850 The number of samples in the signal 

851 damping_srs : float 

852 Fraction of critical damping to use in the SRS calculation (e.g. you 

853 should specify 0.03 to represent 3%, not 3). The default is 0.03. 

854 srs_type : int 

855 The type of spectrum desired: 

856 If `spectrum_type` > 0 (pos) then the SRS will be a base 

857 acceleration-absolute acceleration model 

858 If `spectrum_type` < 0 (neg) then the SRS will be a base acceleration-relative 

859 displacement model (expressed in equivalent static acceleration units). 

860 If abs(`spectrum_type`) is: 

861 1--positive primary, 2--negative primary, 3--absolute maximum primary 

862 4--positive residual, 5--negative residual, 6--absolute maximum residual 

863 7--largest of 1&4, maximum positive, 8--largest of 2&5, maximum negative 

864 9 -- maximax, the largest absolute value of 1-8 

865 10 -- returns a matrix s(9,length(fn)) with all the types 1-9. 

866 number_of_iterations : int 

867 Maximum number of iterations that can be performed at each frequency line. 

868 Default is 10. 

869 convergence : float, optional 

870 The fraction of the error corrected each iteration. The default is 0.8. 

871 error_tolerance : float, optional 

872 Allowable relative error in the SRS. The default is 0.05. 

873 ignore_compensation_pulse : bool, optional 

874 If True, do not use a compensation pulse. Default is false. 

875 verbose : bool, optional 

876 If True, information on the interations will be provided. The default 

877 is False. 

878 

879 Raises 

880 ------ 

881 ValueError 

882 If compensation delay is required to be too long. 

883 

884 Returns 

885 ------- 

886 sine_amplitudes : np.ndarray 

887 An array the same size as the input `sine_amplitudes` array with updated 

888 amplitudes. 

889 compensation_amplitude : float 

890 Amplitude of the compensation pulse 

891 compensation_delay : float 

892 Delay of the compensation pulse. 

893 """ 

894 # Define some helper variables 

895 Amax = np.max(np.abs(sine_amplitudes)) 

896 # Reduce the error tolerance so there's a bit of room for round-off 

897 error_tolerance = error_tolerance*0.9 

898 # Get filter weights for SRS calculations 

899 b, a = sdof_ramp_invariant_filter_weights(sine_frequencies, sample_rate, 

900 damping_srs, srs_type) 

901 # Copy the arrays so we don't overwrite anything 

902 sine_frequencies = np.array(sine_frequencies).copy() 

903 sine_amplitudes = np.array(sine_amplitudes).copy() 

904 sine_decays = np.array(sine_decays).copy() 

905 sine_delays = np.array(sine_delays).copy() 

906 # Iterate one frequency at a time 

907 for i, (frequency, amplitude, decay, delay, desired_srs) in enumerate(zip( 

908 sine_frequencies, sine_amplitudes, sine_decays, sine_delays, 

909 required_srs)): 

910 # if i == 10: 

911 # break 

912 srs_error = float('inf') 

913 iteration_count = 1 

914 increment = 0.1 

915 # Get the pulse without this frequency line 

916 other_sine_frequencies = np.concatenate((sine_frequencies[:i], sine_frequencies[i+1:])) 

917 other_sine_amplitudes = np.concatenate((sine_amplitudes[:i], sine_amplitudes[i+1:])) 

918 other_sine_decays = np.concatenate((sine_decays[:i], sine_decays[i+1:])) 

919 other_sine_delays = np.concatenate((sine_delays[:i], sine_delays[i+1:])) 

920 other_pulse = sum_decayed_sines_reconstruction( 

921 other_sine_frequencies, other_sine_amplitudes, other_sine_decays, 

922 other_sine_delays, sample_rate, block_size) 

923 # Now iterate at the single frequency until convergence 

924 while abs(srs_error) > error_tolerance: 

925 sine_amplitudes[i] = amplitude 

926 # Find the pulse at this frequency line 

927 this_pulse = sum_decayed_sines_reconstruction( 

928 frequency, amplitude, decay, delay, sample_rate, block_size) 

929 # Get the compensating pulse 

930 if not ignore_compensation_pulse: 

931 compensation_amplitude, compensation_delay = sum_decayed_sines_compensating_pulse_parameters( 

932 sine_frequencies, sine_amplitudes, sine_decays, sine_delays, compensation_frequency, compensation_decay) 

933 else: 

934 compensation_amplitude = 0 

935 compensation_delay = 0 

936 # Find the number of samples to shift the waveform 

937 num_shift = -int(np.floor(compensation_delay*sample_rate)) 

938 if abs(num_shift) >= block_size: 

939 raise ValueError('The number of samples in the compensation delay ({:}) is larger than the block_size ({:}).'.format(num_shift, block_size) 

940 + ' The entire pulse will consist of part of the compensation pulse.' 

941 + ' Please increase the block_size or compensation frequency.') 

942 compensation_delay_corrected = compensation_delay + num_shift/sample_rate 

943 # Find compensating time history 

944 compensation_pulse = sum_decayed_sines_reconstruction( 

945 compensation_frequency, compensation_amplitude, compensation_decay, 

946 compensation_delay_corrected, sample_rate, block_size) 

947 # Build the composite waveform, need to shift the signals to align 

948 if num_shift >= 0: 

949 composite_pulse = ( 

950 compensation_pulse 

951 + np.concatenate(( 

952 np.zeros(num_shift), 

953 (other_pulse + this_pulse)[:block_size-num_shift]))) 

954 else: 

955 composite_pulse = ( 

956 other_pulse + this_pulse + 

957 + np.concatenate(( 

958 np.zeros(-num_shift), 

959 (compensation_pulse)[:block_size+num_shift]))) 

960 # Find the SRS at the current frequency line 

961 srs_prediction = srs(composite_pulse, 1/sample_rate, frequency, damping_srs, 

962 srs_type, b[i], a[i])[0][0] # only get the SRS and there should only be one value due to one frequency line 

963 srs_error = (srs_prediction - desired_srs)/desired_srs 

964 if verbose: 

965 print('Iteration at frequency {:}, {:0.4f}\n Iteration Count: {:}, SRS Error: {:0.4f}'.format(i, frequency, iteration_count, srs_error)) 

966 # Now we're going to compute the same thing again with a perturbed 

967 # amplitude, this will allow us to compute the slope change at the 

968 # current amplitude 

969 amplitude_change = np.sign(srs_error)*increment*np.sign(amplitude)*Amax 

970 # Now check and see if we need to modify the amplitude 

971 if abs(srs_error) > error_tolerance: 

972 if amplitude_change == 0: # perturb it a bit 

973 amplitude_change = np.sign(srs_error)*np.sign(sine_amplitudes[i])*Amax*increment/10 

974 new_amplitude = amplitude + amplitude_change 

975 # Can't allow the sign of the amplitude to change 

976 if np.sign(amplitude) != np.sign(new_amplitude): 

977 new_amplitude *= -1 

978 if amplitude == new_amplitude: 

979 new_amplitude = amplitude * 1.01 

980 sine_amplitudes[i] = new_amplitude 

981 # Find new component at this frequency 

982 this_pulse = sum_decayed_sines_reconstruction( 

983 frequency, new_amplitude, decay, delay, sample_rate, block_size) 

984 # Get the compensating pulse 

985 if not ignore_compensation_pulse: 

986 compensation_amplitude, compensation_delay = sum_decayed_sines_compensating_pulse_parameters( 

987 sine_frequencies, sine_amplitudes, sine_decays, sine_delays, compensation_frequency, compensation_decay) 

988 else: 

989 compensation_amplitude = 0 

990 compensation_delay = 0 

991 # Find the number of samples to shift the waveform 

992 num_shift = -int(np.floor(compensation_delay*sample_rate)) 

993 if abs(num_shift) >= block_size: 

994 raise ValueError('The number of samples in the compensation delay ({:}) is larger than the block_size ({:}).'.format(num_shift, block_size) 

995 + ' The entire pulse will consist of part of the compensation pulse. ' 

996 + 'Please increase the block_size or compensation frequency.') 

997 compensation_delay_corrected = compensation_delay + num_shift/sample_rate 

998 # Find compensating time history 

999 compensation_pulse = sum_decayed_sines_reconstruction( 

1000 compensation_frequency, compensation_amplitude, compensation_decay, 

1001 compensation_delay_corrected, sample_rate, block_size) 

1002 # Build the composite waveform, need to shift the signals to align 

1003 if num_shift >= 0: 

1004 composite_pulse = ( 

1005 compensation_pulse 

1006 + np.concatenate(( 

1007 np.zeros(num_shift), 

1008 (other_pulse + this_pulse)[:block_size-num_shift]))) 

1009 else: 

1010 composite_pulse = ( 

1011 other_pulse + this_pulse + 

1012 + np.concatenate(( 

1013 np.zeros(-num_shift), 

1014 (compensation_pulse)[:block_size+num_shift]))) 

1015 # Find the SRS at the current frequency line 

1016 srs_perturbed = srs(composite_pulse, 1/sample_rate, frequency, damping_srs, 

1017 srs_type, b[i], a[i])[0][0] # only get the SRS and there should only be one value due to one frequency line 

1018 # Get slope of correction 

1019 correction_slope = (abs(amplitude)-abs(new_amplitude))/(srs_prediction-srs_perturbed) 

1020 if correction_slope > 0: 

1021 # this is what we want, it means we are changing in the right 

1022 # direction 

1023 new_amplitude = convergence*correction_slope*(desired_srs-srs_prediction) + abs(amplitude) 

1024 # Never let the change be too large 

1025 amplitude_check = max([abs(amplitude), Amax/10]) 

1026 if new_amplitude > 2*amplitude_check: 

1027 new_amplitude = 2*amplitude_check 

1028 # Never let it be less than zero 

1029 if new_amplitude < 0: 

1030 new_amplitude = np.abs(amplitude)/10 

1031 # Move the previous amplitude into a different variable 

1032 old_amplitude = amplitude 

1033 # Never allow new amplitude to be zero 

1034 if new_amplitude == 0: 

1035 amplitude = 2*np.finfo(float).eps*np.sign(amplitude) 

1036 else: 

1037 amplitude = new_amplitude*np.sign(amplitude) 

1038 # Store it for the next iteration 

1039 sine_amplitudes[i] = amplitude 

1040 # Find new component at this frequency 

1041 this_pulse = sum_decayed_sines_reconstruction( 

1042 frequency, amplitude, decay, delay, sample_rate, block_size) 

1043 # Get the compensating pulse 

1044 if not ignore_compensation_pulse: 

1045 compensation_amplitude, compensation_delay = sum_decayed_sines_compensating_pulse_parameters( 

1046 sine_frequencies, sine_amplitudes, sine_decays, sine_delays, compensation_frequency, compensation_decay) 

1047 else: 

1048 compensation_amplitude = 0 

1049 compensation_delay = 0 

1050 # Find the number of samples to shift the waveform 

1051 num_shift = -int(np.floor(compensation_delay*sample_rate)) 

1052 if abs(num_shift) >= block_size: 

1053 raise ValueError('The number of samples in the compensation delay' 

1054 + ' ({:}) is larger than the block_size ({:}).'.format(num_shift, block_size) 

1055 + ' The entire pulse will consist of part of the compensation pulse. ' 

1056 + 'Please increase the block_size or compensation frequency.') 

1057 compensation_delay_corrected = compensation_delay + num_shift/sample_rate 

1058 # Find compensating time history 

1059 compensation_pulse = sum_decayed_sines_reconstruction( 

1060 compensation_frequency, compensation_amplitude, compensation_decay, 

1061 compensation_delay_corrected, sample_rate, block_size) 

1062 # Build the composite waveform, need to shift the signals to align 

1063 if num_shift >= 0: 

1064 composite_pulse = ( 

1065 compensation_pulse 

1066 + np.concatenate(( 

1067 np.zeros(num_shift), 

1068 (other_pulse + this_pulse)[:block_size-num_shift]))) 

1069 else: 

1070 composite_pulse = ( 

1071 other_pulse + this_pulse + 

1072 + np.concatenate(( 

1073 np.zeros(-num_shift), 

1074 (compensation_pulse)[:block_size+num_shift]))) 

1075 # Find the SRS at the current frequency line 

1076 srs_corrected = srs(composite_pulse, 1/sample_rate, frequency, damping_srs, 

1077 srs_type, b[i], a[i])[0][0] # only get the SRS and there should only be one value due to one frequency line 

1078 srs_error = (srs_corrected - desired_srs)/desired_srs 

1079 # If the SRS error is positive and the amplitude has already been reduced to zero, stop trying 

1080 if srs_error > 0 and amplitude == 0: 

1081 iteration_count = number_of_iterations + 1 

1082 if verbose: 

1083 print(' Old Amplitude: {:0.4f}, New Amplitude: {:0.4f}, Error: {:0.4f}'.format(old_amplitude, amplitude, srs_error)) 

1084 # Don't allow amplitude to change more than a factor of 10 in an iteration 

1085 if np.abs(10*old_amplitude) < np.abs(amplitude) or np.abs(0.1*old_amplitude) > np.abs(amplitude): 

1086 iteration_count = number_of_iterations + 1 

1087 else: 

1088 # The slope is negative, so we try a bigger increment 

1089 increment *= 2 

1090 amplitude_change = np.sign(srs_error)*increment*np.sign(amplitude)*Amax 

1091 if verbose: 

1092 print('Slope of correction was negative, trying a bigger increment') 

1093 iteration_count += 1 

1094 if iteration_count > number_of_iterations: 

1095 print(' Warning: SRS did not converge for frequency {:}: {:0.4f}'.format(i, frequency)) 

1096 break 

1097 if not ignore_compensation_pulse: 

1098 compensation_amplitude, compensation_delay = sum_decayed_sines_compensating_pulse_parameters( 

1099 sine_frequencies, sine_amplitudes, sine_decays, sine_delays, compensation_frequency, compensation_decay) 

1100 else: 

1101 compensation_amplitude = 0 

1102 compensation_delay = 0 

1103 return sine_amplitudes, compensation_amplitude, compensation_delay 

1104 

1105 

1106def sum_decayed_sines_compensating_pulse_parameters(sine_frequencies, sine_amplitudes, sine_decays, sine_delays, 

1107 compensation_frequency, compensation_decay): 

1108 omegas = sine_frequencies*2*np.pi 

1109 omega_comp = compensation_frequency*2*np.pi 

1110 var = -np.sum(sine_amplitudes/(sine_frequencies*(sine_decays**2+1))) 

1111 compensation_amplitude = compensation_frequency*(compensation_decay**2+1)*var 

1112 var0 = (np.sum(sine_amplitudes*sine_delays/(omegas*(sine_decays**2+1))))/compensation_amplitude 

1113 # TODO Verify if this is a bug or not, I think this should be matlab code but graflab code had it as a matrix division. 

1114 # var1 = (np.sum(2*sine_decays*sine_amplitudes/(omegas**2*(sine_decays**2 + 1)**2)))/compensation_amplitude 

1115 var1 = (np.sum((2*sine_decays*sine_amplitudes)[np.newaxis, :]@np.linalg.pinv((omegas**2*(sine_decays**2 + 1)**2)[np.newaxis, :])))/compensation_amplitude 

1116 var2 = 2*compensation_decay/(omega_comp*omega_comp*(compensation_decay**2+1)**2) 

1117 var3 = omega_comp*(compensation_decay**2+1) 

1118 compensation_delay = -var3*(var2 + var1 + var0) 

1119 return compensation_amplitude, compensation_delay 

1120 

1121 

1122def sum_decayed_sines_reconstruction(sine_frequencies, sine_amplitudes, 

1123 sine_decays, sine_delays, sample_rate, 

1124 block_size): 

1125 """ 

1126 Computes a sum of decayed sines signal from a set of frequencies, amplitudes, 

1127 decays, and delays. 

1128 

1129 Parameters 

1130 ---------- 

1131 sine_frequencies : ndarray 

1132 An array of frequencies for each sine tone 

1133 sine_amplitudes : ndarray 

1134 An array of amplitudes for each sine tone 

1135 sine_decays : ndarray 

1136 An array of decay values for each sine tone 

1137 sine_delays : ndarray 

1138 An array of delay values for each sine tone 

1139 sample_rate : float 

1140 The sample rate of the signal 

1141 block_size : int 

1142 The number of samples in the signal 

1143 

1144 Returns 

1145 ------- 

1146 ndarray 

1147 A signal containing the sum of decayed sinusoids. 

1148 

1149 """ 

1150 times = np.arange(block_size)/sample_rate 

1151 # See if any delays are below zero and if so then shift all delays forward 

1152 if np.any(sine_delays < 0): 

1153 sine_delays = sine_delays - np.min(sine_delays) 

1154 # Now go through and compute the sine tones 

1155 omegas = 2*np.pi*sine_frequencies 

1156 this_times = times[:, np.newaxis] - sine_delays 

1157 response = sine_amplitudes*np.exp(-sine_decays*omegas*this_times)*np.sin(omegas*this_times) 

1158 response[this_times < 0] = 0 

1159 return np.sum(response, axis=-1) 

1160 

1161 

1162def sum_decayed_sines_reconstruction_with_compensation( 

1163 sine_frequencies, sine_amplitudes, 

1164 sine_decays, sine_delays, compensation_frequency, compensation_decay, 

1165 sample_rate, block_size, ignore_compensation_pulse = False): 

1166 """ 

1167 

1168 

1169 Parameters 

1170 ---------- 

1171 sine_frequencies : ndarray 

1172 An array of frequencies for each sine tone 

1173 sine_amplitudes : ndarray 

1174 An array of amplitudes for each sine tone 

1175 sine_decays : ndarray 

1176 An array of decay values for each sine tone 

1177 sine_delays : ndarray 

1178 An array of delay values for each sine tone 

1179 compensation_frequency : float 

1180 The frequency of the compensation pulse 

1181 compensation_decay : float 

1182 The decay value for the compensation pulse 

1183 sample_rate : float 

1184 The sample rate of the signal 

1185 block_size : int 

1186 The number of samples in the signal 

1187 ignore_compensation_pulse : bool, optional 

1188 If True, ignores the compensation pulse 

1189 

1190 Returns 

1191 ------- 

1192 signal : ndarray 

1193 A signal containing the sum of decayed sinusoids. 

1194 compensation_frequency : float 

1195 The frequency of the compensation pulse 

1196 compensation_amplitude : float 

1197 The amplitude value for the compensation pulse 

1198 compensation_decay : float 

1199 The decay value for the compensation pulse 

1200 compensation_delay : float 

1201 The delay value for the compensation pulse 

1202 """ 

1203 sine_frequencies = np.array(sine_frequencies).flatten() 

1204 sine_amplitudes = np.array(sine_amplitudes).flatten() 

1205 sine_delays = np.array(sine_delays).flatten() 

1206 sine_decays = np.array(sine_decays).flatten() 

1207 if not ignore_compensation_pulse: 

1208 compensation_amplitude, compensation_delay = sum_decayed_sines_compensating_pulse_parameters( 

1209 sine_frequencies, sine_amplitudes, sine_decays, sine_delays, 

1210 compensation_frequency, compensation_decay) 

1211 else: 

1212 compensation_amplitude = 0 

1213 compensation_delay = 0 

1214 sine_frequencies = np.concatenate((sine_frequencies, [compensation_frequency])) 

1215 sine_amplitudes = np.concatenate((sine_amplitudes, [compensation_amplitude])) 

1216 sine_delays = np.concatenate((sine_delays, [compensation_delay])) 

1217 sine_decays = np.concatenate((sine_decays, [compensation_decay])) 

1218 signal = sum_decayed_sines_reconstruction( 

1219 sine_frequencies, sine_amplitudes, sine_decays, sine_delays, 

1220 sample_rate, block_size) 

1221 return (signal, compensation_frequency, compensation_amplitude, 

1222 compensation_decay, compensation_delay) 

1223 

1224 

1225def sum_decayed_sines_displacement_velocity( 

1226 sine_frequencies, sine_amplitudes, 

1227 sine_decays, sine_delays, sample_rate, block_size, 

1228 acceleration_factor=1): 

1229 """ 

1230 Creates velocity and displacement signals from acceleration sinusoids. 

1231 

1232 Parameters 

1233 ---------- 

1234 sine_frequencies : ndarray 

1235 An array of frequencies for each sine tone 

1236 sine_amplitudes : ndarray 

1237 An array of amplitudes for each sine tone 

1238 sine_decays : ndarray 

1239 An array of decay values for each sine tone 

1240 sine_delays : ndarray 

1241 An array of delay values for each sine tone 

1242 sample_rate : float 

1243 The sample rate of the signal 

1244 block_size : int 

1245 The number of samples in the signal 

1246 acceleration_factor : float, optional 

1247 Optional scale factor to convert acceleration into velocity and 

1248 displacement. For example, if sine amplitudes are in G and displacement 

1249 is desired in inches, the acceleration factor should be set to 386.089. 

1250 If sine amplitudes are in G and displacement is desired in meters, the 

1251 acceleration factor should be set to 9.80665. The default is 1, which 

1252 assumes consistent units (e.g. acceleration in m/s^2, velocity in m/s, 

1253 displacement in m). 

1254 

1255 Returns 

1256 ------- 

1257 v : ndarray 

1258 The velocity of the signal. 

1259 d : ndarray 

1260 The displacement of the signal. 

1261 

1262 """ 

1263 # Make sure everything is a numpy array 

1264 sine_frequencies = np.array(sine_frequencies).flatten() 

1265 sine_amplitudes = np.array(sine_amplitudes).flatten() 

1266 sine_delays = np.array(sine_delays).flatten() 

1267 sine_decays = np.array(sine_decays).flatten() 

1268 # Transform units 

1269 sine_amplitudes = sine_amplitudes * acceleration_factor 

1270 # See if any delays are below zero and if so then shift all delays forward 

1271 if np.any(sine_delays < 0): 

1272 sine_delays = sine_delays - np.min(sine_delays) 

1273 f = sine_frequencies 

1274 A = sine_amplitudes 

1275 z = sine_decays 

1276 tau = sine_delays 

1277 x = np.zeros(block_size) 

1278 tmp1 = x.copy() 

1279 tmp2 = x.copy() 

1280 tmp3 = x.copy() 

1281 x1 = np.ones(x.shape) 

1282 v = x.copy() 

1283 d = x.copy() 

1284 w = 2*np.pi*f 

1285 w2 = w*w 

1286 zw = z*w 

1287 zp1 = z*z + 1 

1288 zm1 = z*z - 1 

1289 t = np.arange(block_size)/sample_rate 

1290 for k in range(len(sine_frequencies)): 

1291 indices = t-tau[k] > 0 # index's where vel and disp are evaluated 

1292 Awz1 = A[k]/(w[k]*zp1[k]) 

1293 tmp1[indices] = Awz1*np.exp(-zw[k]*(t[indices]-tau[k])) 

1294 tmp2[indices] = z[k]*np.sin(w[k]*(t[indices]-tau[k])) + np.cos(w[k]*(t[indices]-tau[k])) 

1295 v[indices] = v[indices] - tmp1[indices]*tmp2[indices] + Awz1*x1[indices] 

1296 Awz2 = A[k]/(w2[k]*zp1[k]*zp1[k]) 

1297 tmp1[indices] = Awz2*np.exp(-zw[k]*(t[indices]-tau[k])) 

1298 tmp2[indices] = zm1[k]*np.sin(w[k]*(t[indices]-tau[k])) + 2*z[k]*np.cos(w[k]*(t[indices]-tau[k])) 

1299 tmp3[indices] = Awz1*(t[indices]-tau[k]) 

1300 tmp4 = 2*z[k]*Awz2 

1301 d[indices] = d[indices] + tmp1[indices]*tmp2[indices] + tmp3[indices] - tmp4 

1302 return v, d 

1303 

1304 

1305def loginterp(x, xp, fp): 

1306 return 10**np.interp(np.log10(x), np.log10(xp), np.log10(fp)) 

1307 

1308 

1309def optimization_error_function( 

1310 amplitude_lin_scales, sample_rate, block_size, 

1311 sine_frequencies, sine_amplitudes, sine_decays, sine_delays, 

1312 compensation_frequency, compensation_decay, control_irfs, limit_irfs, 

1313 srs_damping, srs_type, control_srs, control_weights, limit_srs, 

1314 b=None, a=None, frequency_index=None): 

1315 if frequency_index is None: 

1316 frequency_index = slice(None) 

1317 # Apply the scale factors 

1318 new_sine_amplitudes = sine_amplitudes.copy() 

1319 new_sine_amplitudes[frequency_index] *= amplitude_lin_scales 

1320 # Compute a new time history signal 

1321 (input_signal, new_compensation_frequency, new_compensation_amplitude, 

1322 new_compensation_decay, new_compensation_delay) = sum_decayed_sines_reconstruction_with_compensation( 

1323 sine_frequencies, new_sine_amplitudes, sine_decays, sine_delays, 

1324 compensation_frequency, compensation_decay, sample_rate, block_size) 

1325 # Transform the signal to the control degrees of freedom 

1326 control_responses = np.array([oaconvolve(control_irf, input_signal) for control_irf in control_irfs])[..., :block_size] 

1327 # Compute SRSs at all frequencies 

1328 predicted_control_srs, frequencies = srs(control_responses, 1/sample_rate, sine_frequencies[frequency_index], 

1329 srs_damping, srs_type, 

1330 None if b is None else b[frequency_index], 

1331 None if a is None else a[frequency_index]) 

1332 # Compute error 

1333 mean_control_error = np.mean( 

1334 (control_weights[:, np.newaxis] 

1335 * ((predicted_control_srs - control_srs[..., frequency_index]) 

1336 / control_srs[..., frequency_index]))) 

1337 if limit_srs is not None: 

1338 limit_responses = np.array([oaconvolve(limit_irf, input_signal) for limit_irf in limit_irfs])[..., :block_size] 

1339 predicted_limit_srs, frequencies = srs(limit_responses, 1/sample_rate, sine_frequencies[frequency_index], 

1340 srs_damping, srs_type, 

1341 None if b is None else b[frequency_index], 

1342 None if a is None else a[frequency_index]) 

1343 max_limit_error = np.max( 

1344 ((predicted_limit_srs - limit_srs[..., frequency_index]) 

1345 / limit_srs[..., frequency_index])) 

1346 if max_limit_error >= 0 and mean_control_error >= 0: 

1347 srs_error = np.max((mean_control_error, max_limit_error)) 

1348 elif max_limit_error <= 0 and mean_control_error <= 0: 

1349 srs_error = np.max((mean_control_error, max_limit_error)) 

1350 elif max_limit_error >= 0 and mean_control_error <= 0: 

1351 srs_error = max_limit_error 

1352 elif max_limit_error <= 0 and mean_control_error >= 0: 

1353 srs_error = mean_control_error 

1354 else: 

1355 limit_responses = None 

1356 predicted_limit_srs = None 

1357 srs_error = mean_control_error 

1358 

1359 return (np.abs(srs_error), input_signal, control_responses, 

1360 predicted_control_srs, limit_responses, predicted_limit_srs, 

1361 new_sine_amplitudes, new_compensation_frequency, 

1362 new_compensation_amplitude, new_compensation_decay, 

1363 new_compensation_delay) 

1364 

1365 

1366def optimization_callback(intermediate_result, rms_error_threshold=0.02, 

1367 verbose=True): 

1368 if verbose: 

1369 print('Amplitude Scale: {:}, error is {:}'.format(intermediate_result.x.squeeze(), 

1370 intermediate_result.fun)) 

1371 if rms_error_threshold is not None: 

1372 if intermediate_result.fun < rms_error_threshold: 

1373 raise StopIteration 

1374 

1375 

1376def sum_decayed_sines_minimize(sample_rate, block_size, 

1377 sine_frequencies=None, sine_tone_range=None, sine_tone_per_octave=None, 

1378 sine_amplitudes=None, sine_decays=None, sine_delays=None, 

1379 control_srs=None, control_breakpoints=None, 

1380 srs_damping=0.05, srs_type=9, 

1381 compensation_frequency=None, compensation_decay=0.95, 

1382 # Parameters for defining decays 

1383 tau=None, num_time_constants=None, decay_resolution=None, 

1384 scale_factor=1.02, 

1385 acceleration_factor=1.0, 

1386 # Parameters for imposing limits 

1387 limit_breakpoints=None, limit_transfer_functions=None, 

1388 control_transfer_functions=None, control_weights=None, 

1389 # Parameters for the optimizer 

1390 minimize_iterations=1, rms_error_threshold=None, 

1391 optimization_passes=3, 

1392 plot_results=False, verbose=False 

1393 ): 

1394 # Handle the sine tone frequencies 

1395 if sine_frequencies is None and sine_tone_range is None: 

1396 raise ValueError('Either `sine_frequencies` or `sine_tone_range` must be specified') 

1397 if sine_frequencies is not None and sine_tone_range is not None: 

1398 raise ValueError('`sine_frequencies` can not be specified simultaneously with `sine_tone_range`') 

1399 if sine_frequencies is None: 

1400 # Create sine tones 

1401 if sine_tone_per_octave is None: 

1402 sine_tone_per_octave = int(np.floor(9-srs_damping*100)) 

1403 sine_frequencies = octspace(sine_tone_range[0], sine_tone_range[1], 

1404 sine_tone_per_octave) 

1405 # Now set up the SRS 

1406 if control_srs is None and control_breakpoints is None: 

1407 raise ValueError('Either `control_srs` or `control_breakpoints` must be specified') 

1408 if control_srs is not None and control_breakpoints is not None: 

1409 raise ValueError('`control_srs` can not be specified simultaneously with `control_breakpoints`') 

1410 if control_srs is None: 

1411 frequencies = control_breakpoints[:, 0] 

1412 breakpoint_curves = control_breakpoints[:, 1:].T 

1413 control_srs = np.array([loginterp(sine_frequencies, 

1414 frequencies, 

1415 breakpoint_curve) for breakpoint_curve in breakpoint_curves]) 

1416 else: 

1417 control_srs = np.atleast_2d(control_srs) 

1418 if control_transfer_functions is None: 

1419 control_transfer_functions = np.ones(((control_srs.shape[0]-1)*2, block_size//2+1)) 

1420 tf_frequencies = np.fft.rfftfreq(control_transfer_functions.shape[-1]*2-1, 1/sample_rate) 

1421 if sine_amplitudes is None: 

1422 tf_at_frequencies = np.array([ 

1423 np.interp(sine_frequencies, tf_frequencies, np.abs(control_transfer_function)) 

1424 for control_transfer_function in control_transfer_functions]) 

1425 srs_amplitudes = np.array([nnls(ai[:, np.newaxis], bi)[0][0] for ai, bi in zip(tf_at_frequencies.T, control_srs.T)]) 

1426 srs_amplitudes[np.arange(srs_amplitudes.size) % 2 == 0] *= -1 

1427 quality = 1/(2*srs_damping) 

1428 srs_amplitudes /= quality 

1429 sine_amplitudes = srs_amplitudes 

1430 

1431 if sine_delays is None: 

1432 sine_delays = np.zeros(sine_frequencies.size) 

1433 

1434 if compensation_frequency is None: 

1435 compensation_frequency = np.min(sine_frequencies)/3 

1436 

1437 # Set up decay terms 

1438 decay_terms_specified = 0 

1439 if sine_decays is not None: 

1440 decay_terms_specified += 1 

1441 if tau is not None: 

1442 decay_terms_specified += 1 

1443 if num_time_constants is not None: 

1444 decay_terms_specified += 1 

1445 if decay_terms_specified == 0: 

1446 raise ValueError('One of `sine_decays`, `tau`, or `num_time_constants` must be specified') 

1447 if decay_terms_specified > 1: 

1448 raise ValueError('Only one of `sine_decays`, `tau`, or `num_time_constants` can be specified') 

1449 

1450 # Now check and see which is defined 

1451 if num_time_constants is not None: 

1452 period = block_size / sample_rate 

1453 if isinstance(num_time_constants, dict): 

1454 tau = {} 

1455 for freq_range, num_time_constant in num_time_constants.items(): 

1456 tau[freq_range] = period / num_time_constant 

1457 else: 

1458 tau = period/num_time_constants 

1459 if tau is not None: 

1460 sine_decays = [] 

1461 for freq in sine_frequencies: 

1462 if isinstance(tau, dict): 

1463 this_decay = None 

1464 for freq_range, this_tau in tau.items(): 

1465 if freq_range[0] <= freq <= freq_range[1]: 

1466 this_decay = 1/(2*np.pi*freq*this_tau) 

1467 break 

1468 if this_decay is None: 

1469 raise ValueError('No frequency range matching frequency {:} was found in the specified decay parameters.'.format(freq)) 

1470 sine_decays.append(this_decay) 

1471 else: 

1472 sine_decays.append(1/(2*np.pi*freq*tau)) 

1473 sine_decays = np.array(sine_decays) 

1474 # Otherwise we just keep the specified sine_decays 

1475 

1476 # Now handle the minimum resolution on the decay 

1477 if decay_resolution is not None: 

1478 sine_decays = decay_resolution*np.round(sine_decays/decay_resolution) 

1479 

1480 if compensation_frequency is None: 

1481 compensation_frequency = np.min(sine_frequencies)/3 

1482 

1483 # Now set up limits and transfer functions 

1484 if control_weights is None: 

1485 control_weights = np.ones((control_srs.shape[0])) 

1486 if limit_breakpoints is not None: 

1487 frequencies = limit_breakpoints[:, 0] 

1488 breakpoint_curves = limit_breakpoints[:, 1:].T 

1489 limit_srs = np.array([loginterp(sine_frequencies, 

1490 frequencies, 

1491 breakpoint_curve) for breakpoint_curve in breakpoint_curves]) 

1492 else: 

1493 limit_srs = None 

1494 

1495 # Compute impulse responses 

1496 control_irfs = np.fft.irfft(control_transfer_functions, axis=-1) 

1497 if limit_transfer_functions is not None: 

1498 limit_irfs = np.fft.irfft(limit_transfer_functions, axis=-1) 

1499 else: 

1500 limit_irfs = None 

1501 b, a = sdof_ramp_invariant_filter_weights(sine_frequencies, sample_rate, srs_damping, srs_type) 

1502 

1503 # Normalize control weights 

1504 control_weights = control_weights/np.linalg.norm(control_weights) 

1505 

1506 # Copy things so we don't overwrite 

1507 sine_amplitudes = sine_amplitudes.copy() 

1508 

1509 # Now we will iterate over all of the frequencies and compute the new 

1510 # amplitudes 

1511 for j in range(optimization_passes): 

1512 for i in range(len(sine_frequencies)): 

1513 if verbose: 

1514 print('Pass {:}, Analyzing Frequency {:}: {:0.2f}'.format(j+1, i, sine_frequencies[i])) 

1515 

1516 # We will now go through and optimize the frequency line 

1517 def error_function(x): 

1518 return optimization_error_function( 

1519 x, sample_rate, block_size, 

1520 sine_frequencies, sine_amplitudes, sine_decays, sine_delays, 

1521 compensation_frequency, compensation_decay, control_irfs, limit_irfs, 

1522 srs_damping, srs_type, control_srs, control_weights, limit_srs, 

1523 b, a, frequency_index=[i])[0] 

1524 

1525 def callback(intermediate_result): 

1526 return optimization_callback( 

1527 intermediate_result, rms_error_threshold, verbose) 

1528 

1529 optimization_result = minimize(error_function, 

1530 np.ones(1), 

1531 method='Powell', 

1532 bounds=[(0, np.inf)], 

1533 callback=callback, 

1534 options={'maxiter': minimize_iterations}) 

1535 

1536 # Populate the amplitudes with the updated result 

1537 if verbose: 

1538 print('Initial Amplitude: {:}, Updated Amplitude: {:}\n'.format(sine_amplitudes[i], sine_amplitudes[i]*optimization_result.x.squeeze())) 

1539 sine_amplitudes[i] *= optimization_result.x.squeeze() 

1540 

1541 (error, input_signal, control_responses, predicted_control_srs, 

1542 limit_responses, predicted_limit_srs, 

1543 sine_amplitudes, compensation_frequency, 

1544 compensation_amplitude, compensation_decay, 

1545 compensation_delay) = optimization_error_function( 

1546 np.ones(sine_frequencies.size), 

1547 sample_rate, block_size, 

1548 sine_frequencies, sine_amplitudes, sine_decays, sine_delays, 

1549 compensation_frequency, compensation_decay, control_irfs, limit_irfs, 

1550 srs_damping, srs_type, control_srs, control_weights, limit_srs, 

1551 b, a) 

1552 

1553 all_frequencies = np.concatenate((sine_frequencies, 

1554 [compensation_frequency])) 

1555 all_amplitudes = np.concatenate((sine_amplitudes, 

1556 [compensation_amplitude])) 

1557 all_decays = np.concatenate((sine_decays, 

1558 [compensation_decay])) 

1559 all_delays = np.concatenate((sine_delays, 

1560 [compensation_delay])) 

1561 

1562 # Now compute displacement and velocity 

1563 velocity_signal, displacement_signal = sum_decayed_sines_displacement_velocity( 

1564 all_frequencies, all_amplitudes, all_decays, all_delays, sample_rate, 

1565 block_size, acceleration_factor) 

1566 

1567 return_vals = (input_signal, velocity_signal, displacement_signal, 

1568 control_responses, predicted_control_srs, 

1569 limit_responses, predicted_limit_srs, 

1570 all_frequencies, all_amplitudes, all_decays, all_delays, 

1571 ) 

1572 

1573 if plot_results: 

1574 fig, ax = plt.subplots(2, 2, figsize=(8, 6)) 

1575 times = np.arange(block_size)/sample_rate 

1576 ax[0, 0].plot(times, input_signal) 

1577 ax[0, 0].set_ylabel('Acceleration') 

1578 ax[0, 0].set_xlabel('Time (s)') 

1579 ax[0, 1].plot(times, velocity_signal) 

1580 ax[0, 1].set_ylabel('Velocity') 

1581 ax[0, 1].set_xlabel('Time (s)') 

1582 ax[1, 0].plot(times, displacement_signal) 

1583 ax[1, 0].set_ylabel('Displacement') 

1584 ax[1, 0].set_xlabel('Time (s)') 

1585 # Compute SRS 

1586 this_srs, this_frequencies = srs( 

1587 input_signal, 1/sample_rate, sine_frequencies, 

1588 srs_damping, srs_type) 

1589 if control_breakpoints is None: 

1590 srs_abscissa = sine_frequencies 

1591 srs_ordinate = control_srs.T 

1592 else: 

1593 srs_abscissa = control_breakpoints[:, 0] 

1594 srs_ordinate = control_breakpoints[:, 1:] 

1595 ax[1, 1].plot(srs_abscissa, srs_ordinate, 'k--') 

1596 ax[1, 1].plot(this_frequencies, this_srs) 

1597 ax[1, 1].set_ylabel('SRS ({:0.2f}% damping)'.format(srs_damping*100)) 

1598 ax[1, 1].set_xlabel('Frequency (Hz)') 

1599 ax[1, 1].set_yscale('log') 

1600 ax[1, 1].set_xscale('log') 

1601 ax[1, 1].legend(('Reference', 'Decayed Sine')) 

1602 fig.tight_layout() 

1603 return_vals += (fig, ax) 

1604 # Now plot all control channels 

1605 for i, (predicted, control, response) in enumerate(zip(predicted_control_srs, control_srs, control_responses)): 

1606 fig, ax = plt.subplots(1, 2, figsize=(8, 3)) 

1607 ax[0].set_title('Control Signal {:}'.format(i)) 

1608 ax[0].plot(times, response) 

1609 ax[0].set_label('Acceleration') 

1610 ax[0].set_xlabel('Time (s)') 

1611 ax[1].set_title('Control SRS {:}'.format(i)) 

1612 ax[1].plot(sine_frequencies, control, 'k--') 

1613 ax[1].plot(sine_frequencies, predicted) 

1614 ax[1].set_ylabel('SRS ({:0.2f}% damping)'.format(srs_damping*100)) 

1615 ax[1].set_xlabel('Frequency (Hz)') 

1616 ax[1].set_yscale('log') 

1617 ax[1].set_xscale('log') 

1618 ax[1].legend(('Desired', 'Achieved')) 

1619 fig.tight_layout() 

1620 return_vals += (fig, ax) 

1621 # Now plot all limit channels 

1622 if limit_srs is not None: 

1623 for i, (predicted, limit, response) in enumerate(zip(predicted_limit_srs, limit_srs, limit_responses)): 

1624 fig, ax = plt.subplots(1, 2, figsize=(8, 3)) 

1625 ax[0].set_title('Limit Signal {:}'.format(i)) 

1626 ax[0].plot(times, response) 

1627 ax[0].set_label('Acceleration') 

1628 ax[0].set_xlabel('Time (s)') 

1629 ax[1].set_title('Limit SRS {:}'.format(i)) 

1630 ax[1].plot(sine_frequencies, limit, 'k--') 

1631 ax[1].plot(sine_frequencies, predicted) 

1632 ax[1].set_ylabel('SRS ({:0.2f}% damping)'.format(srs_damping*100)) 

1633 ax[1].set_xlabel('Frequency (Hz)') 

1634 ax[1].set_yscale('log') 

1635 ax[1].set_xscale('log') 

1636 ax[1].legend(('Desired', 'Achieved')) 

1637 fig.tight_layout() 

1638 return_vals += (fig, ax) 

1639 

1640 return return_vals