diff --git a/aviary/mission/height_energy_problem_configurator.py b/aviary/mission/height_energy_problem_configurator.py index 5f393aaa4..7e29b40d8 100644 --- a/aviary/mission/height_energy_problem_configurator.py +++ b/aviary/mission/height_energy_problem_configurator.py @@ -394,7 +394,11 @@ def add_post_mission_systems(self, aviary_group): ) if aviary_group.post_mission_info['include_landing']: - self._add_landing_systems(aviary_group) + if 'aircraft:wing:area' in aviary_group.aviary_inputs: + self._add_landing_systems(aviary_group) + else: + print('Aircraft.Wing.AREA is not given. Set include_landing = False') + aviary_group.post_mission_info['include_landing'] = False aviary_group.add_subsystem( 'range_constraint', diff --git a/aviary/models/aircraft/blended_wing_body/bwb_detailed_FLOPS.csv b/aviary/models/aircraft/blended_wing_body/bwb_detailed_FLOPS.csv index c28fdf40c..105c4c780 100644 --- a/aviary/models/aircraft/blended_wing_body/bwb_detailed_FLOPS.csv +++ b/aviary/models/aircraft/blended_wing_body/bwb_detailed_FLOPS.csv @@ -152,7 +152,6 @@ aircraft:wing:var_sweep_mass_penalty,0.0,unitless aircraft:wing:wetted_area_scaler,1.0,unitless mission:constraints:max_mach,0.85,unitless mission:design:gross_mass,874099,lbm -mission:design:lift_coefficient,-1.0,unitless mission:design:range,7750,NM mission:design:thrust_takeoff_per_eng,0.25,lbf mission:landing:initial_velocity,140,ft/s @@ -164,6 +163,7 @@ mission:summary:fuel_flow_scaler,1.0,unitless settings:aerodynamics_method,FLOPS,unitless settings:equations_of_motion,height_energy,unitless settings:mass_method,FLOPS,unitless +settings:verbosity,1,unitless # Unconverted Values AERIN.FLLDG,11000 diff --git a/aviary/models/aircraft/blended_wing_body/bwb_simple_FLOPS.csv b/aviary/models/aircraft/blended_wing_body/bwb_simple_FLOPS.csv index 47e12e0f2..099e63e1c 100644 --- a/aviary/models/aircraft/blended_wing_body/bwb_simple_FLOPS.csv +++ b/aviary/models/aircraft/blended_wing_body/bwb_simple_FLOPS.csv @@ -151,7 +151,6 @@ aircraft:wing:var_sweep_mass_penalty,0.0,unitless aircraft:wing:wetted_area_scaler,1.0,unitless mission:constraints:max_mach,0.85,unitless mission:design:gross_mass,874099,lbm -mission:design:lift_coefficient,-1.0,unitless mission:design:range,7750,NM mission:design:thrust_takeoff_per_eng,0.25,lbf mission:landing:initial_velocity,140,ft/s @@ -163,6 +162,7 @@ mission:summary:fuel_flow_scaler,1.0,unitless settings:aerodynamics_method,FLOPS,unitless settings:equations_of_motion,height_energy,unitless settings:mass_method,FLOPS,unitless +settings:verbosity,1,unitless # Unconverted Values AERIN.FLLDG,11000 diff --git a/aviary/subsystems/aerodynamics/aerodynamics_builder.py b/aviary/subsystems/aerodynamics/aerodynamics_builder.py index e5167ffc9..0172d7a43 100644 --- a/aviary/subsystems/aerodynamics/aerodynamics_builder.py +++ b/aviary/subsystems/aerodynamics/aerodynamics_builder.py @@ -494,7 +494,15 @@ def get_parameters(self, aviary_inputs=None, **kwargs): params[Aircraft.Design.LIFT_DEPENDENT_DRAG_POLAR] = opts if method == 'computed': - for var in COMPUTED_CORE_INPUTS: + try: + design_type = aviary_inputs.get_val(Aircraft.Design.TYPE) + except: + design_type = AircraftTypes.TRANSPORT + if design_type is AircraftTypes.BLENDED_WING_BODY: + core_inputs_computed = COMPUTED_CORE_INPUTS_BWB + else: + core_inputs_computed = COMPUTED_CORE_INPUTS + for var in core_inputs_computed: meta = _MetaData[var] val = meta['default_value'] @@ -622,7 +630,10 @@ def get_parameters(self, aviary_inputs=None, **kwargs): 'tabular_cruise, low_speed, tabular_low_speed)' ) - design_type = aviary_inputs.get_val(Aircraft.Design.TYPE) + try: + design_type = aviary_inputs.get_val(Aircraft.Design.TYPE) + except: + design_type = AircraftTypes.TRANSPORT if design_type is AircraftTypes.BLENDED_WING_BODY: all_vars.add(Aircraft.Fuselage.LIFT_CURVE_SLOPE_MACH0) @@ -729,6 +740,37 @@ def report(self, prob, reports_folder, **kwargs): Mission.Design.MACH, ] +COMPUTED_CORE_INPUTS_BWB = [ + Aircraft.Design.BASE_AREA, + Aircraft.Design.LIFT_DEPENDENT_DRAG_COEFF_FACTOR, + Aircraft.Design.SUBSONIC_DRAG_COEFF_FACTOR, + Aircraft.Design.SUPERSONIC_DRAG_COEFF_FACTOR, + Aircraft.Design.ZERO_LIFT_DRAG_COEFF_FACTOR, + Aircraft.Fuselage.CHARACTERISTIC_LENGTH, + Aircraft.Fuselage.CROSS_SECTION, + Aircraft.Fuselage.DIAMETER_TO_WING_SPAN, + Aircraft.Fuselage.FINENESS, + Aircraft.Fuselage.LAMINAR_FLOW_LOWER, + Aircraft.Fuselage.LAMINAR_FLOW_UPPER, + Aircraft.Fuselage.LENGTH_TO_DIAMETER, + Aircraft.Fuselage.WETTED_AREA, + Aircraft.Wing.AREA, + Aircraft.Wing.ASPECT_RATIO, + Aircraft.Wing.CHARACTERISTIC_LENGTH, + Aircraft.Wing.FINENESS, + Aircraft.Wing.LAMINAR_FLOW_LOWER, + Aircraft.Wing.LAMINAR_FLOW_UPPER, + Aircraft.Wing.MAX_CAMBER_AT_70_SEMISPAN, + Aircraft.Wing.SPAN_EFFICIENCY_FACTOR, + Aircraft.Wing.SWEEP, + Aircraft.Wing.TAPER_RATIO, + Aircraft.Wing.THICKNESS_TO_CHORD, + Aircraft.Wing.WETTED_AREA, + # Mission.Summary.GROSS_MASS, + Mission.Design.LIFT_COEFFICIENT, + Mission.Design.MACH, +] + TABULAR_CORE_INPUTS = [ Aircraft.Wing.AREA, Aircraft.Design.SUBSONIC_DRAG_COEFF_FACTOR, diff --git a/aviary/subsystems/aerodynamics/flops_based/skin_friction.py b/aviary/subsystems/aerodynamics/flops_based/skin_friction.py index 502378cf3..0cf9fb870 100644 --- a/aviary/subsystems/aerodynamics/flops_based/skin_friction.py +++ b/aviary/subsystems/aerodynamics/flops_based/skin_friction.py @@ -1,6 +1,7 @@ import numpy as np import openmdao.api as om +from aviary.variable_info.enums import AircraftTypes from aviary.variable_info.functions import add_aviary_input, add_aviary_option from aviary.variable_info.variables import Aircraft, Dynamic @@ -36,17 +37,23 @@ def initialize(self): desc='The number of points at which the cross product is computed.', ) + add_aviary_option(self, Aircraft.Design.TYPE) add_aviary_option(self, Aircraft.Engine.NUM_ENGINES) add_aviary_option(self, Aircraft.Fuselage.NUM_FUSELAGES) add_aviary_option(self, Aircraft.VerticalTail.NUM_TAILS) def setup(self): nn = self.options['num_nodes'] + design_type = self.options[Aircraft.Design.TYPE] num_engines = self.options[Aircraft.Engine.NUM_ENGINES] num_fuselages = self.options[Aircraft.Fuselage.NUM_FUSELAGES] num_tails = self.options[Aircraft.VerticalTail.NUM_TAILS] - self.nc = nc = 2 + num_tails + num_fuselages + int(sum(num_engines)) + if design_type is AircraftTypes.BLENDED_WING_BODY: + # No horizontal tail for BWB + self.nc = nc = 1 + num_tails + num_fuselages + int(sum(num_engines)) + else: + self.nc = nc = 2 + num_tails + num_fuselages + int(sum(num_engines)) # Simulation inputs add_aviary_input(self, Dynamic.Atmosphere.TEMPERATURE, shape=nn, units='degR') diff --git a/aviary/subsystems/aerodynamics/flops_based/skin_friction_drag.py b/aviary/subsystems/aerodynamics/flops_based/skin_friction_drag.py index 2324ecee4..284cc12e4 100644 --- a/aviary/subsystems/aerodynamics/flops_based/skin_friction_drag.py +++ b/aviary/subsystems/aerodynamics/flops_based/skin_friction_drag.py @@ -1,6 +1,7 @@ import numpy as np import openmdao.api as om +from aviary.variable_info.enums import AircraftTypes from aviary.variable_info.functions import add_aviary_input, add_aviary_option, get_units from aviary.variable_info.variables import Aircraft @@ -33,6 +34,7 @@ def initialize(self): desc='The number of points at which the cross product is computed.', ) + add_aviary_option(self, Aircraft.Design.TYPE) add_aviary_option(self, Aircraft.Engine.NUM_ENGINES) add_aviary_option(self, Aircraft.Fuselage.NUM_FUSELAGES) add_aviary_option(self, Aircraft.VerticalTail.NUM_TAILS) @@ -47,12 +49,17 @@ def initialize(self): def setup(self): nn = self.options['num_nodes'] + design_type = self.options[Aircraft.Design.TYPE] nvtail = self.options[Aircraft.VerticalTail.NUM_TAILS] nfuse = self.options[Aircraft.Fuselage.NUM_FUSELAGES] num_engines = self.options[Aircraft.Engine.NUM_ENGINES] - self.nc = nc = 2 + nvtail + nfuse + int(sum(num_engines)) + if design_type is AircraftTypes.BLENDED_WING_BODY: + # No horizontal tail for BWB + self.nc = nc = 1 + nvtail + nfuse + int(sum(num_engines)) + else: + self.nc = nc = 2 + nvtail + nfuse + int(sum(num_engines)) # Computed by other components in drag group. self.add_input('skin_friction_coeff', np.zeros((nn, nc)), units='unitless') diff --git a/aviary/subsystems/geometry/flops_based/canard.py b/aviary/subsystems/geometry/flops_based/canard.py index 40095d929..46bee4a1f 100644 --- a/aviary/subsystems/geometry/flops_based/canard.py +++ b/aviary/subsystems/geometry/flops_based/canard.py @@ -12,6 +12,7 @@ class Canard(om.ExplicitComponent): """Calculate the wetted area of canard.""" + # TODO: what is it for? def initialize(self): self.options.declare( 'aviary_options', diff --git a/aviary/subsystems/geometry/flops_based/fuselage.py b/aviary/subsystems/geometry/flops_based/fuselage.py index 68de29cf1..ebfa5a8bb 100644 --- a/aviary/subsystems/geometry/flops_based/fuselage.py +++ b/aviary/subsystems/geometry/flops_based/fuselage.py @@ -167,7 +167,7 @@ def compute(self, inputs, outputs): if verbosity > Verbosity.BRIEF: raise UserWarning( 'Passenger compartment lenght is longer than recommended maximum' - ' length (of 190 ft). Suggest using detailed layout algorithm.' + ' length. Suggest using detailed layout algorithm.' ) outputs[Aircraft.Fuselage.PASSENGER_COMPARTMENT_LENGTH] = pax_compart_length @@ -480,8 +480,8 @@ def compute(self, inputs, outputs): # Enforce maximum number of bays num_bays_max = self.options[Aircraft.BWB.MAX_NUM_BAYS] - num_bays = int(0.5 + max_width.real / bay_width_max.real) - if num_bays > num_bays_max and num_bays_max > 0: + num_bays = int(0.5 + max_width / bay_width_max) + if num_bays.real > num_bays_max and num_bays_max > 0: num_bays = num_bays_max outputs[Aircraft.BWB.NUM_BAYS] = smooth_int_tanh(num_bays, mu=20.0) @@ -657,27 +657,30 @@ def compute(self, inputs, outputs): pax_compart_length = root_chord + tan_sweep * max_width / 2.0 # Enforce maximum number of bays - z = 0.5 + max_width / bay_width_max - z = z[0] - num_bays = int(z.real) - if num_bays > num_bays_max and num_bays_max > 0: + num_bays_tmp = 0.5 + max_width / bay_width_max + if num_bays_tmp[0].real > num_bays_max and num_bays_max > 0: num_bays = num_bays_max + else: + num_bays = int(num_bays_tmp[0].real) # Enforce maximum bay width bay_width = max_width / num_bays if bay_width > bay_width_max: bay_width = bay_width_max - num_bays = int(0.999 + max_width / bay_width) - if num_bays > num_bays_max and num_bays_max > 0: + num_bays_tmp = 0.999 + max_width / bay_width + if num_bays_tmp.real > num_bays_max and num_bays_max > 0: num_bays = num_bays_max max_width = bay_width_max * bay_width pax_compart_length = area_cabin / max_width + tan_sweep * max_width / 4.0 root_chord = pax_compart_length - tan_sweep * max_width / 2.0 + else: + num_bays = smooth_int_tanh(num_bays_tmp, mu=40.0) + # If number of bays has changed, recalculate cabin area length = pax_compart_length / rear_spar_percent_chord max_height = height_to_width * length - outputs[Aircraft.BWB.NUM_BAYS] = smooth_int_tanh(num_bays, mu=20.0) + outputs[Aircraft.BWB.NUM_BAYS] = num_bays outputs[Aircraft.Fuselage.LENGTH] = length outputs[Aircraft.Fuselage.PASSENGER_COMPARTMENT_LENGTH] = pax_compart_length diff --git a/aviary/subsystems/test/test_flops_based_premission.py b/aviary/subsystems/test/test_flops_based_premission.py index 648e58885..04886134d 100644 --- a/aviary/subsystems/test/test_flops_based_premission.py +++ b/aviary/subsystems/test/test_flops_based_premission.py @@ -503,7 +503,7 @@ def test_case_geom(self): assert_near_equal(prob[Aircraft.Wing.ROOT_CHORD], 63.96, tol) assert_near_equal(prob[Aircraft.Fuselage.CABIN_AREA], 5173.187202504683, tol) assert_near_equal(prob[Aircraft.Fuselage.MAX_HEIGHT], 15.125, tol) - assert_near_equal(prob[Aircraft.BWB.NUM_BAYS], 5.0, tol) + assert_near_equal(prob[Aircraft.BWB.NUM_BAYS], 5.0, 1e-4) # BWBFuselagePrelim assert_near_equal(prob[Aircraft.Fuselage.REF_DIAMETER], 39.8525, tol) assert_near_equal(prob[Aircraft.Fuselage.PLANFORM_AREA], 7390.267432149546, tol) diff --git a/aviary/utils/math.py b/aviary/utils/math.py index 5c01de748..378184e98 100644 --- a/aviary/utils/math.py +++ b/aviary/utils/math.py @@ -215,7 +215,7 @@ def smooth_int_tanh(x, mu=10.0): """ Smooth approximation of int(x) using tanh. """ - f = np.floor(x) + f = np.floor(x.real) + x.imag * 1j frac = x - f t = np.tanh(mu * (frac - 0.5)) s = 0.5 * (t + 1) @@ -228,7 +228,7 @@ def d_smooth_int_tanh(x, mu=10.0): Smooth approximation of int(x) using tanh. Returns (y, dy_dx). """ - f = np.floor(x) + f = np.floor(x) + x.imag * 1j frac = x - f t = np.tanh(mu * (frac - 0.5)) dy_dx = 0.5 * mu * (1 - t**2) diff --git a/aviary/validation_cases/benchmark_tests/test_bwb_FwFm.py b/aviary/validation_cases/benchmark_tests/test_bwb_FwFm.py new file mode 100644 index 000000000..6bfff72c0 --- /dev/null +++ b/aviary/validation_cases/benchmark_tests/test_bwb_FwFm.py @@ -0,0 +1,144 @@ +import unittest +from copy import deepcopy + +from openmdao.core.problem import _clear_problem_names +from openmdao.utils.assert_utils import assert_near_equal +from openmdao.utils.testing_utils import require_pyoptsparse, use_tempdirs + +from aviary.models.aircraft.large_turboprop_freighter.phase_info import ( + energy_phase_info as phase_info, +) + +from aviary.interface.methods_for_level1 import run_aviary +from aviary.variable_info.variables import Mission + + +# @use_tempdirs +class BWBProblemPhaseTestCase(unittest.TestCase): + """ + Test the setup and run of a BWB aircraft using FLOPS mass and aero method + and HEIGHT_ENERGY mission method. Expected outputs based on + 'models/aircraft/blended_wing_body/bwb_simple_FLOPS.csv' model. + """ + + def setUp(self): + _clear_problem_names() # need to reset these to simulate separate runs + + @require_pyoptsparse(optimizer='SNOPT') + def test_bench_bwb_FwFm_SNOPT(self): + local_phase_info = deepcopy(phase_info) + prob = run_aviary( + 'models/aircraft/blended_wing_body/bwb_simple_FLOPS.csv', + local_phase_info, + optimizer='SNOPT', + verbosity=1, + max_iter=60, + ) + # prob.model.list_vars(units=True, print_arrays=True) + # prob.list_indep_vars() + # prob.list_problem_vars() + # prob.model.list_outputs() + + rtol = 1e-3 + + # There are no truth values for these. + assert_near_equal( + prob.get_val(Mission.Design.GROSS_MASS, units='lbm'), + 139803.667415, + tolerance=rtol, + ) + + assert_near_equal( + prob.get_val(Mission.Summary.OPERATING_MASS, units='lbm'), + 79873.05255347, + tolerance=rtol, + ) + + assert_near_equal( + prob.get_val(Mission.Summary.TOTAL_FUEL_MASS, units='lbm'), + 26180.61486153, + tolerance=rtol, + ) + + assert_near_equal(prob.get_val(Mission.Summary.RANGE, units='NM'), 3500.0, tolerance=rtol) + + assert_near_equal( + prob.get_val(Mission.Landing.GROUND_DISTANCE, units='ft'), + 2216.0066613, + tolerance=rtol, + ) + + assert_near_equal( + prob.get_val(Mission.Landing.TOUCHDOWN_MASS, units='lbm'), + 116003.31044998, + tolerance=rtol, + ) + + +class ProblemPhaseTestCase(unittest.TestCase): + """ + Test the setup and run of a test aircraft using FLOPS mass and aero method + and HEIGHT_ENERGY mission method. Expected outputs based on + 'models/aircraft/test_aircraft/aircraft_for_bench_FwFm.csv' model. + """ + + def setUp(self): + _clear_problem_names() # need to reset these to simulate separate runs + + @require_pyoptsparse(optimizer='SNOPT') + def test_bench_FwFm_SNOPT(self): + local_phase_info = deepcopy(phase_info) + prob = run_aviary( + 'models/aircraft/test_aircraft/aircraft_for_bench_FwFm.csv', + local_phase_info, + optimizer='SNOPT', + verbosity=0, + max_iter=60, + ) + # prob.list_indep_vars() + # prob.list_problem_vars() + # prob.model.list_outputs() + + # self.assertTrue(prob.result.success) + + rtol = 1e-3 + + # There are no truth values for these. + assert_near_equal( + prob.get_val(Mission.Design.GROSS_MASS, units='lbm'), + 169804.16225263, + tolerance=rtol, + ) + + assert_near_equal( + prob.get_val(Mission.Summary.OPERATING_MASS, units='lbm'), + 97096.89284117, + tolerance=rtol, + ) + + assert_near_equal( + prob.get_val(Mission.Summary.TOTAL_FUEL_MASS, units='lbm'), + 34682.26941131, + tolerance=rtol, + ) + + assert_near_equal(prob.get_val(Mission.Summary.RANGE, units='NM'), 2020.0, tolerance=rtol) + + assert_near_equal( + prob.get_val(Mission.Landing.GROUND_DISTANCE, units='ft'), + 2216.0066613, + tolerance=rtol, + ) + + assert_near_equal( + prob.get_val(Mission.Landing.TOUCHDOWN_MASS, units='lbm'), + 116003.31044998, + tolerance=rtol, + ) + + +if __name__ == '__main__': + # unittest.main() + test = BWBProblemPhaseTestCase() + test.setUp() + test.test_bench_bwb_FwFm_SNOPT()