diff --git a/pypk/api_core.py b/pypk/api_core.py index ff57890cdc3241c909abf76a185bc067e483fde2..d5bab2b2c88f8426f6b62935965d1bf79e609c3a 100644 --- a/pypk/api_core.py +++ b/pypk/api_core.py @@ -23,6 +23,8 @@ from .nipk import Nipk def init_pypk(cfg): """Initialize PyPK """ + # Case name + name = cfg['name'] if 'name' in cfg else 'noname' # Fluid type if cfg['fluid'] == 'unmatched': n_speed = len(cfg['u_idx']) # number of airspeed test points @@ -38,9 +40,9 @@ def init_pypk(cfg): agg = KSAggregator(cfg['rho_ks']) # p-k method if cfg['method'] == 'pk': - sol = Pk(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) elif cfg['method'] == 'nipk': - sol = Nipk(flu, eig, agg, cfg['mach'], cfg['k_ref'], cfg['l_ref'], cfg['vrb'], cfg['n_modes'], n_speed) + sol = Nipk(name, flu, eig, agg, cfg['mach'], cfg['k_ref'], cfg['l_ref'], cfg['vrb'], cfg['n_modes'], n_speed) else: raise RuntimeError('"method" entry must be "pk" or "nipk"!\n') return sol diff --git a/pypk/api_om.py b/pypk/api_om.py index 95861575dcc4c16c33f010ffc807eb98d45b3b9e..894bfe0a2968c69cdec27e6fed293aa73363bc28 100644 --- a/pypk/api_om.py +++ b/pypk/api_om.py @@ -23,11 +23,13 @@ class PypkSolver(om.ExplicitComponent): """OpenMDAO flutter solution wrapper """ def initialize(self): + self.options.declare('scn_name', desc='name of current scenario') self.options.declare('nm', desc='number of modes') self.options.declare('nu', desc='number of airspeed test points') self.options.declare('sol', desc='PyPK solver', recordable=False) def setup(self): + self.scn_name = self.options['scn_name'] self.nm = self.options['nm'] self.nu = self.options['nu'] self.sol = self.options['sol'] @@ -41,18 +43,26 @@ class PypkSolver(om.ExplicitComponent): self.add_output('f_speed', val=1., desc='flutter speed') self.add_output('f_mode', val=1, desc='flutter mode') # Partials - # self.declare_partials(of='g_ks', wrt=['M', 'K', 'Q_re', 'Q_im'], method='exact') + self.declare_partials(of='g_agg', wrt=['M', 'K', 'Q_re', 'Q_im'], method='exact') def compute(self, inputs, outputs): self.sol.set_matrices(inputs['M'], inputs['K'], inputs['Q_re'] + 1j * inputs['Q_im']) self.sol.compute() self.sol.find_flutter() + self.sol.save('_' + self.scn_name) outputs['f'] = self.sol.freq outputs['g'] = self.sol.damp outputs['g_agg'] = self.sol.damp_agg outputs['f_speed'] = self.sol.f_speed outputs['f_mode'] = self.sol.f_mode + def compute_partials(self, inputs, partials): + self.sol.compute_gradients() + partials['g_agg', 'M'] = self.sol.damp_m + partials['g_agg', 'K'] = self.sol.damp_k + partials['g_agg', 'Q_re'] = self.sol.damp_qre + partials['g_agg', 'Q_im'] = self.sol.damp_qim + class PypkBuilder(Builder): """PyPK builder for OpenMDAO """ @@ -66,7 +76,7 @@ class PypkBuilder(Builder): """ self.__sol = init_pypk(cfg) - def get_solver(self): + def get_solver(self, scenario_name=''): """Return OpenMDAO component containing the solver """ - return PypkSolver(nm=self.__sol.n_modes, nu=self.__sol.n_speed, sol=self.__sol) + return PypkSolver(scn_name=scenario_name, nm=self.__sol.n_modes, nu=self.__sol.n_speed, sol=self.__sol) diff --git a/pypk/nipk.py b/pypk/nipk.py index 7b5110633e9785d449b3c573e45fe4964196d6b1..c6e875996d848ecdb42ced55046f3c7ce29dd3b5 100644 --- a/pypk/nipk.py +++ b/pypk/nipk.py @@ -20,8 +20,8 @@ import numpy as np class Nipk(Solution): """Non-iterative p-k method for flutter solution """ - def __init__(self, flu, eig, agg, mach, k_ref, l_ref, vrb, n_modes, n_speed): - super().__init__(flu, eig, agg, mach, k_ref, l_ref, vrb, n_modes, n_speed) + def __init__(self, name, flu, eig, agg, mach, k_ref, l_ref, vrb, n_modes, n_speed): + super().__init__(name, flu, eig, agg, mach, k_ref, l_ref, vrb, n_modes, n_speed) self.name = 'NIPK' # solver name self._pk = np.zeros((self.n_speed, self.n_modes, 2), dtype=complex) # eigenvalues (at reference reduced frequencies) self._vk = np.zeros((self.n_speed, self.n_modes, 2, self.n_modes), dtype=complex) # eigenmodes (at reference reduced frequencies) diff --git a/pypk/pk.py b/pypk/pk.py index e4aa9d28d6278d108c312ff664068ba4d936aa26..695ba7f4c12401625407f40fe1073e9f4479fbd7 100644 --- a/pypk/pk.py +++ b/pypk/pk.py @@ -22,8 +22,8 @@ import scipy.linalg as spla class Pk(Solution): """Standard p-k method for flutter solution """ - def __init__(self, flu, eig, agg, mach, k_ref, l_ref, vrb, n_modes, n_speed, tol=1e-3): - super().__init__(flu, eig, agg, mach, k_ref, l_ref, vrb, n_modes, n_speed) + def __init__(self, name, flu, eig, agg, mach, k_ref, l_ref, vrb, n_modes, n_speed, tol=1e-3): + 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 diff --git a/pypk/solution.py b/pypk/solution.py index a14acc25f015ade6a719887d9db2833a2650023e..d2c9c7cf1d127750b4acce63ecaf6ab9dc43fa86 100644 --- a/pypk/solution.py +++ b/pypk/solution.py @@ -19,8 +19,9 @@ import numpy as np class Solution: """Base class for solving the aeroelastic equation """ - def __init__(self, flu, eig, agg, mach, k_ref, l_ref, vrb, n_modes, n_speed): + def __init__(self, name, flu, eig, agg, mach, k_ref, l_ref, vrb, n_modes, n_speed): self.name = None # solver name + self._name = name # case name self._flu = flu # fluid type self._eig = eig # eigen solver self._agg = agg # aggregator @@ -69,7 +70,7 @@ class Solution: for imod in range(self.n_modes): if (self.damp[ivel, imod] > 0 or self.damp[ivel, imod] < 0 and self.damp[ivel+1, imod] > 0): if self._v > 0: - print('{:s}: flutter found at Mach {:.2f} for mode #{:d} near u_inf = {:.0f}.'.format(self.name, self._mach, imod, self.speed[ivel])) + print('{:s}: flutter found at Mach {:.2f} for mode #{:d} at {:.0f} Hz near u_inf = {:.0f}.'.format(self.name, self._mach, imod, self.freq[ivel, imod], self.speed[ivel])) self.f_speed = self.speed[ivel] self.f_mode = imod found = True @@ -77,21 +78,21 @@ class Solution: if self._v > 0 and not found: print('{:s}: no flutter found at Mach {:.2f} for speed range [{:.0f}; {:.0f}].'.format(self.name, self._mach, self.speed[1], self.speed[-1])) - def save(self, sfx): + def save(self, sfx=''): """Write results to disk sfx : filename suffix """ hdr = '{:>8s}, {:>10s}, {:>10s}, {:>10s}'.format('Mach', 'u_f', 'mode', 'AGG(g)') - np.savetxt(f'results_{sfx}.dat', np.array([self._mach, self.f_speed, self.f_mode, self.damp_agg]).reshape(1,4), fmt='%10.4f', delimiter=', ', header=hdr) + np.savetxt(f'results_{self._name}{sfx}.dat', np.array([self._mach, self.f_speed, self.f_mode, self.damp_agg]).reshape(1,4), fmt='%10.4f', delimiter=', ', header=hdr) hdr = '{:>9s}'.format('u_inf') for j in range(self.n_modes): hdr += ', {:>11s}'.format(f'f_{j}') for j in range(self.n_modes): hdr += ', {:>11s}'.format(f'g_{j}') data = np.concatenate((self.speed.reshape(self.speed.shape[0], 1), self.freq, self.damp), axis=1) - np.savetxt(f'vgf_{sfx}.dat', data, fmt='%+.4e', delimiter=', ', header=hdr) + np.savetxt(f'vgf_{self._name}{sfx}.dat', data, fmt='%+.4e', delimiter=', ', header=hdr) - def plot(self, sfx, show=True, format=''): + def plot(self, sfx='', show=True, format=''): """Plot frequency and damping as a function of the velocity show : whether to display the plot format : format to which the plot will be written to disk, if not empty @@ -116,5 +117,5 @@ class Solution: axs[1].spines['right'].set_visible(False) axs[1].plot(u, [0.] * self.n_speed, color='k', lw=1, ls='-.') # zero-damping line fig.tight_layout() - if format: plt.savefig(f'vgf_{sfx}.' + format) + if format: plt.savefig(f'vgf_{self._name}{sfx}.' + format) if show: plt.show() diff --git a/tests/agard.py b/tests/agard.py index 489f3395e20adf2d7fa1715c1ccef55be4ba08ba..a62f30fb5bd5029657c5694eab46668054eee5da 100644 --- a/tests/agard.py +++ b/tests/agard.py @@ -23,6 +23,7 @@ import numpy as np def get_cfg(): cfg = { + 'name': 'agard', # case name 'k_ref': np.array([0.1, 0.3]), # reference reduced frequencies 'l_ref': 0.5 * 0.559, # reference length (half root chord) 'mach': 0.901, # freesteam Mach number @@ -60,8 +61,8 @@ def compute(sol): sol.compute() sol.find_flutter() # Save results - sol.save('agard_' + sol.name.lower()) - sol.plot('agard_' + sol.name.lower(), show=False, format='.pdf') + sol.save('_' + sol.name.lower()) + sol.plot('_' + sol.name.lower(), show=False, format='pdf') def main(): # Set up p-k and non-iterative p-k method diff --git a/tests/agard_grad.py b/tests/agard_grad.py new file mode 100644 index 0000000000000000000000000000000000000000..e44e5c495abab311e0ba21c86532839b052275f3 --- /dev/null +++ b/tests/agard_grad.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# -*- coding: utf8 -*- +# test encoding: à -é-è-ô-ï-€ + +# Copyright 2024 University of Liège +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Compute flutter of AGARD 445.6 wing + +from pypk import init_pypk +import numpy as np + +def get_cfg(): + cfg = { + 'name': 'agard', # case name + 'k_ref': np.array([0.1, 0.3]), # reference reduced frequencies + 'l_ref': 0.5 * 0.559, # reference length (half root chord) + 'mach': 0.901, # freesteam Mach number + 'rho_inf': 0.0995, # freesteam density + 'u_idx': np.linspace(0.2, 0.5, 31, endpoint=True), # velocity index range + 'mu': 143.920, # mass ratio + 'f_ref': 40.3511, # frequency of first torsional mode + 'n_modes': 4, # number of modes + 'rho_ks': 1e6, # KS aggregation parameter for KS + 'method': 'nipk', # method type + 'fluid': 'unmatched', # fluid type + 'vrb': 1 # verbosity level + } + return cfg + +def get_matrices(): + # Structural mass matrix + m = np.diag([1.0, 1.0, 1.0, 1.0]) + # Structural stiffness matrix + k = np.diag([3695.2, 63392.2, 99892.1, 368026.0]) + # Generalized aerodynamic force matrices (at k=0.1 and k=0.3) + q = np.array([[[-7.747e-01-2.058e-01j, -6.014e+00-1.949e-01j, 3.962e+00+6.734e-02j, -3.403e+00-1.784e-01j], + [ 9.520e-01-1.703e-02j, 4.900e+00-1.707e+00j, -4.416e+00+1.048e+00j, -5.951e+00-3.470e-01j], + [-6.597e-02+6.036e-02j, 3.580e-01+5.907e-01j, -1.046e+00-6.893e-01j, -8.272e-02-6.273e-01j], + [-1.238e-01+8.540e-02j, 3.588e-01+5.434e-01j, 8.221e-01-4.957e-01j, 2.922e+00-9.594e-01j]], + [[-6.560e-01-6.408e-01j, -5.628e+00-1.028e+00j, 3.637e+00+5.146e-01j, -3.498e+00-6.270e-01j], + [ 9.918e-01-1.579e-02j, 3.545e+00-3.832e+00j, -3.405e+00+2.284e+00j, -5.881e+00-6.028e-01j], + [-3.243e-02+1.393e-01j, 9.077e-01+1.124e+00j, -1.434e+00-1.622e+00j, -6.827e-01-1.797e+00j], + [ 3.645e-03+2.359e-01j, 1.521e+00+7.309e-01j, 1.015e-01-9.865e-01j, 2.600e+00-3.367e+00j]]] + ) + return m, k, q + +def main(): + # Set up non-iterative p-k method + nipk = init_pypk(get_cfg()) + m, k, q = get_matrices() + nipk.set_matrices(m, k, q) + # Compute solution + nipk.compute() + nipk.find_flutter() + # Compute analytical gradients + print('Computing NIPK gradients...') + nipk.compute_gradients() + np.savetxt('gradients_agard.dat', nipk.damp_k, fmt='%+.4e', delimiter=', ', header='dg/dk_i') + # Compute gradients using finite differences + eps = 1e-3 + dg = np.zeros(k.size) + for i in range(k.size): + idx = np.unravel_index(i, k.shape) + k_ref = k[idx] + k[idx] = k_ref + eps + nipk.compute() + gp = nipk.damp_agg + k[idx] = k_ref - eps + nipk.compute() + gm = nipk.damp_agg + dg[i] = (gp - gm) / (2 * eps) + k[idx] = k_ref + # Check + for i in range(k.size): + print(f'- checking gradient {i}/{k.size}') + diff = abs(nipk.damp_k[i] - dg[i]) / nipk.damp_k[i] + if diff > 1e-3: + raise RuntimeError('Test failed: relative error {} between analtical derivative ({}) and finite difference derivative ({}) is too large!\n'.format(diff, nipk.damp_k[i], dg[i])) + print('All tests ok.') + +if __name__ == '__main__': + main()