Coverage for src / sdynpy / signal_processing / sdynpy_frf_inverse.py: 9%

89 statements  

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

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

2""" 

3Tools for computing inverses of frequency response functions 

4""" 

5""" 

6Copyright 2022 National Technology & Engineering Solutions of Sandia, 

7LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. 

8Government retains certain rights in this software. 

9 

10This program is free software: you can redistribute it and/or modify 

11it under the terms of the GNU General Public License as published by 

12the Free Software Foundation, either version 3 of the License, or 

13(at your option) any later version. 

14 

15This program is distributed in the hope that it will be useful, 

16but WITHOUT ANY WARRANTY; without even the implied warranty of 

17MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

18GNU General Public License for more details. 

19 

20You should have received a copy of the GNU General Public License 

21along with this program. If not, see <https://www.gnu.org/licenses/>. 

22""" 

23 

24import numpy as np 

25from scipy.interpolate import interp1d 

26import warnings 

27 

28 

29def frf_inverse(frf_matrix, 

30 method='standard', 

31 response_weighting_matrix=None, 

32 reference_weighting_matrix=None, 

33 regularization_weighting_matrix=None, 

34 regularization_parameter=None, 

35 cond_num_threshold=None, 

36 num_retained_values=None): 

37 """ 

38 Computes the inverse of an FRF matrix for source estimation problems. 

39 

40 Parameters 

41 ---------- 

42 frf_matrix : NDArray 

43 Transfer function as an np.ndarray, should be organized such that the 

44 size is [number of lines, number of responses, number of references] 

45 method : str, optional 

46 The method to be used for the FRF matrix inversions. The available 

47 methods are: 

48 - standard - basic pseudo-inverse via numpy.linalg.pinv with the 

49 default rcond parameter, this is the default method 

50 - threshold - pseudo-inverse via numpy.linalg.pinv with a specified 

51 condition number threshold 

52 - tikhonov - pseudo-inverse using the Tikhonov regularization method 

53 - truncation - pseudo-inverse where a fixed number of singular values 

54 are retained for the inverse 

55 response_weighting_matrix : sdpy.Matrix or np.ndarray, optional 

56 Diagonal matrix used to weight response degrees of freedom (to solve the 

57 problem as a weight least squares) by multiplying the rows of the FRF 

58 matrix by a scalar weights. This matrix can also be a 3D matrix such that 

59 the the weights are different for each frequency line. The matrix should 

60 be sized [number of lines, number of references, number of references], 

61 where the number of lines either be one (the same weights at all frequencies) 

62 or the length of the abscissa (for the case where a 3D matrix is supplied). 

63 reference_weighting_matrix : sdpy.Matrix or np.ndarray, optional 

64 Diagonal matrix used to weight reference degrees of freedom (generally for 

65 normalization) by multiplying the columns of the FRF matrix by a scalar weights. 

66 This matrix can also be a 3D matrix such that the the weights are different 

67 for each frequency line. The matrix should be sized 

68 [number of lines, number of references, number of references], where the number 

69 of lines either be one (the same weights at all frequencies) or the length 

70 of the abscissa (for the case where a 3D matrix is supplied). 

71 regularization_weighting_matrix : sdpy.Matrix or np.ndarray, optional 

72 Matrix used to weight input degrees of freedom via Tikhonov regularization. 

73 This matrix can also be a 3D matrix such that the the weights are different 

74 for each frequency line. The matrix should be sized 

75 [number of lines, number of references, number of references], where the number 

76 of lines either be one (the same weights at all frequencies) or the length 

77 of the abscissa (for the case where a 3D matrix is supplied). 

78 regularization_parameter : float or np.ndarray, optional 

79 Scaling parameter used on the regularization weighting matrix when the tikhonov 

80 method is chosen. A vector of regularization parameters can be provided so the 

81 regularization is different at each frequency line. The vector must match the 

82 length of the abscissa in this case (either be size [num_lines,] or [num_lines, 1]). 

83 cond_num_threshold : float or np.ndarray, optional 

84 Condition number used for SVD truncation when the threshold method is chosen. 

85 A vector of condition numbers can be provided so it varies as a function of 

86 frequency. The vector must match the length of the abscissa in this case. 

87 num_retained_values : float or np.ndarray, optional 

88 Number of singular values to retain in the pseudo-inverse when the truncation 

89 method is chosen. A vector of can be provided so the number of retained values 

90 can change as a function of frequency. The vector must match the length of the 

91 abscissa in this case. 

92 

93 Returns 

94 ------- 

95 np.ndarray 

96 Inverse of the supplied FRF matrix 

97 

98 Raises 

99 ------ 

100 Warning 

101 If regularization_weighting_matrix is supplied without selecting the tikhonov method 

102 Exception 

103 If the threshold method is chosen but a condition number threshold isn't supplied 

104 Exception 

105 If the declared method is not one of the available options 

106 

107 Notes 

108 ----- 

109 This function solves the inverse problem for the supplied FRF matrix. All of the inverse 

110 methods use the SVD (or modified SVD) to compute the pseudo-inverse. 

111 

112 References 

113 ---------- 

114 .. [1] Wikipedia, "Moore-Penrose inverse". 

115 https://en.wikipedia.org/wiki/Moore%E2%80%93Penrose_inverse 

116 .. [2] A.N. Tithe, D.J. Thompson, The quantification of structure-borne transmission pathsby inverse methods. Part 2: Use of regularization techniques, 

117 Journal of Sound and Vibration, Volume 264, Issue 2, 2003, Pages 433-451, ISSN 0022-460X, 

118 https://doi.org/10.1016/S0022-460X(02)01203-8. 

119 .. [3] Wikipedia, "Ridge regression". 

120 https://en.wikipedia.org/wiki/Ridge_regression 

121 """ 

122 if regularization_weighting_matrix is not None and method != 'tikhonov': 

123 warnings.warn("Regularization weighting matrix is only used during Tikhonov regularization and is not being used") 

124 

125 if response_weighting_matrix is not None: 

126 frf_matrix = response_weighting_matrix@frf_matrix 

127 if reference_weighting_matrix is not None: 

128 frf_matrix = frf_matrix@reference_weighting_matrix 

129 

130 if method == 'standard': 

131 frf_pinv = np.linalg.pinv(frf_matrix) 

132 elif method == 'threshold': 

133 if cond_num_threshold is None: 

134 raise ValueError('A condition number threshold must be supplied when using the threshold method') 

135 rcond_thresh = 1/cond_num_threshold 

136 frf_pinv = np.linalg.pinv(frf_matrix, rcond=rcond_thresh) 

137 elif method == 'tikhonov': 

138 frf_pinv = pinv_by_tikhonov(frf_matrix, 

139 regularization_weighting_matrix=regularization_weighting_matrix, 

140 regularization_parameter=regularization_parameter) 

141 elif method == 'truncation': 

142 frf_pinv = pinv_by_truncation(frf_matrix, 

143 num_retained_values=num_retained_values) 

144 else: 

145 error_string_start = 'Declared method "' 

146 error_string_end = '" is not an available option' 

147 raise NameError(error_string_start+method+error_string_end) 

148 

149 return frf_pinv 

150 

151 

152def pinv_by_tikhonov(frf_matrix, 

153 regularization_weighting_matrix=None, 

154 regularization_parameter=None): 

155 """ 

156 Computes the pseudo-inverse of an FRF matrix via Tikhonov regularization 

157 

158 Parameters 

159 ---------- 

160 frf_matrix : NDArray 

161 Transfer function as an np.ndarray, should be organized such that the 

162 frequency is the first axis, the responses are the second axis, and the 

163 references are the third axis 

164 regularization_weighting_matrix : sdpy.Matrix or np.ndarray, optional 

165 Matrix used to weight input degrees of freedom in the regularization, the 

166 default is identity. This can be a 3D matrix such that the the weights are 

167 different for each frequency line. The matrix should be sized 

168 [number of lines, number of references, number of references], where the number 

169 of lines either be one (the same weights at all frequencies) or the length 

170 of the abscissa (for the case where a 3D matrix is supplied). 

171 regularization_parameter : float or np.ndarray, optional 

172 Scaling parameter used on the regularization weighting matrix. A vector of 

173 regularization parameters can be provided so the regularization is different at 

174 each frequency line. The vector must match the length of the abscissa in this 

175 case (either be size [num_lines,] or [num_lines, 1]). 

176 

177 Returns 

178 ------- 

179 np.ndarray 

180 Inverse of the supplied FRF matrix 

181 

182 Raises 

183 ------ 

184 Exception 

185 If a regularization parameter isn't supplied 

186 Warning 

187 If a 2D regularization weighting matrix is supplied when a vector of regularization parameters is supplied 

188 

189 Notes 

190 ----- 

191 Tikhonov regularization is being done via the SVD method (details on Wikipedia or 

192 in the paper by Tithe and Thompson). 

193 

194 References 

195 ---------- 

196 .. [1] A.N. Tithe, D.J. Thompson, The quantification of structure-borne transmission pathsby inverse methods. Part 2: Use of regularization techniques, 

197 Journal of Sound and Vibration, Volume 264, Issue 2, 2003, Pages 433-451, ISSN 0022-460X, 

198 https://doi.org/10.1016/S0022-460X(02)01203-8. 

199 .. [2] Wikipedia, "Ridge regression". 

200 https://en.wikipedia.org/wiki/Ridge_regression 

201 """ 

202 

203 if regularization_parameter is None: 

204 raise ValueError('A regularization parameter must be supplied when using Tikhonov regularization') 

205 

206 regularization_parameter = np.asarray(regularization_parameter, dtype=np.float64) 

207 

208 u, s, vh = np.linalg.svd(frf_matrix, full_matrices=False) 

209 vh_conjT = np.swapaxes(vh.conj(), -2, -1) 

210 u_conjT = np.swapaxes(u.conj(), -2, -1) 

211 

212 # Need to add a second axis to the regularization parameter if it's a vector 

213 # so it broadcasts appropriately. 

214 if regularization_parameter.size > 1 and regularization_parameter.ndim == 1: 

215 regularization_parameter = regularization_parameter[..., np.newaxis] 

216 

217 if regularization_parameter.size > 1 and regularization_parameter.shape[1] > 1: 

218 raise ValueError('The regularization parameter is not a scalar or vector and cannot be interpreted') 

219 

220 if regularization_parameter.size > 1 and regularization_parameter.size != frf_matrix.shape[0]: 

221 raise ValueError('The regularization parameter is a vector but the size' 

222 + ' does not match the number of frequency lines and cannot be broadcasted to the FRFs') 

223 

224 # This is adding a third dimension to the regularization weighting matrix (if 

225 # it doesn't already exist) in the case that the regularization parameter is a 

226 # vector, this is required so the weighting matrix and parameter will broadcast 

227 # together appropriately. 

228 if regularization_weighting_matrix is not None and regularization_parameter.size > 1 and regularization_weighting_matrix.ndim == 2: 

229 warnings.warn('The regularization weighting matrix is 2D while the regularization' 

230 + ' parameter is a vector, the regularization weighting matrix is ' 

231 + 'being expanded to 3D. Consider adding a third dimension to the regularization weighting matrix as appropriate') 

232 desired_shape = [regularization_parameter.shape[0], regularization_weighting_matrix.shape[0], regularization_weighting_matrix.shape[1]] 

233 regularization_weighting_matrix = np.broadcast_to(regularization_weighting_matrix, desired_shape) 

234 

235 # This is adding a third dimension to the regularization parameter vector when a 

236 # regularization weighting matrix is being used. This is required so the broadcasting 

237 # works correctly. I.E., if the regularization weighting matrix is sized 

238 # [num_lines, num_refs, num_refs] (which it always is), the regularization parameter 

239 # vector has to be sized [num_lines, 1, 1]. This is not required when the regularization 

240 # parameter is a scaler. 

241 if (regularization_weighting_matrix is not None 

242 and regularization_weighting_matrix.ndim == 3 

243 and regularization_parameter.size > 1 and regularization_parameter.ndim != 3): 

244 regularization_parameter = regularization_parameter[..., np.newaxis] 

245 

246 if regularization_weighting_matrix is None: 

247 regularized_sInv = s/(s*s+regularization_parameter) 

248 if frf_matrix.ndim == 3: 

249 # This turns s into a 3D matrix with the singular values on the diagonal 

250 regularized_sInv = np.apply_along_axis(np.diag, 1, regularized_sInv) 

251 else: 

252 # This is for the case where a single frequency line is supplied for the FRF 

253 regularized_sInv = np.diag(regularized_sInv) 

254 else: 

255 if frf_matrix.ndim == 3: 

256 # This turns s into a 3D matrix with the singular values on the diagonal 

257 s = np.apply_along_axis(np.diag, 1, s) 

258 regularized_sInv = np.linalg.pinv(np.moveaxis(s, -1, -2)@s+regularization_weighting_matrix*regularization_parameter)@np.moveaxis(s, -1, -2) 

259 else: 

260 # This is for the case where a single frequency line is supplied for the FRF 

261 regularized_sInv = s/(s*s+regularization_parameter*regularization_weighting_matrix) 

262 

263 frf_pinv = vh_conjT@regularized_sInv@u_conjT 

264 

265 return frf_pinv 

266 

267 

268def pinv_by_truncation(frf_matrix, 

269 num_retained_values): 

270 """ 

271 Computes the pseudo-inverse of an FRF matrix where a fixed number of singular 

272 values are retained for the inverse 

273 

274 Parameters 

275 ---------- 

276 frf_matrix : NDArray 

277 Transfer function as an np.ndarray, should be organized such that the 

278 frequency is the first axis, the responses are the second axis, and the 

279 references are the third axis 

280 num_retained_values : float or np.ndarray, optional 

281 Number of singular values to retain in the pseudo-inverse when the truncation 

282 method is chosen. A vector of can be provided so the number of retained values 

283 can change as a function of frequency. The vector must match the length of the 

284 abscissa in this case. 

285 

286 Returns 

287 ------- 

288 np.ndarray 

289 Inverse of the supplied FRF matrix 

290 

291 Raises 

292 ------ 

293 Exception 

294 If a number of retained values isn't supplied 

295 Warning 

296 if the number of retained values is greater than the actual number of singular 

297 values 

298 """ 

299 if num_retained_values is None: 

300 raise ValueError('The number of retained values must be supplied when using the truncation method') 

301 

302 num_retained_values = np.asarray(num_retained_values, dtype=np.intc) 

303 

304 # Need to add a second axis to the number of retained values if it's a vector 

305 # so it broadcasts appropriately. 

306 if num_retained_values.size > 1 and num_retained_values.ndim == 1: 

307 num_retained_values = num_retained_values[..., np.newaxis] 

308 

309 if num_retained_values.size > 1 and num_retained_values.shape[1] > 1: 

310 raise ValueError('The number of retained values parameter is not a scalar or vector and cannot be interpreted') 

311 

312 if num_retained_values.size > 1 and num_retained_values.size != frf_matrix.shape[0]: 

313 raise ValueError('The number of retained values parameter is a vector but ' 

314 + 'the size does not match the number of frequency lines and cannot be broadcasted to the FRFs') 

315 

316 u, s, vh = np.linalg.svd(frf_matrix, full_matrices=False) 

317 vh_conjT = np.swapaxes(vh.conj(), -2, -1) 

318 u_conjT = np.swapaxes(u.conj(), -2, -1) 

319 

320 # The weird looking logic is to handle if num_retained_values is either a string 

321 # or a scaler. It also changes the number of retained values in the case that 

322 # it is greater than the actual number of singular values (it changes to retain 

323 # all the singular values). 

324 if num_retained_values.size > 1 and any(num_retained_values > s.shape[1]) or num_retained_values.size == 1 and num_retained_values > s.shape[1]: 

325 warnings.warn('The requested number of retained singular values is greater than the ' 

326 + 'actual number of singular values (for at least one frequency line). All singular values are being retained.') 

327 num_retained_values[num_retained_values > s.shape[1]] = s.shape[1] 

328 

329 s_truncated_inv = np.zeros((s.shape[0], s.shape[1], s.shape[1])) 

330 num_lines = s.shape[0] 

331 

332 # Making num_retained_values a vector if an integer was supplied 

333 if not num_retained_values.size > 1: 

334 num_retained_values = (np.ones(num_lines) * num_retained_values)[..., np.newaxis] 

335 

336 num_retained_values = num_retained_values.astype(int) 

337 

338 for ii in range(num_lines): 

339 if np.any(s[ii, :num_retained_values[ii, 0]] == 0): 

340 continue 

341 else: 

342 s_truncated_inv[ii, :num_retained_values[ii, 0], :num_retained_values[ii, 0]] = np.diag(1/s[ii, :num_retained_values[ii, 0]]) 

343 

344 frf_pinv = vh_conjT@s_truncated_inv@u_conjT 

345 

346 return frf_pinv 

347 

348 

349def compute_tikhonov_modified_singular_values(original_singular_values, regularization_parameter): 

350 """ 

351 Computes the modified singular values that would be seen in Tikhonov 

352 regularization. This is only intended for visualization purposes 

353 only. 

354 

355 Parameters 

356 ---------- 

357 original_singular_values : np.ndarray 

358 The singular values from the oringinal FRF matrix. Should be organized 

359 [frequency lines, singular values.] 

360 regularization_parameter : float or np.ndarray 

361 The regularization parameter that would be used in an inverse 

362 via Tikhonov regularization. 

363 

364 Returns 

365 ------- 

366 modified_singular_values : np.ndarray 

367 The modified singular values from the Tikhonov regularization. 

368 

369 Notes 

370 ----- 

371 This is not a mathematically rigorous version of the modified singular 

372 values and should only be used for subjective evaluation of the effect of 

373 the regularization parameter. 

374 """ 

375 regularization_parameter = np.asarray(regularization_parameter, dtype=np.float64) 

376 

377 if regularization_parameter.size != 1 and regularization_parameter.ndim == 1: 

378 regularization_parameter = regularization_parameter[..., np.newaxis] 

379 

380 modified_singular_values = (original_singular_values**2 + regularization_parameter)/original_singular_values 

381 

382 return modified_singular_values