From 35649ca8b17a31ea49a643613c244878c0da4f63 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Fri, 23 Jan 2026 12:49:20 -0500 Subject: [PATCH 01/21] add experiment mission tests --- .../test_bwb_Experiment_FwFm_1.py | 75 ++++++ .../test_bwb_Experiment_FwFm_2.py | 223 ++++++++++++++++++ 2 files changed, 298 insertions(+) create mode 100644 aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_1.py create mode 100644 aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_2.py diff --git a/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_1.py b/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_1.py new file mode 100644 index 000000000..d6423866d --- /dev/null +++ b/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_1.py @@ -0,0 +1,75 @@ +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 ProblemPhaseTestCase(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=0, + max_iter=60, + ) + + 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.Landing.GROUND_DISTANCE, units='ft'), + 2216.0066613, + 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.TOUCHDOWN_MASS, units='lbm'), + 116003.31044998, + tolerance=rtol, + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_2.py b/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_2.py new file mode 100644 index 000000000..6aa68c53e --- /dev/null +++ b/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_2.py @@ -0,0 +1,223 @@ +import unittest + +import numpy as np +from openmdao.core.problem import _clear_problem_names +from openmdao.utils.mpi import MPI +from openmdao.utils.testing_utils import require_pyoptsparse, use_tempdirs + +from aviary.api import Mission +from aviary.interface.methods_for_level1 import run_aviary +from aviary.validation_cases.benchmark_utils import compare_against_expected_values + +try: + from openmdao.vectors.petsc_vector import PETScVector +except ImportError: + PETScVector = None + + +@use_tempdirs +class ProblemPhaseTestCase(unittest.TestCase): + """ + Setup 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): + expected_dict = {} + + # block auto-formatting of tables + # fmt: off + expected_dict['times'] = np.array( + [ + [120.],[163.76268404], [224.14625594], [243.25744998], [243.25744998], + [336.40804126], [464.93684491], [505.61577182], [505.61577182], [626.46953842], + [793.22306972], [845.99999224], [845.99999224], [966.85375884], [1133.60729014], + [1186.38421266], [1186.38421266], [1279.53480393], [1408.06360758], [1448.74253449], + [1448.74253449], [1492.50521853], [1552.88879042], [1571.99998447], [1571.99998447], + [10224.87383109], [22164.07366288], [25942.78958866], [25942.78958866], + [26009.11685074], [26100.63493484], [26129.60009555], [26129.60009555], + [26265.05921709], [26451.96515722], [26511.12024823], [26511.12024823], + [26672.16774132], [26894.38041154], [26964.7099619], [26964.7099619], + [27100.16908344], [27287.07502357], [27346.23011458], [27346.23011458], + [27412.55737667], [27504.07546076], [27533.04062147] + ] + ) + + expected_dict['altitudes'] = np.array( + [ + [10.668], [0.], [1001.70617719], [1429.27176545], [1429.27176545], [3413.27102762], + [5642.3831233], [6169.75300447], [6169.75300447], [7399.140983], [8514.78661356], + [8803.21405264], [8803.21405264], [9373.68426297], [10020.99237958], + [10196.42552457], [10196.42552457],[10451.72258036], [10652.38789684], [10668.], + [10668.], [10660.42246376], [10656.16585151], [10668.], [10668.], [10668.], + [10668.], [10668.], [10668.], [10668.], [10142.11478951], [9922.15743555], + [9922.15743555], [8891.66886638], [7502.1861348], [7069.1900852], [7069.1900852], + [5896.44637998], [4264.29354306], [3737.8471594], [3737.8471594], [2702.15624637], + [1248.18960736], [793.03526817], [793.03526817], [345.06939295], [10.668], [10.668] + ] + ) + + expected_dict['masses'] = np.array( + [ + [79303.30184763], [79221.39668215], [79075.19453181], [79028.6003426], + [79028.6003426], [78828.82221909], [78613.60466821], [78557.84739563], + [78557.84739563], [78411.06578989], [78238.0916773], [78186.75440341], + [78186.75440341], [78077.23953313], [77938.37965175], [77896.59718975], + [77896.59718975], [77825.81832958], [77732.75016916], [77704.11629998], + [77704.11629998], [77673.32196072], [77630.75735319], [77617.25716885], + [77617.25716885], [72178.78521803], [65072.41395049], [62903.84179505], + [62903.84179505], [62896.27636813], [62888.3612195], [62885.93748938], + [62885.93748938], [62874.48788511], [62857.70600096], [62852.13740881], + [62852.13740881], [62835.97069937], [62810.37776063], [62801.1924259], + [62801.1924259], [62781.32471014], [62748.91017128], [62737.32520462], + [62737.32520462], [62723.59895849], [62703.94977811], [62697.71513264] + ] + ) + + expected_dict['ranges'] = np.array( + [ + [1452.84514351], [6093.51223933], [15820.03029119], [19123.61258676], + [19123.61258676], [36374.65336952], [61265.3984918], [69106.49687132], + [69106.49687132], [92828.04820577], [126824.13801408], [138011.02420534], + [138011.02420534], [164027.18014424], [200524.66550565], [212113.49107256], + [212113.49107256], [232622.50720766], [261189.53466522], [270353.40501262], + [270353.40501262], [280350.48472685], [294356.27080588], [298832.61221641], + [298832.61221641], [2325837.11255987], [5122689.60556392], [6007883.85695889], + [6007883.85695889], [6022237.43153219], [6039575.06318219], [6044873.89820027], + [6044873.89820027], [6068553.1921364], [6099290.23732297], [6108673.67260778], + [6108673.67260778], [6133535.09572671], [6166722.19545137], [6177077.72115854], + [6177077.72115854], [6197011.1330154], [6224357.63792683], [6232920.45309764], + [6232920.45309764], [6242332.46480721], [6254144.50957549], [6257352.4] + ] + ) + + expected_dict['velocities'] = np.array( + [ + [69.30879167], [137.49019035], [174.54683946], [179.28863383], [179.28863383], + [191.76748742], [194.33322917], [194.52960387], [194.52960387], [199.01184603], + [209.81696863], [212.86546124], [212.86546124], [217.37467051], [219.67762167], + [219.97194272], [219.97194272], [220.67963782], [224.38113484], [226.77184704], + [226.77184704], [230.01128033], [233.72454583], [234.25795132], [234.25795132], + [234.25795132], [234.25795132], [234.25795132], [234.25795132], [201.23881], + [182.84158341], [180.10650108], [180.10650108], [169.77497514], [159.59034446], + [157.09907013], [157.09907013], [151.659491], [147.52098882], [147.07683999], + [147.07683999], [147.05392009], [145.31556891], [143.47446173], [143.47446173], + [138.99109332], [116.22447082], [102.07377559] + ] + ) + # fmt: on + + self.expected_dict = expected_dict + + phase_info = { + 'pre_mission': {'include_takeoff': True, 'optimize_mass': True}, + 'climb': { + 'subsystem_options': {'aerodynamics': {'method': 'computed'}}, + 'user_options': { + 'num_segments': 6, + 'order': 3, + 'mach_bounds': ((0.1, 0.8), 'unitless'), + 'mach_optimize': True, + 'altitude_bounds': ((0.0, 35000.0), 'ft'), + 'altitude_optimize': True, + 'throttle_enforcement': 'path_constraint', + 'mass_ref': (200000, 'lbm'), + 'time_initial': (0.0, 'min'), + 'time_duration_bounds': ((20.0, 60.0), 'min'), + 'no_descent': True, + }, + 'initial_guesses': { + 'time': ([0, 40.0], 'min'), + 'altitude': ([35, 35000.0], 'ft'), + 'mach': ([0.3, 0.79], 'unitless'), + }, + }, + 'cruise': { + 'subsystem_options': {'aerodynamics': {'method': 'computed'}}, + 'user_options': { + 'num_segments': 1, + 'order': 3, + 'mach_initial': (0.79, 'unitless'), + 'mach_bounds': ((0.79, 0.79), 'unitless'), + 'mach_optimize': True, + 'mach_polynomial_order': 1, + 'altitude_initial': (35000.0, 'ft'), + 'altitude_bounds': ((35000.0, 35000.0), 'ft'), + 'altitude_optimize': True, + 'altitude_polynomial_order': 1, + 'throttle_enforcement': 'boundary_constraint', + 'mass_ref': (200000, 'lbm'), + 'time_initial_bounds': ((20.0, 60.0), 'min'), + 'time_duration_bounds': ((60.0, 720.0), 'min'), + }, + 'initial_guesses': { + 'time': ([40, 200], 'min'), + 'altitude': ([35000, 35000.0], 'ft'), + 'mach': ([0.79, 0.79], 'unitless'), + }, + }, + 'descent': { + 'subsystem_options': {'aerodynamics': {'method': 'computed'}}, + 'user_options': { + 'num_segments': 5, + 'order': 3, + 'mach_initial': (0.79, 'unitless'), + 'mach_final': (0.3, 'unitless'), + 'mach_bounds': ((0.2, 0.8), 'unitless'), + 'mach_optimize': True, + 'altitude_initial': (35000.0, 'ft'), + 'altitude_final': (35.0, 'ft'), + 'altitude_bounds': ((0.0, 35000.0), 'ft'), + 'altitude_optimize': True, + 'throttle_enforcement': 'path_constraint', + 'mass_ref': (200000, 'lbm'), + 'distance_ref': (3375, 'nmi'), + 'time_initial_bounds': ((80.0, 780.0), 'min'), + 'time_duration_bounds': ((5.0, 45.0), 'min'), + 'no_climb': True, + }, + 'initial_guesses': { + 'time': ([240, 30], 'min'), + }, + }, + 'post_mission': { + 'include_landing': True, + 'constrain_range': True, + 'target_range': (3375.0, 'nmi'), + }, + } + + self.phase_info = phase_info + + _clear_problem_names() # need to reset these to simulate separate runs + + +class TestBenchFwFmSerial(ProblemPhaseTestCase): + """Run the model in serial that is setup in ProblemPhaseTestCase class.""" + + @require_pyoptsparse(optimizer='SNOPT') + def test_bench_FwFm_SNOPT(self): + prob = run_aviary( + 'models/aircraft/blended_wing_body/bwb_simple_FLOPS.csv', + self.phase_info, + verbosity=1, + max_iter=50, + optimizer='SNOPT', + ) + + # self.assertTrue(prob.result.success) + compare_against_expected_values(prob, self.expected_dict) + + # This is one of the few places we test Height Energy + simple takeoff. + overall_fuel = prob.get_val(Mission.Summary.TOTAL_FUEL_MASS) + + # Making sure we include the fuel mass consumed in take-off and taxi. + self.assertGreater(overall_fuel, 40000.0) + + +if __name__ == '__main__': + # unittest.main() + test = TestBenchFwFmSerial() + test.setUp() + test.test_bench_FwFm_SNOPT() From 70d18ef39e3ed81040035e5b13d639e091e92d98 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Fri, 23 Jan 2026 18:57:24 -0500 Subject: [PATCH 02/21] add settings:verbosity to BWB csv files --- aviary/models/aircraft/blended_wing_body/bwb_detailed_FLOPS.csv | 1 + aviary/models/aircraft/blended_wing_body/bwb_simple_FLOPS.csv | 1 + 2 files changed, 2 insertions(+) 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..e91e3c86a 100644 --- a/aviary/models/aircraft/blended_wing_body/bwb_detailed_FLOPS.csv +++ b/aviary/models/aircraft/blended_wing_body/bwb_detailed_FLOPS.csv @@ -164,6 +164,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..f44c184c6 100644 --- a/aviary/models/aircraft/blended_wing_body/bwb_simple_FLOPS.csv +++ b/aviary/models/aircraft/blended_wing_body/bwb_simple_FLOPS.csv @@ -163,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 From 3018e3eabccb4183292b31efd8f4230639407588 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Fri, 23 Jan 2026 18:58:48 -0500 Subject: [PATCH 03/21] work in progress --- .../benchmark_tests/test_bwb_Experiment_FwFm_1.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_1.py b/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_1.py index d6423866d..a8946ff13 100644 --- a/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_1.py +++ b/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_1.py @@ -13,7 +13,7 @@ from aviary.variable_info.variables import Mission -@use_tempdirs +# @use_tempdirs class ProblemPhaseTestCase(unittest.TestCase): """ Test the setup and run of a BWB aircraft using FLOPS mass and aero method @@ -34,6 +34,9 @@ def test_bench_bwb_FwFm_SNOPT(self): verbosity=0, max_iter=60, ) + # prob.list_indep_vars() + # prob.list_problem_vars() + prob.model.list_outputs() rtol = 1e-3 From 61c1d6ddeb741099d74ea91898569f5deb7bd987 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Fri, 23 Jan 2026 19:00:16 -0500 Subject: [PATCH 04/21] check divided by zero in ground_effect.py --- .../aerodynamics/flops_based/ground_effect.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/aviary/subsystems/aerodynamics/flops_based/ground_effect.py b/aviary/subsystems/aerodynamics/flops_based/ground_effect.py index 5eca2ce12..4b8c04d4f 100644 --- a/aviary/subsystems/aerodynamics/flops_based/ground_effect.py +++ b/aviary/subsystems/aerodynamics/flops_based/ground_effect.py @@ -11,8 +11,9 @@ import numpy as np import openmdao.api as om -from aviary.variable_info.functions import add_aviary_input -from aviary.variable_info.variables import Aircraft, Dynamic +from aviary.variable_info.enums import Verbosity +from aviary.variable_info.functions import add_aviary_input, add_aviary_option +from aviary.variable_info.variables import Aircraft, Dynamic, Settings class GroundEffect(om.ExplicitComponent): @@ -29,6 +30,7 @@ class GroundEffect(om.ExplicitComponent): def initialize(self): options = self.options + add_aviary_option(self, Settings.VERBOSITY) options.declare('num_nodes', default=1, types=int, lower=0) options.declare( @@ -138,6 +140,7 @@ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): options = self.options ground_altitude = options['ground_altitude'] + verbosity = self.options[Settings.VERBOSITY] angle_of_attack = inputs[Dynamic.Vehicle.ANGLE_OF_ATTACK] altitude = inputs[Dynamic.Mission.ALTITUDE] @@ -148,6 +151,9 @@ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): aspect_ratio = inputs[Aircraft.Wing.ASPECT_RATIO] height = inputs[Aircraft.Wing.HEIGHT] span = inputs[Aircraft.Wing.SPAN] + if span <= 0.0: + if verbosity > Verbosity.BRIEF: + raise UserWarning('Aircraft.Wing.SPAN is not positive.') ground_effect_state = ((altitude - ground_altitude) + height) / span height_factor = np.ones_like(ground_effect_state) @@ -164,6 +170,9 @@ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): + 4.0 * ground_effect_state * aspect_ratio_term * np.sqrt(ground_effect_term0) ) + if lift_coeff_factor_denom <= 0.0: + if verbosity > Verbosity.BRIEF: + raise UserWarning('lift_coeff_factor_denom is not positive.') lift_coeff_factor = 1.0 + height_factor / lift_coeff_factor_denom lift_coefficient = base_lift_coefficient * lift_coeff_factor @@ -175,6 +184,9 @@ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): 4.0 * ground_effect_state * np.sqrt(ground_effect_term1) + ground_effect_term1 ) + if drag_coeff_factor_denom <= 0.0: + if verbosity > Verbosity.BRIEF: + raise UserWarning('drag_coeff_factor_denom is not positive.') drag_coeff_factor = 1.0 - height_factor / drag_coeff_factor_denom drag_coefficient = ( From eead582ed2fc8368da40e9a6312df6a7ebfeda89 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Fri, 23 Jan 2026 19:01:49 -0500 Subject: [PATCH 05/21] add COMPUTED_CORE_INPUTS_BWB --- .../aerodynamics/aerodynamics_builder.py | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/aviary/subsystems/aerodynamics/aerodynamics_builder.py b/aviary/subsystems/aerodynamics/aerodynamics_builder.py index e5167ffc9..69007339d 100644 --- a/aviary/subsystems/aerodynamics/aerodynamics_builder.py +++ b/aviary/subsystems/aerodynamics/aerodynamics_builder.py @@ -494,7 +494,12 @@ 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: + design_type = aviary_inputs.get_val(Aircraft.Design.TYPE) + 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'] @@ -729,6 +734,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, From 535929030b6876a34b26d7af8fbe09dee30a99f6 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Fri, 23 Jan 2026 19:02:56 -0500 Subject: [PATCH 06/21] deal with BWB where there is no horizontal tails --- .../aerodynamics/flops_based/skin_friction.py | 10 +++++++++- .../aerodynamics/flops_based/skin_friction_drag.py | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/aviary/subsystems/aerodynamics/flops_based/skin_friction.py b/aviary/subsystems/aerodynamics/flops_based/skin_friction.py index 502378cf3..e226d161b 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,24 @@ 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 + nc = 1 + num_tails + num_fuselages + int(sum(num_engines)) + else: + nc = 2 + num_tails + num_fuselages + int(sum(num_engines)) + self.nc = nc # 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..e30ada57f 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,18 @@ 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 + nc = 1 + nvtail + nfuse + int(sum(num_engines)) + else: + nc = 2 + nvtail + nfuse + int(sum(num_engines)) + self.nc = nc # Computed by other components in drag group. self.add_input('skin_friction_coeff', np.zeros((nn, nc)), units='unitless') From 4ec92d961e2f085d1eb6b30a801ab9e2028cdbc0 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Wed, 28 Jan 2026 13:59:00 -0500 Subject: [PATCH 07/21] post mission require aircraft:wing:area. If it is not available, do not do landing --- aviary/mission/height_energy_problem_configurator.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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', From 67001d8de53db7e40781b2081d431ac28a415d64 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Wed, 28 Jan 2026 13:59:30 -0500 Subject: [PATCH 08/21] work in progress. --- .../test_bwb_Experiment_FwFm_1.py | 74 ++++++++++++++++++- .../test_bwb_Experiment_FwFm_2.py | 37 +++++++++- 2 files changed, 103 insertions(+), 8 deletions(-) diff --git a/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_1.py b/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_1.py index a8946ff13..ea2a65ab5 100644 --- a/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_1.py +++ b/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_1.py @@ -14,7 +14,7 @@ # @use_tempdirs -class ProblemPhaseTestCase(unittest.TestCase): +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 @@ -34,9 +34,10 @@ def test_bench_bwb_FwFm_SNOPT(self): verbosity=0, max_iter=60, ) + # prob.model.list_vars(units=True, print_arrays=True) # prob.list_indep_vars() # prob.list_problem_vars() - prob.model.list_outputs() + # prob.model.list_outputs() rtol = 1e-3 @@ -59,13 +60,75 @@ def test_bench_bwb_FwFm_SNOPT(self): 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.Summary.RANGE, units='NM'), 3500.0, 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'), @@ -75,4 +138,7 @@ def test_bench_bwb_FwFm_SNOPT(self): if __name__ == '__main__': - unittest.main() + # unittest.main() + test = BWBProblemPhaseTestCase() + test.setUp() + test.test_bench_bwb_FwFm_SNOPT() diff --git a/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_2.py b/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_2.py index 6aa68c53e..f24972bfb 100644 --- a/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_2.py +++ b/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_2.py @@ -193,13 +193,36 @@ def setUp(self): _clear_problem_names() # need to reset these to simulate separate runs +class TestBWBFwFmSerial(ProblemPhaseTestCase): + """Run the model in serial that is setup in ProblemPhaseTestCase class.""" + + @require_pyoptsparse(optimizer='SNOPT') + def test_bwb_FwFm_SNOPT(self): + prob = run_aviary( + 'models/aircraft/blended_wing_body/bwb_simple_FLOPS.csv', + self.phase_info, + verbosity=1, + max_iter=50, + optimizer='SNOPT', + ) + + # self.assertTrue(prob.result.success) + compare_against_expected_values(prob, self.expected_dict) + + # This is one of the few places we test Height Energy + simple takeoff. + overall_fuel = prob.get_val(Mission.Summary.TOTAL_FUEL_MASS) + + # Making sure we include the fuel mass consumed in take-off and taxi. + self.assertGreater(overall_fuel, 40000.0) + + class TestBenchFwFmSerial(ProblemPhaseTestCase): """Run the model in serial that is setup in ProblemPhaseTestCase class.""" @require_pyoptsparse(optimizer='SNOPT') def test_bench_FwFm_SNOPT(self): prob = run_aviary( - 'models/aircraft/blended_wing_body/bwb_simple_FLOPS.csv', + 'models/aircraft/test_aircraft/aircraft_for_bench_FwFm.csv', self.phase_info, verbosity=1, max_iter=50, @@ -218,6 +241,12 @@ def test_bench_FwFm_SNOPT(self): if __name__ == '__main__': # unittest.main() - test = TestBenchFwFmSerial() - test.setUp() - test.test_bench_FwFm_SNOPT() + test_bwb = True + if test_bwb: + test = TestBWBFwFmSerial() + test.setUp() + test.test_bwb_FwFm_SNOPT() + else: + test = TestBenchFwFmSerial() + test.setUp() + test.test_bench_FwFm_SNOPT() From ec1b682f0ffb0ad47f6ea1059698ebbb1b4ca229 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Wed, 28 Jan 2026 14:02:04 -0500 Subject: [PATCH 09/21] if Aircraft.Design.TYPE is not in input, assume default AircraftTypes.TRANSPORT --- aviary/subsystems/aerodynamics/aerodynamics_builder.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/aviary/subsystems/aerodynamics/aerodynamics_builder.py b/aviary/subsystems/aerodynamics/aerodynamics_builder.py index 69007339d..0172d7a43 100644 --- a/aviary/subsystems/aerodynamics/aerodynamics_builder.py +++ b/aviary/subsystems/aerodynamics/aerodynamics_builder.py @@ -494,7 +494,10 @@ def get_parameters(self, aviary_inputs=None, **kwargs): params[Aircraft.Design.LIFT_DEPENDENT_DRAG_POLAR] = opts if method == 'computed': - 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: core_inputs_computed = COMPUTED_CORE_INPUTS_BWB else: @@ -627,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) From 840f3cfa1f776a90a35face19b5706ad3decead2 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Wed, 28 Jan 2026 14:02:53 -0500 Subject: [PATCH 10/21] add TODO. It is a question --- aviary/subsystems/geometry/flops_based/canard.py | 1 + 1 file changed, 1 insertion(+) 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', From 66925c2d6a73acd9392794c0ec5859dd41d289fc Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Wed, 28 Jan 2026 18:05:10 -0500 Subject: [PATCH 11/21] remove mission:design:lift_coefficient from csv file --- aviary/models/aircraft/blended_wing_body/bwb_detailed_FLOPS.csv | 1 - aviary/models/aircraft/blended_wing_body/bwb_simple_FLOPS.csv | 1 - 2 files changed, 2 deletions(-) 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 e91e3c86a..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 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 f44c184c6..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 From 88f22b73e6b85d48c4a2428941b9abc10781d089 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Wed, 28 Jan 2026 18:05:40 -0500 Subject: [PATCH 12/21] work in progress --- .../benchmark_tests/test_bwb_Experiment_FwFm_1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_1.py b/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_1.py index ea2a65ab5..6bfff72c0 100644 --- a/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_1.py +++ b/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_1.py @@ -31,7 +31,7 @@ def test_bench_bwb_FwFm_SNOPT(self): 'models/aircraft/blended_wing_body/bwb_simple_FLOPS.csv', local_phase_info, optimizer='SNOPT', - verbosity=0, + verbosity=1, max_iter=60, ) # prob.model.list_vars(units=True, print_arrays=True) From d87758e6cc949da2c51247f74c7f04d3462aa86e Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Wed, 28 Jan 2026 19:00:39 -0500 Subject: [PATCH 13/21] smooth out int function --- aviary/subsystems/geometry/flops_based/fuselage.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aviary/subsystems/geometry/flops_based/fuselage.py b/aviary/subsystems/geometry/flops_based/fuselage.py index 68de29cf1..199678dda 100644 --- a/aviary/subsystems/geometry/flops_based/fuselage.py +++ b/aviary/subsystems/geometry/flops_based/fuselage.py @@ -480,7 +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) + + num_bays = smooth_int_tanh(0.5 + max_width / bay_width_max, mu=20.0) if num_bays > 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) @@ -659,7 +660,7 @@ def compute(self, inputs, outputs): # Enforce maximum number of bays z = 0.5 + max_width / bay_width_max z = z[0] - num_bays = int(z.real) + num_bays = smooth_int_tanh(z, mu=20.0) if num_bays > num_bays_max and num_bays_max > 0: num_bays = num_bays_max From 59de74b9f87636959e2e1dacca00a36adaa05eea Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Wed, 28 Jan 2026 22:48:04 -0500 Subject: [PATCH 14/21] roll back --- aviary/subsystems/geometry/flops_based/fuselage.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/aviary/subsystems/geometry/flops_based/fuselage.py b/aviary/subsystems/geometry/flops_based/fuselage.py index 199678dda..c5653a2d0 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,7 @@ def compute(self, inputs, outputs): # Enforce maximum number of bays num_bays_max = self.options[Aircraft.BWB.MAX_NUM_BAYS] - - num_bays = smooth_int_tanh(0.5 + max_width / bay_width_max, mu=20.0) + 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 = num_bays_max outputs[Aircraft.BWB.NUM_BAYS] = smooth_int_tanh(num_bays, mu=20.0) @@ -660,7 +659,7 @@ def compute(self, inputs, outputs): # Enforce maximum number of bays z = 0.5 + max_width / bay_width_max z = z[0] - num_bays = smooth_int_tanh(z, mu=20.0) + num_bays = int(z.real) if num_bays > num_bays_max and num_bays_max > 0: num_bays = num_bays_max From 849f7ad8d3bc82a83c60c568b66b2d04951b6b23 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Thu, 29 Jan 2026 13:30:18 -0500 Subject: [PATCH 15/21] lower tolerance for Aircraft.BWB.NUM_BAYS --- aviary/subsystems/test/test_flops_based_premission.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From dedfd72d6f9787337564233650dd2a81bbaa028e Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Thu, 29 Jan 2026 14:08:38 -0500 Subject: [PATCH 16/21] modify the computation of num_bays --- .../geometry/flops_based/fuselage.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/aviary/subsystems/geometry/flops_based/fuselage.py b/aviary/subsystems/geometry/flops_based/fuselage.py index c5653a2d0..ebfa5a8bb 100644 --- a/aviary/subsystems/geometry/flops_based/fuselage.py +++ b/aviary/subsystems/geometry/flops_based/fuselage.py @@ -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 From 858c4702c2bba22e032c480922827043186125f5 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Thu, 29 Jan 2026 14:09:33 -0500 Subject: [PATCH 17/21] modify function smooth_int_tanh() --- aviary/utils/math.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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) From 4387402db5a0bcabd0b3f41231b0ca0f213093c3 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Thu, 29 Jan 2026 14:10:52 -0500 Subject: [PATCH 18/21] remove test_bwb_Experiment_FwFm_2.py --- .../test_bwb_Experiment_FwFm_2.py | 252 ------------------ 1 file changed, 252 deletions(-) delete mode 100644 aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_2.py diff --git a/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_2.py b/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_2.py deleted file mode 100644 index f24972bfb..000000000 --- a/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_2.py +++ /dev/null @@ -1,252 +0,0 @@ -import unittest - -import numpy as np -from openmdao.core.problem import _clear_problem_names -from openmdao.utils.mpi import MPI -from openmdao.utils.testing_utils import require_pyoptsparse, use_tempdirs - -from aviary.api import Mission -from aviary.interface.methods_for_level1 import run_aviary -from aviary.validation_cases.benchmark_utils import compare_against_expected_values - -try: - from openmdao.vectors.petsc_vector import PETScVector -except ImportError: - PETScVector = None - - -@use_tempdirs -class ProblemPhaseTestCase(unittest.TestCase): - """ - Setup 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): - expected_dict = {} - - # block auto-formatting of tables - # fmt: off - expected_dict['times'] = np.array( - [ - [120.],[163.76268404], [224.14625594], [243.25744998], [243.25744998], - [336.40804126], [464.93684491], [505.61577182], [505.61577182], [626.46953842], - [793.22306972], [845.99999224], [845.99999224], [966.85375884], [1133.60729014], - [1186.38421266], [1186.38421266], [1279.53480393], [1408.06360758], [1448.74253449], - [1448.74253449], [1492.50521853], [1552.88879042], [1571.99998447], [1571.99998447], - [10224.87383109], [22164.07366288], [25942.78958866], [25942.78958866], - [26009.11685074], [26100.63493484], [26129.60009555], [26129.60009555], - [26265.05921709], [26451.96515722], [26511.12024823], [26511.12024823], - [26672.16774132], [26894.38041154], [26964.7099619], [26964.7099619], - [27100.16908344], [27287.07502357], [27346.23011458], [27346.23011458], - [27412.55737667], [27504.07546076], [27533.04062147] - ] - ) - - expected_dict['altitudes'] = np.array( - [ - [10.668], [0.], [1001.70617719], [1429.27176545], [1429.27176545], [3413.27102762], - [5642.3831233], [6169.75300447], [6169.75300447], [7399.140983], [8514.78661356], - [8803.21405264], [8803.21405264], [9373.68426297], [10020.99237958], - [10196.42552457], [10196.42552457],[10451.72258036], [10652.38789684], [10668.], - [10668.], [10660.42246376], [10656.16585151], [10668.], [10668.], [10668.], - [10668.], [10668.], [10668.], [10668.], [10142.11478951], [9922.15743555], - [9922.15743555], [8891.66886638], [7502.1861348], [7069.1900852], [7069.1900852], - [5896.44637998], [4264.29354306], [3737.8471594], [3737.8471594], [2702.15624637], - [1248.18960736], [793.03526817], [793.03526817], [345.06939295], [10.668], [10.668] - ] - ) - - expected_dict['masses'] = np.array( - [ - [79303.30184763], [79221.39668215], [79075.19453181], [79028.6003426], - [79028.6003426], [78828.82221909], [78613.60466821], [78557.84739563], - [78557.84739563], [78411.06578989], [78238.0916773], [78186.75440341], - [78186.75440341], [78077.23953313], [77938.37965175], [77896.59718975], - [77896.59718975], [77825.81832958], [77732.75016916], [77704.11629998], - [77704.11629998], [77673.32196072], [77630.75735319], [77617.25716885], - [77617.25716885], [72178.78521803], [65072.41395049], [62903.84179505], - [62903.84179505], [62896.27636813], [62888.3612195], [62885.93748938], - [62885.93748938], [62874.48788511], [62857.70600096], [62852.13740881], - [62852.13740881], [62835.97069937], [62810.37776063], [62801.1924259], - [62801.1924259], [62781.32471014], [62748.91017128], [62737.32520462], - [62737.32520462], [62723.59895849], [62703.94977811], [62697.71513264] - ] - ) - - expected_dict['ranges'] = np.array( - [ - [1452.84514351], [6093.51223933], [15820.03029119], [19123.61258676], - [19123.61258676], [36374.65336952], [61265.3984918], [69106.49687132], - [69106.49687132], [92828.04820577], [126824.13801408], [138011.02420534], - [138011.02420534], [164027.18014424], [200524.66550565], [212113.49107256], - [212113.49107256], [232622.50720766], [261189.53466522], [270353.40501262], - [270353.40501262], [280350.48472685], [294356.27080588], [298832.61221641], - [298832.61221641], [2325837.11255987], [5122689.60556392], [6007883.85695889], - [6007883.85695889], [6022237.43153219], [6039575.06318219], [6044873.89820027], - [6044873.89820027], [6068553.1921364], [6099290.23732297], [6108673.67260778], - [6108673.67260778], [6133535.09572671], [6166722.19545137], [6177077.72115854], - [6177077.72115854], [6197011.1330154], [6224357.63792683], [6232920.45309764], - [6232920.45309764], [6242332.46480721], [6254144.50957549], [6257352.4] - ] - ) - - expected_dict['velocities'] = np.array( - [ - [69.30879167], [137.49019035], [174.54683946], [179.28863383], [179.28863383], - [191.76748742], [194.33322917], [194.52960387], [194.52960387], [199.01184603], - [209.81696863], [212.86546124], [212.86546124], [217.37467051], [219.67762167], - [219.97194272], [219.97194272], [220.67963782], [224.38113484], [226.77184704], - [226.77184704], [230.01128033], [233.72454583], [234.25795132], [234.25795132], - [234.25795132], [234.25795132], [234.25795132], [234.25795132], [201.23881], - [182.84158341], [180.10650108], [180.10650108], [169.77497514], [159.59034446], - [157.09907013], [157.09907013], [151.659491], [147.52098882], [147.07683999], - [147.07683999], [147.05392009], [145.31556891], [143.47446173], [143.47446173], - [138.99109332], [116.22447082], [102.07377559] - ] - ) - # fmt: on - - self.expected_dict = expected_dict - - phase_info = { - 'pre_mission': {'include_takeoff': True, 'optimize_mass': True}, - 'climb': { - 'subsystem_options': {'aerodynamics': {'method': 'computed'}}, - 'user_options': { - 'num_segments': 6, - 'order': 3, - 'mach_bounds': ((0.1, 0.8), 'unitless'), - 'mach_optimize': True, - 'altitude_bounds': ((0.0, 35000.0), 'ft'), - 'altitude_optimize': True, - 'throttle_enforcement': 'path_constraint', - 'mass_ref': (200000, 'lbm'), - 'time_initial': (0.0, 'min'), - 'time_duration_bounds': ((20.0, 60.0), 'min'), - 'no_descent': True, - }, - 'initial_guesses': { - 'time': ([0, 40.0], 'min'), - 'altitude': ([35, 35000.0], 'ft'), - 'mach': ([0.3, 0.79], 'unitless'), - }, - }, - 'cruise': { - 'subsystem_options': {'aerodynamics': {'method': 'computed'}}, - 'user_options': { - 'num_segments': 1, - 'order': 3, - 'mach_initial': (0.79, 'unitless'), - 'mach_bounds': ((0.79, 0.79), 'unitless'), - 'mach_optimize': True, - 'mach_polynomial_order': 1, - 'altitude_initial': (35000.0, 'ft'), - 'altitude_bounds': ((35000.0, 35000.0), 'ft'), - 'altitude_optimize': True, - 'altitude_polynomial_order': 1, - 'throttle_enforcement': 'boundary_constraint', - 'mass_ref': (200000, 'lbm'), - 'time_initial_bounds': ((20.0, 60.0), 'min'), - 'time_duration_bounds': ((60.0, 720.0), 'min'), - }, - 'initial_guesses': { - 'time': ([40, 200], 'min'), - 'altitude': ([35000, 35000.0], 'ft'), - 'mach': ([0.79, 0.79], 'unitless'), - }, - }, - 'descent': { - 'subsystem_options': {'aerodynamics': {'method': 'computed'}}, - 'user_options': { - 'num_segments': 5, - 'order': 3, - 'mach_initial': (0.79, 'unitless'), - 'mach_final': (0.3, 'unitless'), - 'mach_bounds': ((0.2, 0.8), 'unitless'), - 'mach_optimize': True, - 'altitude_initial': (35000.0, 'ft'), - 'altitude_final': (35.0, 'ft'), - 'altitude_bounds': ((0.0, 35000.0), 'ft'), - 'altitude_optimize': True, - 'throttle_enforcement': 'path_constraint', - 'mass_ref': (200000, 'lbm'), - 'distance_ref': (3375, 'nmi'), - 'time_initial_bounds': ((80.0, 780.0), 'min'), - 'time_duration_bounds': ((5.0, 45.0), 'min'), - 'no_climb': True, - }, - 'initial_guesses': { - 'time': ([240, 30], 'min'), - }, - }, - 'post_mission': { - 'include_landing': True, - 'constrain_range': True, - 'target_range': (3375.0, 'nmi'), - }, - } - - self.phase_info = phase_info - - _clear_problem_names() # need to reset these to simulate separate runs - - -class TestBWBFwFmSerial(ProblemPhaseTestCase): - """Run the model in serial that is setup in ProblemPhaseTestCase class.""" - - @require_pyoptsparse(optimizer='SNOPT') - def test_bwb_FwFm_SNOPT(self): - prob = run_aviary( - 'models/aircraft/blended_wing_body/bwb_simple_FLOPS.csv', - self.phase_info, - verbosity=1, - max_iter=50, - optimizer='SNOPT', - ) - - # self.assertTrue(prob.result.success) - compare_against_expected_values(prob, self.expected_dict) - - # This is one of the few places we test Height Energy + simple takeoff. - overall_fuel = prob.get_val(Mission.Summary.TOTAL_FUEL_MASS) - - # Making sure we include the fuel mass consumed in take-off and taxi. - self.assertGreater(overall_fuel, 40000.0) - - -class TestBenchFwFmSerial(ProblemPhaseTestCase): - """Run the model in serial that is setup in ProblemPhaseTestCase class.""" - - @require_pyoptsparse(optimizer='SNOPT') - def test_bench_FwFm_SNOPT(self): - prob = run_aviary( - 'models/aircraft/test_aircraft/aircraft_for_bench_FwFm.csv', - self.phase_info, - verbosity=1, - max_iter=50, - optimizer='SNOPT', - ) - - # self.assertTrue(prob.result.success) - compare_against_expected_values(prob, self.expected_dict) - - # This is one of the few places we test Height Energy + simple takeoff. - overall_fuel = prob.get_val(Mission.Summary.TOTAL_FUEL_MASS) - - # Making sure we include the fuel mass consumed in take-off and taxi. - self.assertGreater(overall_fuel, 40000.0) - - -if __name__ == '__main__': - # unittest.main() - test_bwb = True - if test_bwb: - test = TestBWBFwFmSerial() - test.setUp() - test.test_bwb_FwFm_SNOPT() - else: - test = TestBenchFwFmSerial() - test.setUp() - test.test_bench_FwFm_SNOPT() From 27a3972d4b22a3fbd0df66d8d6ac67aa3845b029 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Thu, 29 Jan 2026 14:13:17 -0500 Subject: [PATCH 19/21] rename test_bwb_Experiment_FwFm_1.py to test_bwb_FwFm.py --- .../{test_bwb_Experiment_FwFm_1.py => test_bwb_FwFm.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename aviary/validation_cases/benchmark_tests/{test_bwb_Experiment_FwFm_1.py => test_bwb_FwFm.py} (100%) diff --git a/aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_1.py b/aviary/validation_cases/benchmark_tests/test_bwb_FwFm.py similarity index 100% rename from aviary/validation_cases/benchmark_tests/test_bwb_Experiment_FwFm_1.py rename to aviary/validation_cases/benchmark_tests/test_bwb_FwFm.py From fcb6723e87cbdc7e1a98ba17eb4d37ca6068c4a2 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Thu, 29 Jan 2026 20:11:48 -0500 Subject: [PATCH 20/21] roll back ground_effect.py --- .../aerodynamics/flops_based/ground_effect.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/aviary/subsystems/aerodynamics/flops_based/ground_effect.py b/aviary/subsystems/aerodynamics/flops_based/ground_effect.py index 4b8c04d4f..5eca2ce12 100644 --- a/aviary/subsystems/aerodynamics/flops_based/ground_effect.py +++ b/aviary/subsystems/aerodynamics/flops_based/ground_effect.py @@ -11,9 +11,8 @@ import numpy as np import openmdao.api as om -from aviary.variable_info.enums import Verbosity -from aviary.variable_info.functions import add_aviary_input, add_aviary_option -from aviary.variable_info.variables import Aircraft, Dynamic, Settings +from aviary.variable_info.functions import add_aviary_input +from aviary.variable_info.variables import Aircraft, Dynamic class GroundEffect(om.ExplicitComponent): @@ -30,7 +29,6 @@ class GroundEffect(om.ExplicitComponent): def initialize(self): options = self.options - add_aviary_option(self, Settings.VERBOSITY) options.declare('num_nodes', default=1, types=int, lower=0) options.declare( @@ -140,7 +138,6 @@ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): options = self.options ground_altitude = options['ground_altitude'] - verbosity = self.options[Settings.VERBOSITY] angle_of_attack = inputs[Dynamic.Vehicle.ANGLE_OF_ATTACK] altitude = inputs[Dynamic.Mission.ALTITUDE] @@ -151,9 +148,6 @@ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): aspect_ratio = inputs[Aircraft.Wing.ASPECT_RATIO] height = inputs[Aircraft.Wing.HEIGHT] span = inputs[Aircraft.Wing.SPAN] - if span <= 0.0: - if verbosity > Verbosity.BRIEF: - raise UserWarning('Aircraft.Wing.SPAN is not positive.') ground_effect_state = ((altitude - ground_altitude) + height) / span height_factor = np.ones_like(ground_effect_state) @@ -170,9 +164,6 @@ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): + 4.0 * ground_effect_state * aspect_ratio_term * np.sqrt(ground_effect_term0) ) - if lift_coeff_factor_denom <= 0.0: - if verbosity > Verbosity.BRIEF: - raise UserWarning('lift_coeff_factor_denom is not positive.') lift_coeff_factor = 1.0 + height_factor / lift_coeff_factor_denom lift_coefficient = base_lift_coefficient * lift_coeff_factor @@ -184,9 +175,6 @@ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): 4.0 * ground_effect_state * np.sqrt(ground_effect_term1) + ground_effect_term1 ) - if drag_coeff_factor_denom <= 0.0: - if verbosity > Verbosity.BRIEF: - raise UserWarning('drag_coeff_factor_denom is not positive.') drag_coeff_factor = 1.0 - height_factor / drag_coeff_factor_denom drag_coefficient = ( From 57df4f51e287654b49d7fa5f7f03fbc8cf381cbf Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Thu, 29 Jan 2026 20:13:30 -0500 Subject: [PATCH 21/21] minor update --- aviary/subsystems/aerodynamics/flops_based/skin_friction.py | 5 ++--- .../aerodynamics/flops_based/skin_friction_drag.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/aviary/subsystems/aerodynamics/flops_based/skin_friction.py b/aviary/subsystems/aerodynamics/flops_based/skin_friction.py index e226d161b..0cf9fb870 100644 --- a/aviary/subsystems/aerodynamics/flops_based/skin_friction.py +++ b/aviary/subsystems/aerodynamics/flops_based/skin_friction.py @@ -51,10 +51,9 @@ def setup(self): if design_type is AircraftTypes.BLENDED_WING_BODY: # No horizontal tail for BWB - nc = 1 + num_tails + num_fuselages + int(sum(num_engines)) + self.nc = nc = 1 + num_tails + num_fuselages + int(sum(num_engines)) else: - nc = 2 + num_tails + num_fuselages + int(sum(num_engines)) - self.nc = nc + 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 e30ada57f..284cc12e4 100644 --- a/aviary/subsystems/aerodynamics/flops_based/skin_friction_drag.py +++ b/aviary/subsystems/aerodynamics/flops_based/skin_friction_drag.py @@ -57,10 +57,9 @@ def setup(self): if design_type is AircraftTypes.BLENDED_WING_BODY: # No horizontal tail for BWB - nc = 1 + nvtail + nfuse + int(sum(num_engines)) + self.nc = nc = 1 + nvtail + nfuse + int(sum(num_engines)) else: - nc = 2 + nvtail + nfuse + int(sum(num_engines)) - self.nc = nc + 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')