diff --git a/pypk/api_core.py b/pypk/api_core.py index 4787eb10579d712050fa867719dad899a1688cf0..3d61916cd5e28ae8d1dee3d5acc33d148277c428 100644 --- a/pypk/api_core.py +++ b/pypk/api_core.py @@ -41,7 +41,7 @@ def init_pypk(cfg): agg = KSAggregator(cfg['rho_ks']) # p-k method if cfg['method'] == 'pk': - sol = Pk(name, flu, eig, agg, cfg['mach'], cfg['k_ref'], cfg['l_ref'], cfg['vrb'], cfg['n_modes'], n_speed, tol=1e-3) + sol = Pk(name, flu, eig, agg, cfg['mach'], cfg['k_ref'], cfg['l_ref'], cfg['vrb'], cfg['n_modes'], n_speed, tol=1e-3, max_it=10) elif cfg['method'] == 'nipk': sol = Nipk(name, flu, eig, agg, cfg['mach'], cfg['k_ref'], cfg['l_ref'], cfg['vrb'], cfg['n_modes'], n_speed) else: diff --git a/pypk/eigensolver.py b/pypk/eigensolver.py index 9ff0e6e787ce202567799d10ab85244b85e5654a..6c2872158884f39021a5a432a937c2f5e0bbfc51 100644 --- a/pypk/eigensolver.py +++ b/pypk/eigensolver.py @@ -25,22 +25,31 @@ class EigenSolver: self._n = n # number of modes self._g = (1 + 1j * g) # structural damping (complex proportional stiffness) - def compute(self, rho, u, m, k, q): + def compute_natural(self, m, k): + """Compute natural frequencies and mode shapes + m : mass matrix + k : stiffness matrix + """ + l, v = spla.eig(k, m) # det(-omega^2 * M + K) = 0 + return np.sqrt(np.real(l)), v + + def compute(self, rho, u, m, k, q, v0=None): """Compute eigenvalues and eigenvectors rho : fuid density u : freestream velocity m : mass matrix k : stiffness matrix q : aerodynamic matrix + v0 : converged mode shapes from previous iteration (airspeed) """ p, v = spla.eig(self._g * k - 0.5 * rho * u**2 * q, -u**2 / self._l**2 * m) # det((p*u/l)^2*M + K - q_inf*Q) = 0 p = np.sqrt(p) # Sort p = np.where(np.imag(p) < 0, -p, p) # change sign of p if imag(p) < 0 - srt = np.imag(p).argsort() - # Normalize (optional since scipy does it) - for j in range(self._n): - v[:,j] /= spla.norm(v[:,j]) + if v0 is None: + srt = np.imag(p).argsort() + else: + srt = self._sort(v, v0) return p[srt].T, v[:,srt] def compute_gradients(self, rho, u, m, k, q, p, v, dm, dk, dq): @@ -66,3 +75,16 @@ class EigenSolver: b[-1,0] = 0 x = spla.lu_solve((spla.lu_factor(a)), b) return x[0,0] + + def _sort(self, v, v0): + """Sort current modes according to their correlation to previous modes + v : mode shapes from current iteration + v0 : converged mode shapes from previous iteration + """ + # Compute correlation matrix + mac = np.zeros((self._n, self._n)) + for i in range(self._n): + for j in range(self._n): + mac[i,j] = np.linalg.norm(np.dot(v[:,i], np.conjugate(v0[:,j]))) / (np.linalg.norm(v[:,i]) * np.linalg.norm(v0[:,j])) + # Find maximum for each mode + return np.argmax(mac, axis=0) diff --git a/pypk/nipk.py b/pypk/nipk.py index c6e875996d848ecdb42ced55046f3c7ce29dd3b5..c097f7407a7afba49341c14483010ddac45c186a 100644 --- a/pypk/nipk.py +++ b/pypk/nipk.py @@ -33,21 +33,21 @@ class Nipk(Solution): def compute(self): """Compute frequencies and dampings """ + omega_0, v_0 = self._eig.compute_natural(self._m, self._k) # natural frequencies and modes self.rho, self.speed = self._flu.compute(self._mach) for ivel in range(self.n_speed): # Compute eigenvalues at zero airspeed if self.speed[ivel] == 0: - for imod in range(self.n_modes): - self.freq[ivel, imod] = 0. + self.freq[ivel, :] = omega_0 continue # Compute eigenvalues for each reference reduced frequency p_k = np.zeros((self.n_freqs, self.n_modes), dtype=complex) v_k = np.zeros((self.n_freqs, self.n_modes, self.n_modes), dtype=complex) for ifrq in range(self.n_freqs): - p_k[ifrq,:], v_k[ifrq,:,:] = self._eig.compute(self.rho[ivel], self.speed[ivel], self._m, self._k, self._q[ifrq,:,:]) + p_k[ifrq,:], v_k[ifrq,:,:] = self._eig.compute(self.rho[ivel], self.speed[ivel], self._m, self._k, self._q[ifrq,:,:], v_0) # Match reduced frequency by linear interpolation for each mode for imod in range(self.n_modes): - delta, idx = self.__find_index(self.speed[ivel], p_k[:, imod], self._kref) + delta, idx = self.__find_index(self.speed[ivel], imod, p_k[:, imod], self._kref) p_re, p_im = self.__interp(self._kref[idx], delta[idx], p_k[idx, imod]) # Compute frequency and damping self.freq[ivel, imod] = p_im * self.speed[ivel] / self._lref / (2 * np.pi) @@ -59,6 +59,11 @@ class Nipk(Solution): self._pk[ivel, imod, :] = p_k[idx, imod] self._vk[ivel, imod, :, :] = v_k[idx, :, imod] self._p[ivel, imod] = p_re + 1j * p_im + # Interpolate mode shapes for mode tracking + v_0 = np.zeros((self.n_modes, self.n_modes), dtype=complex) + for imod in range(self.n_modes): + a = (self._p[ivel, imod].imag - self._kk[ivel, imod, 0]) / (self._kk[ivel, imod, 1] - self._kk[ivel, imod, 0]) + v_0[:, imod] = (1 - a) * self._vk[ivel, imod, 0, :] + a * self._vk[ivel, imod, 1, :] # Compute aggregated damping over modes then over dynamic pressures self._ksm = np.zeros(self.n_speed) for i in range(self.n_speed): @@ -87,9 +92,10 @@ class Nipk(Solution): dq[np.unravel_index(i, dq.shape)] = 1. self.damp_qim[i] = self.__compute_dg(z, z, zk+1j*dq) - def __find_index(self, u, p, k): + def __find_index(self, u, m, p, k): """Find the indices between which the frequency must be interpolated u : freestream velocity + m : mode number p : eigenvalue solutions k : reference reduced frequencies """ @@ -109,7 +115,7 @@ class Nipk(Solution): elif ai == len(delta) - 1: idx = [ai-1, ai] else: - raise RuntimeError(f'NIPK: could not match frequency for mode {i} at velocity {u}!\n') + raise RuntimeError(f'NIPK: could not match frequency for mode {m} at velocity {u}!\n') return delta, idx def __interp(self, k, delta, p): diff --git a/pypk/pk.py b/pypk/pk.py index 695ba7f4c12401625407f40fe1073e9f4479fbd7..803cfd1b71b2b92b985f34c8a61a3f83b82cd0f4 100644 --- a/pypk/pk.py +++ b/pypk/pk.py @@ -17,41 +17,48 @@ from .solution import Solution import numpy as np from scipy import interpolate -import scipy.linalg as spla class Pk(Solution): """Standard p-k method for flutter solution """ - def __init__(self, name, flu, eig, agg, mach, k_ref, l_ref, vrb, n_modes, n_speed, tol=1e-3): + def __init__(self, name, flu, eig, agg, mach, k_ref, l_ref, vrb, n_modes, n_speed, tol=1e-3, max_it=10): super().__init__(name, flu, eig, agg, mach, k_ref, l_ref, vrb, n_modes, n_speed) self.name = 'PK' # solver name self._tol = tol # tolerance + self._max_it = max_it # maximum number of iterations def compute(self): """Compute frequencies and dampings """ - omega = self.__sfreq(self._m, self._k) # natural frequencies + omega_0, v_0 = self._eig.compute_natural(self._m, self._k) # natural frequencies and modes self.rho, self.speed = self._flu.compute(self._mach) for ivel in range(self.n_speed): # Compute eigenvalues at zero airspeed if self.speed[ivel] == 0: - for imod in range(self.n_modes): - self.freq[ivel, imod] = 0. + self.freq[ivel, :] = omega_0 continue + v_tmp = np.zeros((self.n_modes, self.n_modes), dtype=complex) # temporary container for modes for imod in range(self.n_modes): - k = omega[imod] * self._lref / self.speed[ivel] # guess reduced frequency + k = omega_0[imod] * self._lref / self.speed[ivel] # guess reduced frequency + it = 0 while True: - # Interpolate loads and solve eigenvalue problem + # Interpolate loads and solve eigenvalue problem q = self.__interp(k, self._kref, self._q) - p, _ = self._eig.compute(self.rho[ivel], self.speed[ivel], self._m, self._k, q) + p, v = self._eig.compute(self.rho[ivel], self.speed[ivel], self._m, self._k, q, v_0) p = p[imod] # Check if reduced frequency is converged - if abs(np.imag(p) - k) < self._tol: + if abs(np.imag(p) - k) < self._tol or it == self._max_it: + v_tmp[:, imod] = v[:, imod] self.freq[ivel, imod] = np.imag(p) * self.speed[ivel] / self._lref / (2 * np.pi) self.damp[ivel, imod] = np.real(p) / np.imag(p) + if it == self._max_it: + print(f'PK: could not converge for mode {imod} at velocity {self.speed[ivel]}, using k={np.imag(p)} with tolerance {abs(np.imag(p) - k)}.') break else: k = np.imag(p) + it += 1 + # Update modes for mode tracking + v_0 = v_tmp # Compute aggregated damping over modes then over dynamic pressures self._ksm = np.zeros(self.n_speed) for i in range(self.n_speed): @@ -63,15 +70,6 @@ class Pk(Solution): """ raise NotImplementedError() - def __sfreq(self, m, k): - """Compute the natural frequencies of the structural model - TODO: should it be given directly by the structural solver? - m : mass matrix - k : stiffness matrix - """ - om, _ = spla.eig(k, m) # det(-omega^2 * M + K) = 0 - return np.sqrt(np.real(om)) - def __interp(self, k, k_ref, q_ref): """Match the frequency and interpolate the eigenvalues and eigenvectors k : interpolation reduced frequency