Skip to content

Commit 8a18eb9

Browse files
committed
add regression test for gust vane generator
Introduce a 6‑timestep gust vane simulation test to ensure backward compatibility. The test compares the simulation output against the current reference results to detect unintended changes in gust vane behavior. Adapt pazy related test modules to integrate gust vane generator functionalities. Fix missing import in writevariables time. Fix airfoil polar code.
1 parent 100c6ca commit 8a18eb9

File tree

6 files changed

+226
-68
lines changed

6 files changed

+226
-68
lines changed

sharpy/aero/utils/airfoilpolars.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def get_coefs(self, aoa_deg):
6161
cd = self.cd_interp(aoa_deg)
6262
cm = self.cm_interp(aoa_deg)
6363

64-
return cl[0], cd[0], cm[0]
64+
return cl, cd, cm
6565

6666
def get_aoa_deg_from_cl_2pi(self, cl):
6767

@@ -115,7 +115,7 @@ def get_cdcm_from_cl(self, cl):
115115
cd = np.interp(cl, self.table[i:i+2, 1], self.table[i:i+2, 2])
116116
cm = np.interp(cl, self.table[i:i+2, 1], self.table[i:i+2, 3])
117117

118-
return float(cd), float(cm)
118+
return cd, cm
119119

120120

121121
def interpolate(polar1, polar2, coef=0.5):

sharpy/postproc/writevariablestime.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import numpy as np
33
from sharpy.utils.solver_interface import solver, BaseSolver
44
import sharpy.utils.settings as settings_utils
5-
5+
import sharpy.aero.utils.uvlmlib as uvlmlib
66

77
@solver
88
class WriteVariablesTime(BaseSolver):
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import unittest
2+
import numpy as np
3+
import os
4+
from tests.coupled.static.test_pazy_static import TestPazyCoupled
5+
6+
class TestPazyCoupledDynamicWithGustVanes(TestPazyCoupled, unittest.TestCase):
7+
"""
8+
Test gust vanes with pazy wing in a dynamic coupled simulation with a free wake convection scheme. The induced velocity field
9+
and wing root loads and moments are compared after 20 timesteps with the reference case produced with SHARPy v2.0 for backward compability.
10+
"""
11+
12+
def test_dynamic_aoa_symmetry_with_gust_vanes(self):
13+
self.setup_test_folders('gust_vanes')
14+
cs_deflection_file = self.route_test_dir + '/gust_vanes/gust_vane_deflections.csv'
15+
16+
self.num_chordwise_panels //= 2
17+
self.num_spanwise_nodes //= 2
18+
self.n_tsteps = 6
19+
20+
self.run_test(True, dynamic=True, gust_vanes=True, cs_deflection_file=cs_deflection_file)
21+
22+
def evaluate_output(self):
23+
self.assert_induced_velocity_matches_reference(
24+
time_step=6,
25+
ref_u_ind_z=-0.4026807
26+
)
27+
28+
def assert_induced_velocity_matches_reference(self, time_step, ref_u_ind_z):
29+
"""
30+
Check if the vertical induced velocity at a given time step and node matches the reference.
31+
32+
Args:
33+
time_step (int): Index of the time step to check (e.g., 6)
34+
ref_u_ind_z (float): Reference vertical induced velocity [m/s]
35+
"""
36+
file_path = os.path.join(self.output_folder, self.case_name, 'WriteVariablesTime', 'vel_field_uind_point0.dat')
37+
induced_velocities = np.loadtxt(file_path)
38+
39+
actual_value = induced_velocities[time_step, -1]
40+
41+
np.testing.assert_almost_equal(
42+
actual_value, ref_u_ind_z, decimal=3,
43+
err_msg='Induced vertical velocity differs more than 0.1% from reference value.',
44+
verbose=True
45+
)
46+
47+
if __name__ == '__main__':
48+
unittest.main()
49+

tests/coupled/dynamic/test_pazy_dynamic.py

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,51 @@
33
import os
44
from tests.coupled.static.test_pazy_static import TestPazyCoupled
55

6-
class TestPazyCoupledDynamic(TestPazyCoupled):
6+
class TestPazyCoupledDynamic(TestPazyCoupled, unittest.TestCase):
77
"""
88
Test Pazy wing dynamic coupled case with a free wake convection scheme by compparing wing root loads and
9-
moments after 20 timesteps with the reference case produced with SHARPy v2.0 for backward compability.
9+
moments after 2 timesteps with the reference case produced with SHARPy v2.0 for backward compability.
1010
Further, symmetry condition is checked for a dynamic free wake as well.
1111
"""
1212

13-
def test_dynamic_aoa(self):
14-
self.run_test(False, dynamic = True)
15-
1613
def test_dynamic_aoa_symmetry(self):
14+
self.setup_test_folders('pazy')
15+
self.num_chordwise_panels //= 4
16+
self.num_spanwise_nodes //= 4
1717
self.run_test(True, dynamic = True)
1818

19-
def evaluate_output(self):
20-
ref_Fz = -1663.6376358639793
21-
ref_My = -12.656888844293645
22-
23-
file = os.path.join(self.output_folder, self.case_name, 'beam/beam_loads_%i.csv' % (self.n_tsteps))
24-
beam_loads_ts = np.loadtxt(file, delimiter=',')
25-
error_Fz = (float(beam_loads_ts[0, 6])-ref_Fz)/ref_Fz
26-
error_My = (float(beam_loads_ts[0, 8])-ref_My)/ref_My
27-
28-
np.testing.assert_almost_equal(error_Fz, 0.,
29-
decimal=3,
30-
err_msg='Vertical load on wing root differs more than 0.1 %% from reference value.',
31-
verbose=True)
32-
np.testing.assert_almost_equal(error_My, 0.,
33-
decimal=3,
34-
err_msg='Pitching moment on wing root differs more than 0.1 %% from reference value.',
35-
verbose=True)
19+
def evaluate_output(self):
20+
self.assert_root_forces_match_reference(ref_Fz=-2.5274941e+04,
21+
ref_My=-1.0703502e+01)
22+
23+
def assert_root_forces_match_reference(self, ref_Fz, ref_My):
24+
"""
25+
Check if the vertical force and pitching moment at the wing root match reference values.
26+
27+
Args:
28+
ref_Fz (float): Reference vertical force at wing root [N]
29+
ref_My (float): Reference pitching moment at wing root [Nm]
30+
"""
31+
file_path = os.path.join(self.output_folder, self.case_name, 'beam', f'beam_loads_{self.n_tsteps}.csv')
32+
beam_loads_ts = np.loadtxt(file_path, delimiter=',')
33+
34+
actual_Fz = float(beam_loads_ts[0, 6])
35+
actual_My = float(beam_loads_ts[0, 8])
36+
37+
error_Fz = (actual_Fz - ref_Fz) / ref_Fz
38+
error_My = (actual_My - ref_My) / ref_My
39+
40+
np.testing.assert_almost_equal(
41+
error_Fz, 0.0, decimal=3,
42+
err_msg='Vertical load on wing root differs more than 0.1% from reference value.',
43+
verbose=True
44+
)
45+
np.testing.assert_almost_equal(
46+
error_My, 0.0, decimal=3,
47+
err_msg='Pitching moment on wing root differs more than 0.1% from reference value.',
48+
verbose=True
49+
)
50+
3651

3752
if __name__ == '__main__':
3853
unittest.main()

tests/coupled/static/pazy/generate_pazy.py

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ def generate_pazy(u_inf, case_name, output_folder='/output/', cases_folder='', *
99
num_modes = 16
1010
gravity_on = kwargs.get('gravity_on', True)
1111
symmetry_condition = kwargs.get('symmetry_condition', False)
12+
gust_vanes = kwargs.get('gust_vanes', False)
1213
dynamic = kwargs.get('dynamic', False)
1314
n_tsteps = kwargs.get('n_tsteps', 1)
14-
15+
cs_deflection_file=kwargs.get('cs_deflection_file', None)
1516
# Lattice Discretisation
1617
M = kwargs.get('M', 4)
1718
N = kwargs.get('N', 32)
@@ -42,15 +43,19 @@ def generate_pazy(u_inf, case_name, output_folder='/output/', cases_folder='', *
4243
ws.reduce_model_to_symmetric_wing()
4344
ws.generate_aero_file()
4445
ws.generate_fem_file()
45-
set_final_settings(ws, dynamic,
46+
set_final_settings(ws,
47+
dynamic=dynamic,
48+
surface_m=M,
4649
output_folder=output_folder,
4750
symmetry_condition = symmetry_condition,
51+
gust_vanes = gust_vanes,
4852
gravity_on = gravity_on,
49-
n_tsteps=n_tsteps)
53+
n_tsteps=n_tsteps,
54+
cs_deflection_file=cs_deflection_file)
5055

5156
sharpy.sharpy_main.main(['', ws.route + ws.case_name + '.sharpy'])
5257

53-
def set_final_settings(ws, dynamic = False, output_folder='/output/', symmetry_condition = False, gravity_on = True, n_tsteps=1,flag_multiple_mstar_input=False):
58+
def set_final_settings(ws, dynamic = False, surface_m=8, output_folder='/output/', symmetry_condition = False, gravity_on = True, n_tsteps=1,flag_multiple_mstar_input=False, gust_vanes = False, cs_deflection_file=None):
5459
ws.config['SHARPy'] = {
5560
'flow':
5661
['BeamLoader',
@@ -204,7 +209,80 @@ def set_final_settings(ws, dynamic = False, output_folder='/output/', symmetry_c
204209
},
205210
}
206211
ws.config['BeamLoads'] = {'csv_output': True}
207-
212+
if gust_vanes:
213+
import numpy as np
214+
ws.config = apply_gust_vane_settings(ws.config,
215+
cs_deflection_file,
216+
ws.dt,
217+
ws.u_inf,
218+
surface_m,
219+
False,
220+
symmetry_condition)
221+
ws.config['DynamicCoupled']['postprocessors'].append('WriteVariablesTime')
222+
ws.config['DynamicCoupled']['postprocessors_settings']['WriteVariablesTime'] = {
223+
'vel_field_variables': ['uind'],
224+
'vel_field_points': np.array([-1.25, 0.0, 0.25 ]),
225+
}
208226
ws.config.write()
209227

210228
sharpy.sharpy_main.main(['', ws.route + ws.case_name + '.sharpy'])
229+
230+
def apply_gust_vane_settings(settings,
231+
cs_deflection_file,
232+
dt,
233+
u_inf,
234+
surface_m,
235+
vertical,
236+
symmetry_condition):
237+
"""
238+
Updates the SHARPy configuration with gust vane definitions.
239+
240+
Args:
241+
settings: SHARPy configuration object.
242+
cs_deflection_file: Path to the gust vane deflection file.
243+
dt: Time step size.
244+
u_inf: Freestream velocity.
245+
surface_m: Chordwise discretisation.
246+
vertical: Orientation of the gust vanes (vertical or horizontal).
247+
symmetry_condition: Whether the problem is symmetric.
248+
only_gust_vanes: Whether gust vanes are the only gust source.
249+
250+
Returns:
251+
cs_deflection_file. Modified the input settings dictionary.
252+
"""
253+
# breakpoint()
254+
wake_length_vanes = 5
255+
gust_vane_parameters = {
256+
'M': surface_m * 3,
257+
'N': 40,
258+
'M_star': int(wake_length_vanes / (dt * u_inf)),
259+
'span': 5,
260+
'chord': 0.3,
261+
'control_surface_deflection_generator_settings': {
262+
'dt': dt,
263+
'deflection_file': cs_deflection_file
264+
}
265+
}
266+
267+
settings['AerogridLoader']['gust_vanes'] = True
268+
settings['AerogridLoader']['gust_vanes_generator_settings'] = {
269+
'n_vanes': 2,
270+
'streamwise_position': [-1.5, -1.5],
271+
'vertical_position': [-0.25, 0.25],
272+
'symmetry_condition': symmetry_condition,
273+
'vane_parameters': [gust_vane_parameters, gust_vane_parameters],
274+
'vertical': vertical
275+
}
276+
277+
# Override velocity field for gust vanes
278+
stepuvlm_updates = {
279+
'convection_scheme': 3,
280+
'velocity_field_generator': 'SteadyVelocityField',
281+
'velocity_field_input': {
282+
'u_inf': u_inf,
283+
'u_inf_direction': [1., 0., 0.],
284+
}
285+
}
286+
settings['DynamicCoupled']['aero_solver_settings'].update(stepuvlm_updates)
287+
288+
return settings

tests/coupled/static/test_pazy_static.py

Lines changed: 54 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,43 +11,46 @@ class TestPazyCoupled(unittest.TestCase):
1111
def setUp(self):
1212
self.u_inf = 50
1313
self.alpha = 7
14-
self.M = 16
15-
self.N= 64
14+
self.num_chordwise_panels = 16
15+
self.num_spanwise_nodes= 64
1616
self.Msf = 1
17-
18-
self.n_tsteps = 20
19-
20-
self.route_test_dir = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
21-
self.cases_folder = self.route_test_dir + '/pazy/cases/'
22-
self.output_folder = self.route_test_dir + '/pazy/cases/'
23-
24-
def run_test(self, symmetry_condition, dynamic=False):
25-
self.case_name = 'pazy_uinf{:04g}_alpha{:04g}_symmetry_{}'.format(self.u_inf * 10, self.alpha * 10, str(int(symmetry_condition)))
17+
self.n_tsteps = 2
18+
19+
self.setup_test_folders('pazy')
20+
21+
def run_test(self, symmetry_condition, dynamic=False, gust_vanes=False, cs_deflection_file=None):
22+
self.case_name = 'pazy_uinf{:04g}_alpha{:04g}_symmetry_{}_gustvanes_{}'.format(self.u_inf * 10, self.alpha * 10, str(int(symmetry_condition)), int(gust_vanes))
2623

2724
gp.generate_pazy(self.u_inf, self.case_name, self.output_folder, self.cases_folder,
2825
alpha=self.alpha,
29-
M=self.M,
30-
N=self.N,
26+
M=self.num_chordwise_panels,
27+
N=self.num_spanwise_nodes,
3128
Msf=self.Msf,
3229
symmetry_condition=symmetry_condition,
3330
dynamic=dynamic,
34-
n_tsteps=self.n_tsteps)
35-
31+
n_tsteps=self.n_tsteps,
32+
gust_vanes=gust_vanes,
33+
cs_deflection_file=cs_deflection_file)
34+
3635
self.evaluate_output()
3736

3837
def evaluate_output(self):
39-
pass
40-
# node_number = self.N / 2 # wing tip node
41-
42-
# # Get results in A frame
43-
# tip_displacement = np.loadtxt(self.output_folder + '/' + self.case_name + '/WriteVariablesTime/struct_pos_node{:g}.dat'.format(node_number))
44-
# # current reference from Technion abstract
45-
# ref_displacement = 2.033291e-1 # m
46-
# print("delta z = ", tip_displacement[-1])
47-
# np.testing.assert_almost_equal(tip_displacement[-1], ref_displacement,
48-
# decimal=3,
49-
# err_msg='Wing tip displacement not within 3 decimal points of reference.',
50-
# verbose=True)
38+
pass
39+
40+
def setup_test_folders(self, subfolder):
41+
"""
42+
Set up the case and output directories for the current test,
43+
based on the location of the test file that calls this method.
44+
45+
Args:
46+
subfolder (str): Subfolder name like "pazy" or "gust_vanes"
47+
"""
48+
import inspect
49+
50+
caller_file = inspect.stack()[1].filename
51+
self.route_test_dir = os.path.abspath(os.path.dirname(caller_file))
52+
self.cases_folder = os.path.join(self.route_test_dir, subfolder, 'cases')
53+
self.output_folder = self.cases_folder
5154

5255
def tearDown(self):
5356
cases_folder = self.route_test_dir + '/pazy/cases/'
@@ -56,7 +59,7 @@ def tearDown(self):
5659
import shutil
5760
shutil.rmtree(cases_folder)
5861

59-
class TestPazyCoupledStatic(TestPazyCoupled):
62+
class TestPazyCoupledStatic(TestPazyCoupled, unittest.TestCase):
6063
"""
6164
Test Pazy wing static coupled case and compare against a benchmark result.
6265
@@ -70,16 +73,29 @@ def test_static_aoa(self):
7073
def test_static_aoa_symmetry(self):
7174
self.run_test(True)
7275

73-
def evaluate_output(self):
74-
node_number = self.N / 2 # wing tip node
75-
# Get results in A frame
76-
tip_displacement = np.loadtxt(self.output_folder + '/' + self.case_name + '/WriteVariablesTime/struct_pos_node{:g}.dat'.format(node_number))
77-
# current reference from Technion abstract
78-
ref_displacement = 2.033291e-1 # m
79-
np.testing.assert_almost_equal(tip_displacement[-1], ref_displacement,
80-
decimal=3,
81-
err_msg='Wing tip displacement not within 3 decimal points of reference.',
82-
verbose=True)
76+
def evaluate_output(self):
77+
self.assert_tip_displacement_matches_reference(
78+
node_index=self.num_spanwise_nodes / 2,
79+
ref_displacement=2.033291e-1
80+
)
81+
82+
def assert_tip_displacement_matches_reference(self, node_index: int, ref_displacement: float):
83+
"""
84+
Check if the tip node displacement matches the reference value.
85+
86+
Args:
87+
node_index (int): Node index along the span (e.g., N/2 for tip)
88+
ref_displacement (float): Reference displacement [m]
89+
"""
90+
file_path = os.path.join(self.output_folder, self.case_name,
91+
f'WriteVariablesTime/struct_pos_node{int(node_index)}.dat')
92+
tip_displacement = np.loadtxt(file_path)
93+
94+
np.testing.assert_almost_equal(
95+
tip_displacement[-1], ref_displacement, decimal=3,
96+
err_msg='Wing tip displacement not within 0.001 m of reference.',
97+
verbose=True
98+
)
8399

84100
if __name__ == '__main__':
85101
unittest.main()

0 commit comments

Comments
 (0)