diff --git a/aviary/api.py b/aviary/api.py index 9dabf220e..bf4f7f281 100644 --- a/aviary/api.py +++ b/aviary/api.py @@ -1,7 +1,7 @@ ''' This module is the API for Aviary aircraft analysis code -For users: All built-in Aviary functions, code, and objects +For users: All built-in Aviary functions, code, and objects should be imported from this file. For developers: All Aviary code which is intended to be @@ -41,7 +41,7 @@ from aviary.interface.utils.check_phase_info import check_phase_info from aviary.utils.engine_deck_conversion import EngineDeckConverter from aviary.utils.fortran_to_aviary import create_aviary_deck -from aviary.utils.functions import set_aviary_initial_values, get_path +from aviary.utils.functions import set_aviary_input_defaults, set_aviary_initial_values, get_path from aviary.utils.options import list_options from aviary.constants import GRAV_METRIC_GASP, GRAV_ENGLISH_GASP, GRAV_METRIC_FLOPS, GRAV_ENGLISH_FLOPS, GRAV_ENGLISH_LBM, RHO_SEA_LEVEL_ENGLISH, RHO_SEA_LEVEL_METRIC, MU_TAKEOFF, MU_LANDING, PSLS_PSF, TSLS_DEGR, RADIUS_EARTH_METRIC from aviary.subsystems.test.subsystem_tester import TestSubsystemBuilderBase, skipIfMissingDependencies @@ -58,7 +58,6 @@ from aviary.utils.preprocessors import preprocess_options, preprocess_propulsion from aviary.utils.process_input_decks import create_vehicle from aviary.utils.functions import create_opts2vals, add_opts2vals, Null -from aviary.variable_info.variables_in import VariablesIn from aviary.utils.preprocessors import preprocess_crewpayload # ODEs diff --git a/aviary/docs/developer_guide/coding_standards.ipynb b/aviary/docs/developer_guide/coding_standards.ipynb index 081c079ca..d8dd668d0 100644 --- a/aviary/docs/developer_guide/coding_standards.ipynb +++ b/aviary/docs/developer_guide/coding_standards.ipynb @@ -23,7 +23,18 @@ "The Aviary repository contains a configuration file that defines what is run when commits are made and with what options enabled. Currently this is limited to autopep8 with a max line length restriction.\n", "\n", "### Controlling Display Levels\n", - "To make debugging issues easier, it is strongly recommended to make use of the `VERBOSITY` setting. This allows control over how much information is displayed to a user; too much information makes finding relevant information difficult and not enough information can make tracking difficult. `Brief` should be the default in most cases; however, `Quiet` should be the default for tests." + "To make debugging issues easier, it is strongly recommended to make use of the option `Settings.Verbosity`. This allows control over how much information is displayed to a user; too much information makes finding relevant information difficult and not enough information can make tracking difficult. Aviary uses a sliding scale of possible verbosity settings:\n", + "| Verbosity Level | Numerical Value | Description |\n", + "| :--- | :--- | :--- |\n", + "| `QUIET` | 0 | All output except errors are suppressed |\n", + "| `BRIEF` | 1 | Only important information is output, in human-readable format |\n", + "| `VERBOSE` | 2 | All user-relevant information is output, in human-readable format |\n", + "| `DEBUG` | 3 | Any information can be outputted, including warnings, intermediate calculations, etc., with no formatting requirement |\n", + "\n", + "Verbosity levels are defined in Aviary using the `Verbosity` Enum. Each verbosity level is paired with an integer value. In source code, verbosity level can be checked either through comparison with the Enum, or through equality or inequality comparisons with the matching integer value. This allows for code to be triggered not just at a specific level, but for any level above or below the desired setting. Numerical comparisons are recommended for several reasons: they don't require importing the `Verbosity` Enum, and activation is more flexible through the use of inequality comparators, preventing issues like a message only being outputted during `BRIEF` but not `VERBOSE` or `DEBUG`, which a user would expect to also see in higher verbosity settings.\n", + "`BRIEF` is default setting and is used in most cases; however, `QUIET` should be used for tests.\n", + "\n", + "It is preferred that within source code, the full Enums are used for better readability (e.g. `Verbosity.BRIEF`). For tests, scripts, examples, and other places where Aviary is called (rather than defined), it is ok to use the integer representations of verbosity to shorten lines and remove the need to import the `Verbosity` Enum (e.g. passing `0` as the verbosity argument to a function when `QUIET` is desired). Of course, it is always acceptable to use the full Enum in these cases for the same readability reasons." ] }, { diff --git a/aviary/docs/developer_guide/unit_tests.md b/aviary/docs/developer_guide/unit_tests.md index 4a4dcb5fa..d44c80fd9 100644 --- a/aviary/docs/developer_guide/unit_tests.md +++ b/aviary/docs/developer_guide/unit_tests.md @@ -27,6 +27,85 @@ Ran 888 tests using 16 processes Wall clock time: 00:00:54.15 ``` +## Current Unit Tests + +### assert_near_equal + +The unit test that Aviary uses most is `assert_near_equal` from the OpenMDAO utility [assert_near_equal](https://openmdao.org/newdocs/versions/latest/_srcdocs/packages/utils/assert_utils.html). This assertion takes about 70% of all the assertions. It has the following format: + +``` +assert_near_equal(actual_value, expected_value, tolerance=1e-15, tol_type='rel') +``` + +where the `actual_value` is the value from Aviary and `expected_value` is what the developer expects. Ideally, the `expected_value` should come from computation by another tool (e.g. GASP, FLOPS or LEAPS1) or hand computation. When it is not possible, one can accept an Aviary computed value as expected. This guarantees that future development will not alter the outputs by mistake. As for the tolerance, it is good practice to take 1.e-6. By default, it checks relative error. If the `expected_value` is 0.0, it checks the absolute error. + +One can find examples mostly in `subsystems` and `mission` The purpose is to make sure that a variable in an object (namely a component and/or a group) is computed as expected. It is advised that `assert_near_equal` test is carried for all outputs both in components and groups. + +### assert_almost_equal and similar assertions + +A similar unit test is NumPy's utility is `assert_almost_equal`. It checks whether the absolute difference of `actual_value` from `expected_value` is within certain tolerance. As [documented](https://numpy.org/doc/stable/reference/generated/numpy.testing.assert_almost_equal.html) by NumPy, it is not recommended. +For strings and integers, `assertEqual` of `unittest` is used. + +There are other similar assertions like `assertIsNone`, `assertIsNotNone`, `assertNotEqual`, `assertGreater`, `assertIn`, `assertNotIn`, `assertTrue`, and `assertFalse`. They together represent about 15% of all the assertions. + + +### assert_check_partials + +The second most used assertion is `assert_check_partials` from the OpenMDAO utility. This is critically important because it checks whether the partial derivatives coded by develops are correct. It is the key in optimization. To use this test, you first prepare the partial derivative `data` by calling `check_partials` on an object. Then call `assert_check_partials` function with the `data`. + +``` +data = prob.check_partials(out_stream=None) +assert_check_partials(data, atol=1e-06, rtol=1e-06) +``` + +This assert makes sure that the computed derivatives in the code match those computed numerically within the given absolute and relative tolerances. + +In Aviary, there are two ways to compute a component's derivatives: analytically or numerically. When the derivatives are analytic, it is best practice to use `check_partials` to compare them against the complex step (`cs`) or finite difference (`fd`) estimates. +Complex step is much more acurate, but all code in your component's `compute` method must be complex-safe to use this -- in other words, no calculation that squelches the imaginary part of the calculation (like `abs`.) +Note that there are some complex-safe alternatives to commonly-used calculations in the openmdao library. If your code is not complex-safe, or it wraps an external component that doesn't support complex numbers, then finite difference should be used. + +If your component computes its derivatives numerically, there is less reason to test it because you are testing one numerical method against another. If you choose to do this, you will need to use a different method, form, or step. +For example, if the partial derivatives are computed using `cs` method, you need to use `fd` method, or use `cs` method but with a different stepsize (e.g. `step=1.01e-40`): + +``` +data = prob.check_partials(out_stream=None, method="cs", step=1.01e-40) +assert_check_partials(data, atol=1e-06, rtol=1e-06) +``` + +Although the default method of `check_partials` is `fd` (finite difference), we prefer `cs` ([complex step](https://openmdao.org/newdocs/versions/latest/advanced_user_guide/complex_step.html) because it usally gives more accurate results. + +````{margin} +```{note} +In general, we really don't have to check partials that are computed with complex step, since you expect that you should already be getting cs level of accuracy from them. Checks are primarily for analytic derivatives, where you can make mistakes. +``` +```` + +`check_partials` allows you to exclude some components in a group. For example `excludes=["*atmosphere*"]` means that atmosphere component will not be included. + +Sometimes, you may need to exclude a particular partial derivative. You need to write your own code to do so. One example is in `subsystems/propulsion/test/test_propeller_performance.py`. + +````{margin} +```{note} +Some of the partials in Aviary use table look up and interpolations. In the openmdao interpolation, the derivatives aren't always continuous if you interpolate right on one of the table points. You may need to tune the points you choose. For example, if 0.04 is a point in your test, you can change it to 0.04000001 and try again. +``` +```` + +### assert_warning + +This assertion checks that a warning is issued as expected. Currently, there is only one usage in Aviary but we should add more. + +### assert_match_varnames + +Another assertion used in tests is `assert_match_varnames` (about 4%). All of them are in `test_IO()` functions. It tests that all of the variables in an object (component or group) that are declared as inputs or outputs exist in the Aviary variable hierarchy. Exceptions are allowed by specifying `exclude_inputs` and `exclude_outputs`. For details, see [assert_utils.py](https://github.com/OpenMDAO/Aviary/blob/main/aviary/utils/test_utils/assert_utils.py). + +### Other Assertions + +Aviary has built several utility functions for unit tests: + +- `assert_no_duplicates` +- `assert_structure_alphabetization` +- `assert_metadata_alphabetization` + ## Adding Unit Tests Whenever you add to Aviary, you should add a unit test to check its functionality. diff --git a/aviary/docs/examples/OAS_subsystem.ipynb b/aviary/docs/examples/OAS_subsystem.ipynb index a3b4420f4..44f0e0a79 100644 --- a/aviary/docs/examples/OAS_subsystem.ipynb +++ b/aviary/docs/examples/OAS_subsystem.ipynb @@ -68,7 +68,7 @@ " -------\n", " pre_mission_sys : openmdao.core.System\n", " An OpenMDAO system containing all computations that need to happen in\n", - " the pre-mission (formerly statics) part of the Aviary problem. This\n", + " the pre-mission part of the Aviary problem. This\n", " includes sizing, design, and other non-mission parameters.\n", " '''\n", "\n", diff --git a/aviary/docs/getting_started/installation.md b/aviary/docs/getting_started/installation.md index 411357b47..c5b443cee 100644 --- a/aviary/docs/getting_started/installation.md +++ b/aviary/docs/getting_started/installation.md @@ -3,7 +3,7 @@ ## Quick start installation ```{note} -If you do not already have Python installed, we recommend installing [miniconda](https://docs.anaconda.com/miniconda/miniconda-install/). +If you do not already have Python installed, we recommend installing [condaforge](https://github.com/conda-forge/miniforge?tab=readme-ov-file#download). The minimum supported version of Python is 3.9; we recommend using the latest release of Python. ``` diff --git a/aviary/docs/getting_started/onboarding_level2.ipynb b/aviary/docs/getting_started/onboarding_level2.ipynb index 44e268852..e37e6c627 100644 --- a/aviary/docs/getting_started/onboarding_level2.ipynb +++ b/aviary/docs/getting_started/onboarding_level2.ipynb @@ -140,7 +140,7 @@ "# Preprocess inputs\n", "prob.check_and_preprocess_inputs()\n", "\n", - "# adds a pre-mission group (propulsion, geometry, static aerodynamics, and mass)\n", + "# adds a pre-mission group (propulsion, geometry, aerodynamics, and mass)\n", "prob.add_pre_mission_systems()\n", "\n", "# adds a sequence of core mission phases.\n", @@ -371,7 +371,7 @@ "id": "12d71c79", "metadata": {}, "source": [ - "This call adds a pre-mission group (also called static analysis group) which includes pre-mission propulsion, geometry, pre-mission aerodynamics, and mass subsystems. \n", + "This call adds a pre-mission group which includes propulsion, geometry, aerodynamics, and mass subsystems. \n", "\n", "For `height_energy` missions, aviary currently models FLOPS' \"simplified\" takeoff as defined in [mission/flops_based/phases/simplified_takeoff.py](https://github.com/OpenMDAO/Aviary/blob/main/aviary/mission/flops_based/phases/simplified_takeoff.py).\n", "\n", diff --git a/aviary/docs/getting_started/onboarding_level3.ipynb b/aviary/docs/getting_started/onboarding_level3.ipynb index b521fc865..f86677072 100644 --- a/aviary/docs/getting_started/onboarding_level3.ipynb +++ b/aviary/docs/getting_started/onboarding_level3.ipynb @@ -275,7 +275,7 @@ " av.Mission.Landing.LIFT_COEFFICIENT_MAX) # no units\n", ")\n", "\n", - "# Upstream static analysis for aero\n", + "# Upstream pre-mission analysis for aero\n", "prob.model.add_subsystem(\n", " 'pre_mission',\n", " av.CorePreMission(aviary_options=aviary_inputs,\n", @@ -351,7 +351,7 @@ "external_parameters['cruise'] = params\n", "external_parameters['descent'] = params\n", "\n", - "traj = av.setup_trajectory_params(prob.model, traj, aviary_inputs, \n", + "traj = av.setup_trajectory_params(prob.model, traj, aviary_inputs,\n", " external_parameters=external_parameters)\n", "\n", "##################################\n", @@ -449,17 +449,20 @@ "prob.model.add_objective('reg_objective', ref=1)\n", "\n", "# Set initial default values for all LEAPS aircraft variables.\n", - "av.set_aviary_initial_values(prob.model, aviary_inputs)\n", - "\n", - "prob.model.add_subsystem(\n", - " 'input_sink',\n", - " av.VariablesIn(aviary_options=aviary_inputs),\n", - " promotes_inputs=['*'],\n", - " promotes_outputs=['*']\n", - ")\n", + "varnames = [\n", + " av.Aircraft.Wing.MAX_CAMBER_AT_70_SEMISPAN,\n", + " av.Aircraft.Wing.SWEEP,\n", + " av.Aircraft.Wing.TAPER_RATIO,\n", + " av.Aircraft.Wing.THICKNESS_TO_CHORD,\n", + " av.Mission.Design.GROSS_MASS,\n", + " av.Mission.Summary.GROSS_MASS,\n", + "]\n", + "av.set_aviary_input_defaults(prob.model, varnames, aviary_inputs)\n", "\n", "prob.setup(force_alloc_complex=True)\n", "\n", + "av.set_aviary_initial_values(prob, aviary_inputs)\n", + "\n", "############################################\n", "# Initial Settings for States and Controls #\n", "############################################\n", diff --git a/aviary/docs/user_guide/FLOPS_based_detailed_takeoff_and_landing.ipynb b/aviary/docs/user_guide/FLOPS_based_detailed_takeoff_and_landing.ipynb index 3a981c00d..99628a270 100644 --- a/aviary/docs/user_guide/FLOPS_based_detailed_takeoff_and_landing.ipynb +++ b/aviary/docs/user_guide/FLOPS_based_detailed_takeoff_and_landing.ipynb @@ -346,22 +346,22 @@ " 'traj.takeoff_decision_speed.states:velocity',\n", " equals=155.36, units='kn', ref=159.0, indices=[-1])\n", "\n", - "takeoff.model.add_subsystem(\n", - " 'input_sink',\n", - " av.VariablesIn(aviary_options=aviary_options),\n", - " promotes_inputs=['*'],\n", - " promotes_outputs=['*']\n", - ")\n", - " \n", + "varnames = [\n", + " av.Aircraft.Wing.AREA,\n", + " av.Aircraft.Wing.ASPECT_RATIO,\n", + " av.Aircraft.Wing.SPAN,\n", + "]\n", + "av.set_aviary_input_defaults(takeoff.model, varnames, aviary_options)\n", + " \n", "# suppress warnings:\n", "# \"input variable '...' promoted using '*' was already promoted using 'aircraft:*'\n", "with warnings.catch_warnings():\n", - " # Set initial default values for all aircraft variables.\n", - " av.set_aviary_initial_values(takeoff.model, aviary_options)\n", "\n", " warnings.simplefilter(\"ignore\", om.PromotionWarning)\n", " takeoff.setup(check=True)\n", "\n", + "av.set_aviary_initial_values(takeoff, aviary_options)\n", + "\n", "takeoff_trajectory_builder.apply_initial_guesses(takeoff, 'traj')\n", "\n" ] @@ -610,22 +610,22 @@ "\n", "fullstop.add_objective(Dynamic.Mission.DISTANCE, loc='final', ref=distance_max, units=units)\n", "\n", - "landing.model.add_subsystem(\n", - " 'input_sink',\n", - " av.VariablesIn(aviary_options=aviary_options),\n", - " promotes_inputs=['*'],\n", - " promotes_outputs=['*']\n", - ")\n", - "\n", + "varnames = [\n", + " av.Aircraft.Wing.AREA,\n", + " av.Aircraft.Wing.ASPECT_RATIO,\n", + " av.Aircraft.Wing.SPAN,\n", + "]\n", + "av.set_aviary_input_defaults(landing.model, varnames, aviary_options)\n", + " \n", "# suppress warnings:\n", "# \"input variable '...' promoted using '*' was already promoted using 'aircraft:*'\n", "with warnings.catch_warnings():\n", - " # Set initial default values for all aircraft variables.\n", - " av.set_aviary_initial_values(landing.model, aviary_options)\n", "\n", " warnings.simplefilter(\"ignore\", om.PromotionWarning)\n", " landing.setup(check=True)\n", "\n", + "av.set_aviary_initial_values(landing, aviary_options)\n", + "\n", "landing_trajectory_builder.apply_initial_guesses(landing, 'traj')\n", "\n" ] @@ -647,7 +647,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/aviary/docs/user_guide/SGM_capabilities.ipynb b/aviary/docs/user_guide/SGM_capabilities.ipynb index 2123ca5b7..e4ded5a99 100644 --- a/aviary/docs/user_guide/SGM_capabilities.ipynb +++ b/aviary/docs/user_guide/SGM_capabilities.ipynb @@ -28,11 +28,10 @@ "outputs": [], "source": [ "from aviary.mission.gasp_based.ode.base_ode import BaseODE\n", - "from aviary.variable_info.variables import Dynamic, Settings, Mission, Aircraft\n", - "from aviary.variable_info.enums import AnalysisScheme, Verbosity, LegacyCode\n", + "from aviary.variable_info.variables import Dynamic, Mission, Aircraft\n", + "from aviary.variable_info.enums import AnalysisScheme, LegacyCode\n", "from aviary.mission.gasp_based.ode.time_integration_base_classes import SimuPyProblem\n", "from aviary.mission.gasp_based.ode.rotation_ode import RotationODE\n", - "from aviary.utils.aviary_values import AviaryValues\n", "\n", "from aviary.subsystems.propulsion.propulsion_builder import CorePropulsionBuilder\n", "from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder\n", diff --git a/aviary/docs/user_guide/aviary_commands.ipynb b/aviary/docs/user_guide/aviary_commands.ipynb index 4eaf274e8..6fea18b3f 100644 --- a/aviary/docs/user_guide/aviary_commands.ipynb +++ b/aviary/docs/user_guide/aviary_commands.ipynb @@ -235,7 +235,7 @@ "with tempfile.TemporaryDirectory() as tempdir:\n", " os.chdir(tempdir)\n", " for command in commands:\n", - " command = 'aviary hangar '+command\n", + " command = 'aviary hangar ' + command\n", " subprocess.run(command.split()).check_returncode();" ] }, @@ -274,8 +274,8 @@ "If an invalid filepath is given for the input file, pre-packaged resources will be checked for input decks with a matching name.\n", "\n", "Notes for input decks:\n", - "- TurboFan decks for both FLOPS and GASP can be converted\n", - "- TurboProp decks for GASP can also be converted\n", + "- Turbofan decks for both FLOPS and GASP can be converted\n", + "- Turboshaft decks for GASP can also be converted\n", "- Comments can be added using #\n", "\n", "\n", @@ -283,7 +283,7 @@ "```\n", "`aviary convert_engine turbofan_23k_1.eng turbofan_23k_1_lbm_s.deck -f GASP` Convert a GASP based turbofan\n", "`aviary convert_engine -f FLOPS turbofan_22k.eng turbofan_22k.txt` Convert a FLOPS based turbofan\n", - "`aviary convert_engine turboprop_4465hp.eng turboprop_4465hp.deck --data_format GASP_TP` Convert a GASP based turboprop\n", + "`aviary convert_engine turboshaft_4465hp.eng turboshaft_4465hp.deck --data_format GASP_TS` Convert a GASP based turboshaft\n", "```\n" ] }, @@ -302,10 +302,10 @@ "commands = [\n", " 'turbofan_23k_1.eng turbofan_23k_1_lbm_s.deck -f GASP',\n", " 'turbofan_22k.eng turbofan_22k.txt -f FLOPS',\n", - " 'turboprop_4465hp.eng turboprop_4465hp.deck -f GASP_TP',\n", + " 'turboshaft_4465hp.eng turboshaft_4465hp.deck -f GASP_TS',\n", " ]\n", "for command in commands:\n", - " run_command_no_file_error('aviary convert_engine '+command)" + " run_command_no_file_error('aviary convert_engine ' + command)" ] }, { @@ -374,7 +374,7 @@ " '-f FLOPS utils/test/flops_test_polar.txt aviary_flops_polar.txt',\n", "]\n", "for command in commands:\n", - " run_command_no_file_error('aviary convert_aero_table '+command)\n" + " run_command_no_file_error('aviary convert_aero_table ' + command)\n" ] }, { @@ -466,7 +466,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/aviary/docs/user_guide/examples_of_the_same_mission_at_different_UI_levels.ipynb b/aviary/docs/user_guide/examples_of_the_same_mission_at_different_UI_levels.ipynb index 96d426175..fa7d12cf6 100644 --- a/aviary/docs/user_guide/examples_of_the_same_mission_at_different_UI_levels.ipynb +++ b/aviary/docs/user_guide/examples_of_the_same_mission_at_different_UI_levels.ipynb @@ -341,7 +341,7 @@ "\n", "av.preprocess_crewpayload(aviary_inputs)\n", "\n", - "# Upstream static analysis for aero\n", + "# Upstream pre-mission analysis for aero\n", "prob.model.add_subsystem(\n", " 'pre_mission',\n", " av.CorePreMission(aviary_options=aviary_inputs,\n", @@ -387,21 +387,15 @@ "traj.link_phases([\"climb\", \"cruise\", \"descent\"], [\n", " \"time\", \"mass\", \"distance\"], connected=strong_couple)\n", "\n", - "param_vars = [av.Aircraft.Nacelle.CHARACTERISTIC_LENGTH,\n", - " av.Aircraft.Nacelle.FINENESS,\n", - " av.Aircraft.Nacelle.LAMINAR_FLOW_LOWER,\n", - " av.Aircraft.Nacelle.LAMINAR_FLOW_UPPER,\n", - " av.Aircraft.Nacelle.WETTED_AREA]\n", - "\n", - "params = {}\n", - "for var in param_vars:\n", - " params[var] = {'shape': (1, ), 'static_target': True}\n", - "\n", - "external_parameters = {}\n", - "external_parameters['climb'] = params\n", - "external_parameters['cruise'] = params\n", - "external_parameters['descent'] = params\n", - "\n", + "# Need to declare dymos parameters for every input that is promoted out of the missions.\n", + "external_parameters = {'climb': {}, 'cruise': {}, 'descent': {}}\n", + "for default_subsys in default_mission_subsystems:\n", + " params = default_subsys.get_parameters(aviary_inputs=aviary_inputs,\n", + " phase_info={})\n", + " for key, val in params.items():\n", + " for phname in external_parameters:\n", + " external_parameters[phname][key] = val\n", + " \n", "traj = av.setup_trajectory_params(prob.model, traj, aviary_inputs, \n", " external_parameters=external_parameters)\n", "\n", @@ -428,18 +422,22 @@ "\n", "prob.model.add_objective(\"fuel_burned\", ref=1e4)\n", "\n", - "# Set initial default values for all LEAPS aircraft variables.\n", - "av.set_aviary_initial_values(prob.model, aviary_inputs)\n", - "\n", - "prob.model.add_subsystem(\n", - " 'input_sink',\n", - " av.VariablesIn(aviary_options=aviary_inputs),\n", - " promotes_inputs=['*'],\n", - " promotes_outputs=['*']\n", - ")\n", + "# Set initial default values for any variables that have\n", + "# different defaults in premission and mission.\n", + "varnames = [\n", + " av.Aircraft.Wing.AREA,\n", + " av.Aircraft.Wing.MAX_CAMBER_AT_70_SEMISPAN,\n", + " av.Aircraft.Wing.SWEEP,\n", + " av.Aircraft.Wing.TAPER_RATIO,\n", + " av.Aircraft.Wing.THICKNESS_TO_CHORD,\n", + " av.Mission.Design.GROSS_MASS,\n", + "]\n", + "av.set_aviary_input_defaults(prob.model, varnames, aviary_inputs)\n", "\n", "prob.setup(force_alloc_complex=True)\n", "\n", + "av.set_aviary_initial_values(prob, aviary_inputs)\n", + "\n", "############################################\n", "# Initial Settings for States and Controls #\n", "############################################\n", diff --git a/aviary/docs/user_guide/external_aero.ipynb b/aviary/docs/user_guide/external_aero.ipynb index 2ab8e7981..40936bd6e 100644 --- a/aviary/docs/user_guide/external_aero.ipynb +++ b/aviary/docs/user_guide/external_aero.ipynb @@ -84,6 +84,15 @@ " This component doesn't do anything, except set the drag and lift\n", " polars from the file we read in.\n", " \"\"\"\n", + "\n", + " # Your component will compute CD and CL for a grid of altitudes, machs, and\n", + " # angles of attack, and return them in a multidimensional array as described \n", + " # in the example text.\n", + "\n", + " # Because it would be prohibitive to embed something like a vortex lattice\n", + " # code in this example, we are \"cheating\" here by sending a pre-computed\n", + " # drag polar.\n", + " \n", " outputs['drag_table'] = CD\n", " outputs['lift_table'] = CL\n", " \n", @@ -156,12 +165,18 @@ " the pre-mission part of the Aviary problem. This includes sizing, design,\n", " and other non-mission parameters.\n", " \"\"\"\n", - "\n", - " return ExternalAero(altitude=self.altitude, mach=self.mach, angle_of_attack=self.angle_of_attack)\n", - "\n", - " def mission_outputs(self):\n", - " return [('drag_table', Aircraft.Design.DRAG_POLAR),\n", - " ('lift_table', Aircraft.Design.LIFT_POLAR)]" + " aero_group = om.Group()\n", + " aero = ExternalAero(altitude=self.altitude, mach=self.mach, angle_of_attack=self.angle_of_attack)\n", + " aero_group.add_subsystem(\n", + " 'premission_aero', \n", + " aero,\n", + " promotes_inputs=['*'],\n", + " promotes_outputs=[\n", + " ('drag_table', Aircraft.Design.DRAG_POLAR),\n", + " ('lift_table', Aircraft.Design.LIFT_POLAR)\n", + " ]\n", + " )\n", + " return aero_group" ] }, { @@ -264,7 +279,28 @@ "prob.add_driver(\"SLSQP\")\n", "prob.add_design_variables()\n", "prob.add_objective(objective_type=\"mass\", ref=-1e5)\n", - "prob.setup()" + "prob.setup()\n", + "prob.set_initial_guesses()\n", + "\n", + "prob.run_model()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc8788ba-9b2b-4b81-8251-32db008d3a60", + "metadata": { + "tags": [ + "remove-cell" + ] + }, + "outputs": [], + "source": [ + "\n", + "# Make sure we succesfully passed the polar\n", + "from openmdao.utils.assert_utils import assert_near_equal\n", + "om_CD = prob.get_val(Aircraft.Design.DRAG_POLAR)[0, 0, 0]\n", + "assert_near_equal(om_CD, CD[0, 0, 0], 1e-6)" ] } ], @@ -285,7 +321,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.18" + "version": "3.12.3" }, "orphan": true }, diff --git a/aviary/docs/user_guide/features/overriding.ipynb b/aviary/docs/user_guide/features/overriding.ipynb index de7b9a634..2948c2b1e 100644 --- a/aviary/docs/user_guide/features/overriding.ipynb +++ b/aviary/docs/user_guide/features/overriding.ipynb @@ -107,7 +107,7 @@ " -------\n", " pre_mission_sys : openmdao.core.System\n", " An OpenMDAO system containing all computations that need to happen in\n", - " the pre-mission (formerly statics) part of the Aviary problem. This\n", + " the pre-mission part of the Aviary problem. This\n", " includes sizing, design, and other non-mission parameters.\n", " '''\n", " wing_group = om.Group()\n", diff --git a/aviary/docs/user_guide/hamilton_standard.ipynb b/aviary/docs/user_guide/hamilton_standard.ipynb index 6231093ac..c7d4bd0ba 100644 --- a/aviary/docs/user_guide/hamilton_standard.ipynb +++ b/aviary/docs/user_guide/hamilton_standard.ipynb @@ -94,7 +94,7 @@ "options.set_val(av.Aircraft.Engine.COMPUTE_PROPELLER_INSTALLATION_LOSS, val=True, units='unitless')\n", "options.set_val(av.Aircraft.Engine.NUM_PROPELLER_BLADES, val=4, units='unitless')\n", "options.set_val(av.Aircraft.Engine.GENERATE_FLIGHT_IDLE, False)\n", - "options.set_val(av.Aircraft.Engine.DATA_FILE, 'models/engines/turboprop_4465hp.deck')\n", + "options.set_val(av.Aircraft.Engine.DATA_FILE, 'models/engines/turboshaft_4465hp.deck')\n", "options.set_val(av.Aircraft.Engine.USE_PROPELLER_MAP, val=False)\n", "\n", "prob = om.Problem()\n", diff --git a/aviary/docs/user_guide/postprocessing_and_visualizing_results.ipynb b/aviary/docs/user_guide/postprocessing_and_visualizing_results.ipynb index 73fff3224..6318f4a45 100644 --- a/aviary/docs/user_guide/postprocessing_and_visualizing_results.ipynb +++ b/aviary/docs/user_guide/postprocessing_and_visualizing_results.ipynb @@ -14,7 +14,7 @@ "\n", "The dashboard assumes these locations for the various reports that are embedded into the dashboard. \n", "\n", - "| **Section** | **Report name** | **Location** |\n", + "| **Section** | **File** | **Location** |\n", "|--------------|-----------------------------------------------|--------------------------------------------------------------------------------|\n", "| Model | Inputs | ./reports/*name_of_run_script*/inputs.html |\n", "| Model | Debug Input List | ./input_list.txt |\n", @@ -25,10 +25,11 @@ "| Optimization | Total Coloring Report | ./reports/*name_of_run_script*/total_coloring.html |\n", "| Optimization | Optimization Report | ./reports/*name_of_run_script*/opt_report.html |\n", "| Optimization | SNOPT Output (similarly for other optimizers) | ./reports/*name_of_run_script*/SNOPT_print.out |\n", - "| Optimization | Desvars, cons, opt plot | Derived from Case Recorder file specified by `driver_recorder` command option |\n", + "| Optimization | Driver recording | Case Recorder file specified by `driver_recorder` command option |\n", "| Results | Trajectory Results Report | ./reports/*name_of_run_script*/traj_results_report.html |\n", "| Results | Subsystem Results | ./reports/subsystems/*name_of_subsystem.md (or .html)* |\n", "| Results | Mission Results | ./reports/subsystems/mission_summary.md |\n", + "| Results | Problem final case recording | Case Recorder file specified by `problem_recorder` command option, default is `problem_history.db` |\n", "\n", "As an example of the workflow for the dashboard, assume that the user has run an Aviary script, `run_aviary_example`, which records both the `Problem` final case and also all the cases of the optimization done by the `Driver`. (To record both the Problem final case and also the Driver optimization iterations, the user must make use of the `optimization_history_filename` option in the call to `run_aviary_problem`.)\n", "\n", diff --git a/aviary/docs/user_guide/pre_mission_and_mission.ipynb b/aviary/docs/user_guide/pre_mission_and_mission.ipynb index 938233aba..31a0afd0b 100644 --- a/aviary/docs/user_guide/pre_mission_and_mission.ipynb +++ b/aviary/docs/user_guide/pre_mission_and_mission.ipynb @@ -18,11 +18,6 @@ "\n", "A nominal diagram showing the pre-mission and mission systems is shown below.\n", "\n", - "```{note}\n", - "In other works the pre-mission systems are sometimes called \"static\" and the mission systems are called \"dynamic\".\n", - "Within Aviary we avoid the terms \"static\" and \"dynamic\" because they are confusing due to their use in other contexts, such as structural dynamics.\n", - "```\n", - "\n", "![Pre-mission vs mission](images/pre_mission_and_mission.svg)\n", "\n", "## Pre-Mission Systems\n", diff --git a/aviary/docs/user_guide/step_by_step_external_guide.ipynb b/aviary/docs/user_guide/step_by_step_external_guide.ipynb index ea7d554b2..b1c70a128 100644 --- a/aviary/docs/user_guide/step_by_step_external_guide.ipynb +++ b/aviary/docs/user_guide/step_by_step_external_guide.ipynb @@ -10,7 +10,7 @@ "\n", "You need to determine if your subsystem has a pre-mission aspect, a mission aspect, post-mission aspect, or any combination of these.\n", "For example, if you are sizing a part of the aircraft, you might only need the pre-mission systems.\n", - "But if you're trying to track the performance of a dynamic variable -- something that varies across the mission -- you'd want to add a dynamic aspect.\n", + "But if you're trying to track the performance of a dynamic variable -- something that varies across the mission -- you'd want to add a mission system.\n", "The most common and general subsystems have both pre-mission and mission systems.\n", "\n", "The external subsystem interface allows you to provide OpenMDAO Systems (Groups or Components) for the pre-mission, mission, and post-mission parts of Aviary.\n", diff --git a/aviary/docs/user_guide/troubleshooting.ipynb b/aviary/docs/user_guide/troubleshooting.ipynb index 40671518e..65803ed76 100644 --- a/aviary/docs/user_guide/troubleshooting.ipynb +++ b/aviary/docs/user_guide/troubleshooting.ipynb @@ -43,9 +43,8 @@ "### Interpreting optimized results\n", "\n", "```{note}\n", - "A `VERBOSITY` control has been added to minimize the amount of unnecessary information that will be displayed.\n", - "Currently Quiet, Brief [default], Verbose, and Debug are supported. Quiet will suppress practically everything other than warnings and errors. Verbose will include information such as the progress of the optimization, instead of just a final summary. And Debug will contain detailed information about many of the steps as they happen.\n", - "Some of Aviary's CLI functions, such as `fortran_to_aviary`, allow the verbosity to be set directly with a command line argument. `run_mission` uses the variable `settings:verbosity` to control the print levels.\n", + "A \"Verbosity\" control has been added to minimize the amount of unnecessary information that will be displayed. Verbosity settings are discussed in more detail in the developer guide. Mode 0 (QUIET) will suppress practically everything other than warnings and errors. Mode 2 (VERBOSE) will include information such as the progress of the optimization, instead of just a final summary. And mode 3 (DEBUG) will contain detailed information about many of the steps as they happen.\n", + "Some of Aviary's CLI functions, such as \"fortran_to_aviary\", allow the verbosity to be set directly with a command line argument. \"run_mission\" uses the variable \"settings:verbosity\" to control the print levels.\n", "```" ] }, diff --git a/aviary/examples/external_subsystems/OAS_weight/OAS_wing_weight_builder.py b/aviary/examples/external_subsystems/OAS_weight/OAS_wing_weight_builder.py index 47d64d373..46eb1309f 100644 --- a/aviary/examples/external_subsystems/OAS_weight/OAS_wing_weight_builder.py +++ b/aviary/examples/external_subsystems/OAS_weight/OAS_wing_weight_builder.py @@ -19,7 +19,7 @@ def build_pre_mission(self, aviary_inputs): ------- pre_mission_sys : openmdao.core.System An OpenMDAO system containing all computations that need to happen in - the pre-mission (formerly statics) part of the Aviary problem. This + the pre-mission part of the Aviary problem. This includes sizing, design, and other non-mission parameters. ''' diff --git a/aviary/examples/external_subsystems/battery/battery_builder.py b/aviary/examples/external_subsystems/battery/battery_builder.py index 54e2b01ea..e561d8721 100644 --- a/aviary/examples/external_subsystems/battery/battery_builder.py +++ b/aviary/examples/external_subsystems/battery/battery_builder.py @@ -92,7 +92,7 @@ def build_pre_mission(self, aviary_inputs): ------- pre_mission_sys : openmdao.core.System An OpenMDAO system containing all computations that need to happen in - the pre-mission (formerly statics) part of the Aviary problem. This + the pre-mission part of the Aviary problem. This includes sizing, design, and other non-mission parameters. ''' return BatteryPreMission() diff --git a/aviary/examples/external_subsystems/simple_weight/simple_weight_builder.py b/aviary/examples/external_subsystems/simple_weight/simple_weight_builder.py index 376932f37..c9c11220b 100644 --- a/aviary/examples/external_subsystems/simple_weight/simple_weight_builder.py +++ b/aviary/examples/external_subsystems/simple_weight/simple_weight_builder.py @@ -29,7 +29,7 @@ def build_pre_mission(self, aviary_inputs): ------- pre_mission_sys : openmdao.core.System An OpenMDAO system containing all computations that need to happen in - the pre-mission (formerly statics) part of the Aviary problem. This + the pre-mission part of the Aviary problem. This includes sizing, design, and other non-mission parameters. ''' wing_group = om.Group() diff --git a/aviary/examples/off_design_example.py b/aviary/examples/off_design_example.py index 2b8f23a49..ff3393990 100644 --- a/aviary/examples/off_design_example.py +++ b/aviary/examples/off_design_example.py @@ -111,88 +111,28 @@ # Preprocess inputs prob.check_and_preprocess_inputs() - prob.add_pre_mission_systems() - prob.add_phases(phase_info_parameterization=phase_info_parameterization) - prob.add_post_mission_systems() # Link phases and variables prob.link_phases() - prob.add_driver('SNOPT', max_iter=100) - prob.add_design_variables() # Load optimization problem formulation # Detail which variables the optimizer can control prob.add_objective() - prob.setup() - prob.set_initial_guesses() - prob.run_aviary_problem() +prob.save_sizing_to_json() -################### -# Fallout Mission # -################### -prob_fallout = av.AviaryProblem() - -# Load inputs from .csv file -prob_fallout.load_inputs('models/test_aircraft/aircraft_for_bench_FwFm.csv', phase_info) - -prob_fallout.problem_type = ProblemType.FALLOUT -prob_fallout.aviary_inputs.set_val('problem_type', ProblemType.FALLOUT, units='unitless') - - -mission_mass = prob.get_val(Mission.Summary.GROSS_MASS, units='lbm') -prob_fallout.aviary_inputs.set_val( - 'mission:design:gross_mass', mission_mass, units='lbm') -prob_fallout.aviary_inputs.set_val( - 'mission:summary:gross_mass', mission_mass, units='lbm') - -prob_fallout.check_and_preprocess_inputs() -prob_fallout.add_pre_mission_systems() -prob_fallout.add_phases(phase_info_parameterization=phase_info_parameterization) -prob_fallout.add_post_mission_systems() -prob_fallout.link_phases() -prob_fallout.add_driver('SNOPT', max_iter=100) -prob_fallout.add_design_variables() -prob_fallout.add_objective() -prob_fallout.setup() -prob_fallout.set_initial_guesses() -prob_fallout.run_aviary_problem() - -##################### -# Alternate Mission # -##################### -prob_alternate = av.AviaryProblem() -# Load inputs from .csv file -prob_alternate.load_inputs( - 'models/test_aircraft/aircraft_for_bench_FwFm.csv', phase_info) -prob_alternate.problem_type = ProblemType.ALTERNATE -prob_alternate.aviary_inputs.set_val( - 'problem_type', ProblemType.ALTERNATE, units='unitless') - -mission_mass = prob.get_val(Mission.Summary.GROSS_MASS, units='lbm') -prob_alternate.aviary_inputs.set_val( - 'mission:design:gross_mass', mission_mass, units='lbm') -prob_alternate.aviary_inputs.set_val( - 'mission:summary:gross_mass', mission_mass, units='lbm') +# Fallout Mission +prob_fallout = prob.fallout_mission() -prob_alternate.check_and_preprocess_inputs() -prob_alternate.add_pre_mission_systems() -prob_alternate.add_phases(phase_info_parameterization=phase_info_parameterization) -prob_alternate.add_post_mission_systems() -prob_alternate.link_phases() -prob_alternate.add_driver('SNOPT', max_iter=100) -prob_alternate.add_design_variables() -prob_alternate.add_objective() -prob_alternate.setup() -prob_alternate.set_initial_guesses() -prob_alternate.run_aviary_problem() +# Alternate Mission +prob_alternate = prob.alternate_mission() print('--------------') print('Sizing Results') diff --git a/aviary/examples/reserve_missions/run_2dof_reserve_mission_fixedrange.py b/aviary/examples/reserve_missions/run_2dof_reserve_mission_fixedrange.py index f0308109f..aafdb4aa1 100644 --- a/aviary/examples/reserve_missions/run_2dof_reserve_mission_fixedrange.py +++ b/aviary/examples/reserve_missions/run_2dof_reserve_mission_fixedrange.py @@ -8,7 +8,7 @@ We then call the correct methods in order to set up and run an Aviary optimization problem. This performs a coupled design-mission optimization and outputs the results from Aviary into the `reports` folder. """ -from aviary.variable_info.enums import Verbosity + import aviary.api as av from aviary.interface.default_phase_info.two_dof import phase_info from copy import deepcopy @@ -42,7 +42,7 @@ # Link phases and variables prob.link_phases() - prob.add_driver("SNOPT", max_iter=50, verbosity=Verbosity.VERBOSE) + prob.add_driver("SNOPT", max_iter=50, verbosity=2) prob.add_design_variables() diff --git a/aviary/interface/cmd_entry_points.py b/aviary/interface/cmd_entry_points.py index 71f2ff959..5b32168cd 100644 --- a/aviary/interface/cmd_entry_points.py +++ b/aviary/interface/cmd_entry_points.py @@ -11,6 +11,7 @@ from aviary.visualization.dashboard import _dashboard_setup_parser, _dashboard_cmd from aviary.interface.graphical_input import _exec_flight_profile, _setup_flight_profile_parser from aviary.interface.download_models import _exec_hangar, _setup_hangar_parser +from aviary.interface.plot_drag_polar import _exec_plot_drag_polar, _setup_plot_drag_polar_parser def _load_and_exec(script_name, user_args): @@ -58,6 +59,7 @@ def _load_and_exec(script_name, user_args): 'Converts FLOPS- or GASP-formatted aero data files into Aviary csv format.'), 'convert_prop_table': (_setup_PMC_parser, _exec_PMC, 'Converts GASP-formatted propeller map file into Aviary csv format.'), + 'plot_drag_polar': (_setup_plot_drag_polar_parser, _exec_plot_drag_polar, 'Plot a Drag Polar Graph using a provided polar data csv input'), } @@ -97,7 +99,7 @@ def aviary_cmd(): cmdargs = [a for a in sys.argv[1:] if a not in ('-h',)] if len(args) == 1 and len(user_args) == 0: - if args[0] != 'draw_mission' and args[0] != 'run_mission': + if args[0] not in ('draw_mission', 'run_mission', 'plot_drag_polar'): parser.parse_args([args[0], '-h']) if not set(args).intersection(subs.choices) and len(args) == 1 and os.path.isfile(cmdargs[0]): diff --git a/aviary/interface/graphical_input.py b/aviary/interface/graphical_input.py index 655d0632d..a76f6c598 100644 --- a/aviary/interface/graphical_input.py +++ b/aviary/interface/graphical_input.py @@ -420,6 +420,14 @@ def redraw_plot(self): if self.show_phase_slope.get(): self.toggle_phase_slope(redraw=False) self.figure_canvas.draw() + if len(self.data[0]) > 1: + units = [ + self.data_info["units"][i].get() + for i in range(len(self.data_info["units"]))] + est_range, range_unit = estimate_total_range_trapezoidal( + times=self.data[0], mach_numbers=self.data[2], units=units) + self.mouse_coords_str.set(self.mouse_coords_str.get().split(" | Est")[0] + + f" | Estimated Range: {est_range} {range_unit}") def clear_plot(self): """Clears all lines from plots except for crosshairs""" diff --git a/aviary/interface/methods_for_level1.py b/aviary/interface/methods_for_level1.py index 8ccd908e3..c73e69727 100644 --- a/aviary/interface/methods_for_level1.py +++ b/aviary/interface/methods_for_level1.py @@ -49,6 +49,11 @@ def run_aviary(aircraft_filename, phase_info, optimizer=None, phase_info_parameterization : function, optional Additional information to parameterize the phase_info object based on desired cruise altitude and Mach. + optimization_history_filename : str or Path + The name of the database file where the driver iterations are to be recorded. The + default is None. + verbosity : Verbosity or int + Sets level of information outputted to the terminal during model execution. Returns ------- @@ -61,6 +66,8 @@ def run_aviary(aircraft_filename, phase_info, optimizer=None, It raises warnings or errors if there are clashing user inputs. Users can modify or add methods to alter the Aviary problem's behavior. """ + # compatibility with being passed int for verbosity + verbosity = Verbosity(verbosity) # Build problem prob = AviaryProblem(analysis_scheme, name=Path(aircraft_filename).stem) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 6d5c016e2..cee35bbd9 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -6,6 +6,8 @@ from datetime import datetime import importlib.util import sys +import json +import enum import numpy as np @@ -24,7 +26,6 @@ from aviary.mission.twodof_phase import TwoDOFPhase from aviary.mission.gasp_based.ode.params import ParamPort from aviary.mission.gasp_based.phases.time_integration_traj import FlexibleTraj -from aviary.mission.gasp_based.phases.time_integration_phases import SGMCruise from aviary.mission.gasp_based.phases.groundroll_phase import GroundrollPhase from aviary.mission.flops_based.phases.groundroll_phase import GroundrollPhase as GroundrollPhaseVelocityIntegrated from aviary.mission.gasp_based.phases.rotation_phase import RotationPhase @@ -43,10 +44,11 @@ from aviary.utils.preprocessors import preprocess_crewpayload from aviary.interface.utils.check_phase_info import check_phase_info from aviary.utils.aviary_values import AviaryValues +from aviary.utils.functions import convert_strings_to_data, set_value from aviary.variable_info.functions import setup_trajectory_params, override_aviary_vars from aviary.variable_info.variables import Aircraft, Mission, Dynamic, Settings -from aviary.variable_info.enums import AnalysisScheme, ProblemType, SpeedType, AlphaModes, EquationsOfMotion, LegacyCode, Verbosity +from aviary.variable_info.enums import AnalysisScheme, ProblemType, EquationsOfMotion, LegacyCode, Verbosity from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData from aviary.subsystems.propulsion.utils import build_engine_deck @@ -79,10 +81,13 @@ class PreMissionGroup(om.Group): def configure(self): external_outputs = promote_aircraft_and_mission_vars(self) - statics = self.core_subsystems - override_aviary_vars(statics, statics.options["aviary_options"], - external_overrides=external_outputs, - manual_overrides=statics.manual_overrides) + pre_mission = self.core_subsystems + override_aviary_vars( + pre_mission, + pre_mission.options["aviary_options"], + external_overrides=external_outputs, + manual_overrides=pre_mission.manual_overrides, + ) class PostMissionGroup(om.Group): @@ -240,8 +245,7 @@ def __init__(self, analysis_scheme=AnalysisScheme.COLLOCATION, **kwargs): self.regular_phases = [] self.reserve_phases = [] - def load_inputs(self, aviary_inputs, phase_info=None, engine_builders=None, meta_data=BaseMetaData, - verbosity=None): + def load_inputs(self, aviary_inputs, phase_info=None, engine_builders=None, meta_data=BaseMetaData, verbosity=Verbosity.BRIEF): """ This method loads the aviary_values inputs and options that the user specifies. They could specify files to load and values to @@ -252,6 +256,8 @@ def load_inputs(self, aviary_inputs, phase_info=None, engine_builders=None, meta This method is not strictly necessary; a user could also supply an AviaryValues object and/or phase_info dict of their own. """ + # compatibility with being passed int for verbosity + verbosity = Verbosity(verbosity) ## LOAD INPUT FILE ### # Create AviaryValues object from file (or process existing AviaryValues object # with default values from metadata) and generate initial guesses @@ -287,7 +293,7 @@ def load_inputs(self, aviary_inputs, phase_info=None, engine_builders=None, meta phase_info = outputted_phase_info.phase_info # if verbosity level is BRIEF or higher, print that we're using the outputted phase info - if verbosity is not None and verbosity.value >= 1: + if verbosity is not None and verbosity >= Verbosity.BRIEF: print('Using outputted phase_info from current working directory') else: @@ -303,7 +309,7 @@ def load_inputs(self, aviary_inputs, phase_info=None, engine_builders=None, meta elif self.mission_method is HEIGHT_ENERGY: from aviary.interface.default_phase_info.height_energy import phase_info - if verbosity is not None and verbosity.value >= 1: + if verbosity is not None and verbosity >= Verbosity.BRIEF: print('Loaded default phase_info for ' f'{self.mission_method.value.lower()} equations of motion') @@ -440,7 +446,9 @@ def check_and_preprocess_inputs(self): if target_distance[0] <= 0: raise ValueError( f"Invalid target_distance in [{phase_name}].[user_options]. " - f"Current (value: {target_distance[0]}), (units: {target_distance[1]}) <= 0") + f"Current (value: {target_distance[0]}), " + f"(units: {target_distance[1]}) <= 0" + ) # Checks to make sure target_duration is positive, # Sets duration_bounds, initial_guesses, and fixed_duration @@ -462,8 +470,10 @@ def check_and_preprocess_inputs(self): target_duration = self.phase_info[phase_name]["user_options"]["target_duration"] if target_duration[0] <= 0: raise ValueError( - f"Invalid target_duration in phase_info[{phase_name}][user_options]. " - f"Current (value: {target_duration[0]}), (units: {target_duration[1]}) <= 0") + f'Invalid target_duration in phase_info[{phase_name}]' + f'[user_options]. Current (value: {target_duration[0]}), ' + f'(units: {target_duration[1]}) <= 0")' + ) # Only applies to non-analytic phases (all HE and most 2DOF) if not analytic: @@ -549,8 +559,7 @@ def check_and_preprocess_inputs(self): def add_pre_mission_systems(self): """ - Add pre-mission systems to the Aviary problem. These systems are executed before the mission - and are also known as the "pre_mission" group. + Add pre-mission systems to the Aviary problem. These systems are executed before the mission. Depending on the mission model specified (`FLOPS` or `GASP`), this method adds various subsystems to the aircraft model. For the `FLOPS` mission model, a takeoff phase is added using the Takeoff class @@ -1081,8 +1090,15 @@ def add_subsystem_timeseries_outputs(phase, phase_name): phase_name, self._get_phase(phase_name, phase_idx)) add_subsystem_timeseries_outputs(phase, phase_name) - if phase_name == 'ascent' and self.mission_method is TWO_DEGREES_OF_FREEDOM: - self._add_groundroll_eq_constraint(phase) + if self.mission_method is TWO_DEGREES_OF_FREEDOM: + + # In GASP, we still use the phase name to infer the phase type. + # We need this information to be available in the builders. + # TODO - Ultimately we should overhaul all of this. + self.phase_info[phase_name]['phase_type'] = phase_name + + if phase_name == 'ascent': + self._add_groundroll_eq_constraint(phase) # loop through phase_info and external subsystems external_parameters = {} @@ -1098,9 +1114,8 @@ def add_subsystem_timeseries_outputs(phase, phase_name): for parameter in parameter_dict: external_parameters[phase_name][parameter] = parameter_dict[parameter] - if self.mission_method in (HEIGHT_ENERGY, SOLVED_2DOF): - traj = setup_trajectory_params( - self.model, traj, self.aviary_inputs, phases, meta_data=self.meta_data, external_parameters=external_parameters) + traj = setup_trajectory_params( + self.model, traj, self.aviary_inputs, phases, meta_data=self.meta_data, external_parameters=external_parameters) self.traj = traj @@ -1108,7 +1123,7 @@ def add_subsystem_timeseries_outputs(phase, phase_name): def add_post_mission_systems(self, include_landing=True): """ - Add post-mission systems to the aircraft model. This is akin to the statics group + Add post-mission systems to the aircraft model. This is akin to the pre-mission group or the "premission_systems", but occurs after the mission in the execution order. Depending on the mission model specified (`FLOPS` or `GASP`), this method adds various subsystems @@ -1597,7 +1612,7 @@ def add_driver(self, optimizer=None, use_coloring=None, max_iter=50, verbosity=V The maximum number of iterations allowed for the optimization process. Default is 50. This option is applicable to "SNOPT", "IPOPT", and "SLSQP" optimizers. - verbosity : Verbosity or list, optional + verbosity : Verbosity, int or list, optional If Verbosity.DEBUG, debug print options ['desvars','ln_cons','nl_cons','objs'] will be set. If a list is provided, it will be used as the debug print options. @@ -1605,8 +1620,8 @@ def add_driver(self, optimizer=None, use_coloring=None, max_iter=50, verbosity=V ------- None """ - if not isinstance(verbosity, Verbosity): - verbosity = Verbosity(verbosity) + # compatibility with being passed int for verbosity + verbosity = Verbosity(verbosity) # Set defaults for optimizer and use_coloring based on analysis scheme if optimizer is None: @@ -1629,7 +1644,7 @@ def add_driver(self, optimizer=None, use_coloring=None, max_iter=50, verbosity=V isumm, iprint = 0, 0 elif verbosity == Verbosity.BRIEF: isumm, iprint = 6, 0 - else: + elif verbosity > Verbosity.BRIEF: isumm, iprint = 6, 9 driver.opt_settings["Major iterations limit"] = max_iter driver.opt_settings["Major optimality tolerance"] = 1e-4 @@ -1646,7 +1661,7 @@ def add_driver(self, optimizer=None, use_coloring=None, max_iter=50, verbosity=V driver.opt_settings['print_frequency_iter'] = 10 elif verbosity == Verbosity.VERBOSE: print_level = 5 - else: + else: # DEBUG print_level = 7 driver.opt_settings['tol'] = 1.0E-6 driver.opt_settings['mu_init'] = 1e-5 @@ -1665,15 +1680,15 @@ def add_driver(self, optimizer=None, use_coloring=None, max_iter=50, verbosity=V driver.options["maxiter"] = max_iter driver.options["disp"] = disp - if verbosity != Verbosity.QUIET: + if verbosity > Verbosity.QUIET: if isinstance(verbosity, list): driver.options['debug_print'] = verbosity - elif verbosity.value > Verbosity.DEBUG.value: + elif verbosity == Verbosity.DEBUG: driver.options['debug_print'] = ['desvars', 'ln_cons', 'nl_cons', 'objs'] if optimizer in ("SNOPT", "IPOPT"): - if verbosity is Verbosity.QUIET: + if verbosity == Verbosity.QUIET: driver.options['print_results'] = False - elif verbosity is not Verbosity.DEBUG: + elif verbosity < Verbosity.DEBUG: driver.options['print_results'] = 'minimal' def add_design_variables(self): @@ -1921,10 +1936,6 @@ def _add_bus_variables_and_connect(self): def setup(self, **kwargs): """ Lightly wrappd setup() method for the problem. - - Allows us to do pre- and post-setup changes, like adding - calls to `set_input_defaults` and do some simple `set_vals` - if needed. """ # suppress warnings: # "input variable '...' promoted using '*' was already promoted using 'aircraft:*' @@ -2325,6 +2336,167 @@ def run_aviary_problem(self, self.problem_ran_successfully = not failed + def alternate_mission(self, run_mission=True, + json_filename='sizing_problem.json', + payload_mass=None, mission_range=None, + phase_info=None, verbosity=Verbosity.BRIEF): + """ + This function runs an alternate mission based on a sizing mission output. + + Parameters + ---------- + run_mission : bool + Flag to determine whether to run the mission before returning the problem object. + json_filename : str + Name of the file that the sizing mission has been saved to. + mission_range : float, optional + Target range for the fallout mission. + payload_mass : float, optional + Mass of the payload for the mission. + phase_info : dict, optional + Dictionary containing the phases and their required parameters. + verbosity : Verbosity or list, optional + If Verbosity.DEBUG, debug print options ['desvars','ln_cons','nl_cons','objs'] will be set. + If a list is provided, it will be used as the debug print options. + """ + if phase_info is None: + phase_info = self.phase_info + if mission_range is None: + design_range = self.get_val(Mission.Design.RANGE) + if payload_mass is None: + if self.mission_method is HEIGHT_ENERGY: + payload_mass = self.get_val(Aircraft.CrewPayload.TOTAL_PAYLOAD_MASS) + elif self.mission_method is TWO_DEGREES_OF_FREEDOM: + payload_mass = self.get_val(Aircraft.CrewPayload.PASSENGER_PAYLOAD_MASS) + + mission_mass = self.get_val(Mission.Design.GROSS_MASS) + optimizer = self.driver.options["optimizer"] + + prob_alternate = _load_off_design(json_filename, ProblemType.ALTERNATE, + phase_info, payload_mass, design_range, mission_mass) + + prob_alternate.check_and_preprocess_inputs() + prob_alternate.add_pre_mission_systems() + prob_alternate.add_phases() + prob_alternate.add_post_mission_systems() + prob_alternate.link_phases() + prob_alternate.add_driver(optimizer, verbosity=verbosity) + prob_alternate.add_design_variables() + prob_alternate.add_objective() + prob_alternate.setup() + prob_alternate.set_initial_guesses() + if run_mission: + prob_alternate.run_aviary_problem( + record_filename='alternate_problem_history.db') + return prob_alternate + + def fallout_mission(self, run_mission=True, + json_filename='sizing_problem.json', + mission_mass=None, payload_mass=None, + phase_info=None, verbosity=Verbosity.BRIEF): + """ + This function runs a fallout mission based on a sizing mission output. + + Parameters + ---------- + run_mission : bool + Flag to determine whether to run the mission before returning the problem object. + json_filename : str + Name of the file that the sizing mission has been saved to. + mission_mass : float, optional + Takeoff mass for the fallout mission. + payload_mass : float, optional + Mass of the payload for the mission. + phase_info : dict, optional + Dictionary containing the phases and their required parameters. + verbosity : Verbosity or list, optional + If Verbosity.DEBUG, debug print options ['desvars','ln_cons','nl_cons','objs'] will be set. + If a list is provided, it will be used as the debug print options. + """ + if phase_info is None: + phase_info = self.phase_info + if mission_mass is None: + mission_mass = self.get_val(Mission.Design.GROSS_MASS) + if payload_mass is None: + if self.mission_method is HEIGHT_ENERGY: + payload_mass = self.get_val(Aircraft.CrewPayload.TOTAL_PAYLOAD_MASS) + elif self.mission_method is TWO_DEGREES_OF_FREEDOM: + payload_mass = self.get_val(Aircraft.CrewPayload.PASSENGER_PAYLOAD_MASS) + + design_range = self.get_val(Mission.Design.RANGE) + optimizer = self.driver.options["optimizer"] + + prob_fallout = _load_off_design(json_filename, ProblemType.FALLOUT, phase_info, + payload_mass, design_range, mission_mass) + + prob_fallout.check_and_preprocess_inputs() + prob_fallout.add_pre_mission_systems() + prob_fallout.add_phases() + prob_fallout.add_post_mission_systems() + prob_fallout.link_phases() + prob_fallout.add_driver(optimizer, verbosity=verbosity) + prob_fallout.add_design_variables() + prob_fallout.add_objective() + prob_fallout.setup() + prob_fallout.set_initial_guesses() + if run_mission: + prob_fallout.run_aviary_problem(record_filename='fallout_problem_history.db') + return prob_fallout + + def save_sizing_to_json(self, json_filename='sizing_problem.json'): + """ + This function saves an aviary problem object into a json file. + + Parameters + ---------- + aviary_problem: OpenMDAO Aviary Problem + Aviary problem object optimized for the aircraft design/sizing mission. + Assumed to contain aviary_inputs and Mission.Summary.GROSS_MASS + json_filename: string + User specified name and relative path of json file to save the data into. + """ + + aviary_input_list = [] + with open(json_filename, 'w') as jsonfile: + # Loop through aviary input datastructure and create a list + for data in self.aviary_inputs: + (name, (value, units)) = data + type_value = type(value) + + # Get the gross mass value from the sizing problem and add it to input list + if name == Mission.Summary.GROSS_MASS or name == Mission.Design.GROSS_MASS: + Mission_Summary_GROSS_MASS_val = self.get_val( + Mission.Summary.GROSS_MASS, units=units) + Mission_Summary_GROSS_MASS_val_list = Mission_Summary_GROSS_MASS_val.tolist() + value = Mission_Summary_GROSS_MASS_val_list[0] + + else: + # there are different data types we need to handle for conversion to json format + # int, bool, float doesn't need anything special + + # Convert numpy arrays to lists + if type_value == np.ndarray: + value = value.tolist() + + # Lists are fine except if they contain enums + if type_value == list: + if type(type(value[0])) == enum.EnumType: + for i in range(len(value)): + value[i] = str([value[i]]) + + # Enums need converting to a string + if type(type(value)) == enum.EnumType: + value = str([value]) + + # Append the data to the list + aviary_input_list.append([name, value, units, str(type_value)]) + + # Write the list to a json file + json.dump(aviary_input_list, jsonfile, sort_keys=True, + indent=4, ensure_ascii=False) + + jsonfile.close() + def _add_hybrid_objective(self, phase_info): phases = list(phase_info.keys()) takeoff_mass = self.aviary_inputs.get_val( @@ -2542,7 +2714,145 @@ def _add_fuel_reserve_component(self, post_mission=True, promotes_inputs=["reserve_fuel_frac_mass", ("reserve_fuel_additional", Aircraft.Design.RESERVE_FUEL_ADDITIONAL), - ("reserve_fuel_burned", Mission.Summary.RESERVE_FUEL_BURNED)], + ("reserve_fuel_burned", + Mission.Summary.RESERVE_FUEL_BURNED)], promotes_outputs=[ ("reserve_fuel", reserves_name)] ) + + +def _read_sizing_json(aviary_problem, json_filename): + """ + This function reads in an aviary problem object from a json file. + + Parameters + ---------- + aviary_problem: OpenMDAO Aviary Problem + Aviary problem object optimized for the aircraft design/sizing mission. + Assumed to contain aviary_inputs and Mission.Summary.GROSS_MASS + json_filename: string + User specified name and relative path of json file to save the data into + + Returns + ---------- + Aviary Problem object with updated input values from json file + + """ + # load saved input list from json file + with open(json_filename) as json_data_file: + loaded_aviary_input_list = json.load(json_data_file) + json_data_file.close() + + # Loop over input list and assign aviary problem input values + counter = 0 # list index tracker + for inputs in loaded_aviary_input_list: + [var_name, var_values, var_units, var_type] = inputs + + # Initialize some flags to idetify arrays and enums + is_array = False + is_enum = False + + if var_type == "": + is_array = True + + elif var_type == "": + # check if the list contains enums + for i in range(len(var_values)): + if isinstance(var_values[i], str): + if var_values[i].find("<") != -1: + # Found a list of enums: set the flag + is_enum = True + + # Manipulate the string to find the value + tmp_var_values = var_values[i].split(':')[-1] + var_values[i] = tmp_var_values.replace(">", "").replace( + "]", "").replace("'", "").replace(" ", "") + + if is_enum: + var_values = convert_strings_to_data(var_values) + + else: + var_values = [var_values] + + elif var_type.find("", "").replace( + "]", "").replace("'", "").replace(" ", "") + var_values = convert_strings_to_data([var_values]) + + else: + # values are expected to be parsed as a list to set_value function + var_values = [var_values] + + # Check if the variable is in meta data + if var_name in BaseMetaData.keys(): + try: + aviary_problem.aviary_inputs = set_value( + var_name, var_values, aviary_problem.aviary_inputs, units=var_units, + is_array=is_array, meta_data=BaseMetaData) + except: + # Print helpful error + print("FAILURE: list_num = ", counter, "Input String = ", inputs, + "Attempted to set_value(", var_name, ",", var_values, ",", var_units, ")") + else: + # Not in the MetaData + print("Name not found in MetaData: list_num =", counter, "Input String =", + inputs, "Attempted set_value(", var_name, ",", var_values, ",", var_units, ")") + + counter = counter + 1 # increment index tracker + return aviary_problem + + +def _load_off_design(json_filename, ProblemType, phase_info, + payload, mission_range, mission_gross_mass): + """ + This function loads a sized aircraft, and sets up an aviary problem + for a specified off design mission. + + Parameters + ---------- + json_filename: string + User specified name and relative path of json file containing the sized aircraft data + ProblemType: enum + Alternate or Fallout. Alternate requires mission_range input and + Fallout requires mission_fuel input + phase_info: phase_info dictionary for off design mission + payload: float + Aircraft.CrewPayload.PASSENGER_PAYLOAD_MASS + mission_range float + Mission.Summary.RANGE 'NM' + mission_gross_mass float + Mission.Summary.GROSS_MASS 'lbm' + + Returns + ---------- + Aviary Problem object with completed load_inputs() for specified off design mission + """ + + # Initialize a new aviary problem and aviary_input data structure + prob = AviaryProblem() + prob.aviary_inputs = AviaryValues() + + prob = _read_sizing_json(prob, json_filename) + + # Update problem type + prob.problem_type = ProblemType + prob.aviary_inputs.set_val('settings:problem_type', ProblemType, units='unitless') + + # Set Payload + prob.aviary_inputs.set_val( + Aircraft.CrewPayload.PASSENGER_PAYLOAD_MASS, payload, units='lbm') + + if ProblemType == ProblemType.ALTERNATE: + # Set mission range, aviary will calculate required fuel + prob.aviary_inputs.set_val(Mission.Design.RANGE, mission_range, units='NM') + + elif ProblemType == ProblemType.FALLOUT: + # Set mission fuel and calculate gross weight, aviary will calculate range + prob.aviary_inputs.set_val(Mission.Summary.GROSS_MASS, + mission_gross_mass, units='lbm') + + # Load inputs + prob.load_inputs(prob.aviary_inputs, phase_info) + return prob diff --git a/aviary/interface/plot_drag_polar.py b/aviary/interface/plot_drag_polar.py new file mode 100644 index 000000000..189519edc --- /dev/null +++ b/aviary/interface/plot_drag_polar.py @@ -0,0 +1,208 @@ +import matplotlib.pyplot as plt +import numpy as np +import aviary.api as av +import matplotlib.cm as cm +from tkinter import Label, Button, StringVar, filedialog, messagebox +from tkinter.ttk import Combobox +from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk) +import tkinter as tk +from aviary.utils.functions import get_path + + +def plot_drag_polar(input_file=None): + if input_file is None: + input_file = filedialog.askopenfilename(filetypes=[("CSV files", "*.csv")]) + if not input_file: + messagebox.showerror( + "Error", "No file selected") + exit() + return + + try: + input_path = get_path(input_file) + polar_data = av.read_data_file(input_path, aliases={ + 'altitude': 'altitude', + 'mach_number': 'mach_number', + 'alpha': 'angle_of_attack', + 'CL': 'lift_coefficient', + 'CD': 'total_drag_coefficient' + }) + except Exception as e: + messagebox.showerror("Error", f"Failed to read the file: {str(e)}") + return + + mach = polar_data.get_val('mach_number') + + mach_values = np.unique(mach) + + altitude = polar_data.get_val('altitude') + + altitude_values = np.unique(altitude) + + alpha_values = polar_data.get_val('alpha') + + CD_values = polar_data.get_val('CD') + + CL_values = polar_data.get_val('CL') + + window = tk.Tk() + + window.title('Drag Polar Plot') + fig, ax = plt.subplots(nrows=1) + + fig.tight_layout(pad=4) + canvas = FigureCanvasTkAgg(fig, master=window) + canvas.get_tk_widget().pack(expand=True, fill='both') + + toolbar = NavigationToolbar2Tk(canvas, window) + + def plot_polar(ax, CD, CL, color, label=None, marker='o'): + ax.plot(CD, CL, color=color, label=label, marker='o') + + def update_plot(): + x_var = set_x_var.get() + y_var = set_y_var.get() + fix_variable = fix_variable_var.get() + fix_value = float(fix_value_var.get()) + ax.clear() + + if fix_variable == 'Mach': + + indices = mach == fix_value + + fixed_values = altitude_values + fixed_label = 'Altitude' + else: + + index = altitude == fix_value + fixed_values = mach_values + fixed_label = 'Mach' + + colors = cm.viridis(np.linspace(0, 1, len(fixed_values))) + for i, val in enumerate(fixed_values): + if fix_variable == 'Mach': + indices = (mach == fix_value) & (altitude == val) + + CD = np.array(CD_values[indices]) + + CL = CL_values[indices] + + alpha = alpha_values[indices] + + else: + index = (altitude == fix_value) & (mach == val) + + CD = np.array(CD_values[index]) + + CL = CL_values[index] + + alpha = alpha_values[index] + + if x_var == 'CD': + x_val = CD + + elif x_var == 'CL': + x_val = CL + + elif x_var == 'Alpha': + x_val = alpha + + elif x_var == 'CL/CD': + x_val = np.array(CL)/np.array(CD) + + if y_var == 'CD': + y_val = CD + + elif y_var == 'CL': + y_val = CL + + elif y_var == 'Alpha': + y_val = alpha + + elif y_var == 'CL/CD': + y_val = np.array(CL)/np.array(CD) + + plot_polar(ax, x_val, y_val, color=colors[i], label=f'{fixed_label} {val}') + + ax.set_xlabel(f'{x_var}') + ax.set_ylabel(f'{y_var}') + ax.legend(loc='center left', bbox_to_anchor=(1, 0.5), title=fixed_label) + ax.set_title(f'{y_var} vs {x_var} for fixed {fix_variable} = {fix_value}') + + canvas.draw() + toolbar.update() + + def update_fix_value_combobox(*args): + if fix_variable_var.get() == 'Mach': + fix_value_combobox['values'] = [str(value) for value in mach_values] + fix_value_var.set(str(mach_values[0])) + fix_value_combobox.current(0) + else: + fix_value_combobox['values'] = [str(value) for value in altitude_values] + fix_value_var.set(str(altitude_values[0])) + + set_x_var = StringVar(value='CD') + + set_x_label = Label(master=window, text="x-axis") + set_x_label.pack(side='left', padx=5, pady=5) + set_x_combobox = Combobox(master=window, textvariable=set_x_var, values=[ + 'CD', 'CL', 'Alpha', 'CL/CD']) + + set_x_combobox.pack(side='left', padx=5, pady=5) + set_y_var = StringVar(value='CL') + set_y_label = Label(master=window, text="y-axis") + set_y_label.pack(side='left', padx=5, pady=5) + set_y_combobox = Combobox(master=window, textvariable=set_y_var, values=[ + 'CL', 'CD', 'Alpha', 'CL/CD']) + set_y_combobox.pack(side='left', padx=5, pady=5) + fix_variable_var = StringVar(value='Mach') + fix_value_var = StringVar(value=float(mach_values[0])) + fix_variable_label = Label(master=window, text="Fix Variable:") + fix_variable_label.pack(side='left', padx=5, pady=5) + + fix_variable_combobox = Combobox( + master=window, textvariable=fix_variable_var, values=['Mach', 'Altitude']) + fix_variable_combobox.pack(side='left', padx=5, pady=5) + fix_variable_combobox.bind("<>", update_fix_value_combobox) + fix_variable_combobox.current(0) + fix_value_label = Label(master=window, text="Fix Value:") + fix_value_label.pack(side='left', padx=5, pady=5) + + fix_value_combobox = Combobox(master=window, textvariable=fix_value_var) + fix_value_combobox.pack(side='left', padx=5, pady=5) + update_fix_value_combobox() + + plot_button = Button(master=window, text="Plot", command=update_plot) + plot_button.pack(side='right', padx=5, pady=5) + window.mainloop() + + +def _setup_plot_drag_polar_parser(parser): + """ + Set up the command line options for the Model Building tool. + Parameters + ---------- + parser : argparse.ArgumentParser + The parser instance. + parser : argparse subparser + The parser we're adding options to. + """ + + pass + + +def _exec_plot_drag_polar(options, user_args): + """ + Run the Model Building tool. + Parameters + ---------- + options : argparse.Namespace + Command line options. + user_args : list of str + Args to be passed to the user script. + """ + plot_drag_polar() + + +if __name__ == "__main__": + plot_drag_polar() diff --git a/aviary/interface/test/test_cmd_entry_points.py b/aviary/interface/test/test_cmd_entry_points.py index 91d18a79a..c255fe9a2 100644 --- a/aviary/interface/test/test_cmd_entry_points.py +++ b/aviary/interface/test/test_cmd_entry_points.py @@ -118,10 +118,10 @@ def test_FLOPS_conversion(self): cmd = f'aviary convert_engine {filepath} {outfile} -f FLOPS' self.run_and_test_cmd(cmd) - def test_GASP_TP_conversion(self): - filepath = self.get_file('models/engines/turboprop_4465hp.eng') - outfile = Path.cwd() / 'turboprop_4465hp.eng' - cmd = f'aviary convert_engine {filepath} {outfile} --data_format GASP_TP' + def test_GASP_TS_conversion(self): + filepath = self.get_file('models/engines/turboshaft_4465hp.eng') + outfile = Path.cwd() / 'turboshaft_4465hp.eng' + cmd = f'aviary convert_engine {filepath} {outfile} --data_format GASP_TS' self.run_and_test_cmd(cmd) diff --git a/aviary/interface/test/test_height_energy_mission.py b/aviary/interface/test/test_height_energy_mission.py index 7d8397ac5..29d24b244 100644 --- a/aviary/interface/test/test_height_energy_mission.py +++ b/aviary/interface/test/test_height_energy_mission.py @@ -10,7 +10,6 @@ from aviary.subsystems.test.test_dummy_subsystem import ArrayGuessSubsystemBuilder from aviary.mission.energy_phase import EnergyPhase from aviary.variable_info.variables import Dynamic -from aviary.variable_info.enums import Verbosity @use_tempdirs @@ -120,9 +119,14 @@ def add_external_subsystem(self, phase_info, subsystem_builder): def run_mission(self, phase_info, optimizer): return run_aviary( - self.aircraft_definition_file, phase_info, - make_plots=self.make_plots, max_iter=self.max_iter, optimizer=optimizer, - optimization_history_filename="driver_test.db", verbosity=Verbosity.QUIET) + self.aircraft_definition_file, + phase_info, + make_plots=self.make_plots, + max_iter=self.max_iter, + optimizer=optimizer, + optimization_history_filename="driver_test.db", + verbosity=0, + ) def test_mission_basic_and_dashboard(self): # We need to remove the TESTFLO_RUNNING environment variable for this test to run. @@ -139,7 +143,10 @@ def test_mission_basic_and_dashboard(self): self.assertIsNotNone(prob) self.assertTrue(prob.problem_ran_successfully) - cmd = f'aviary dashboard --problem_recorder dymos_solution.db --driver_recorder driver_test.db {prob.driver._problem()._name}' + cmd = ( + 'aviary dashboard --problem_recorder dymos_solution.db --driver_recorder ' + f'driver_test.db {prob.driver._problem()._name}' + ) # this only tests that a given command line tool returns a 0 return code. It doesn't # check the expected output at all. The underlying functions that implement the # commands should be tested seperately. @@ -224,16 +231,26 @@ def test_custom_phase_builder(self): local_phase_info = self.phase_info.copy() local_phase_info['climb']['phase_builder'] = EnergyPhase - run_aviary(self.aircraft_definition_file, local_phase_info, - verbosity=Verbosity.QUIET, max_iter=1, optimizer='SLSQP') + run_aviary( + self.aircraft_definition_file, + local_phase_info, + verbosity=0, + max_iter=1, + optimizer='SLSQP', + ) def test_custom_phase_builder_error(self): local_phase_info = self.phase_info.copy() local_phase_info['climb']['phase_builder'] = "fake phase object" with self.assertRaises(TypeError): - run_aviary(self.aircraft_definition_file, local_phase_info, - verbosity=Verbosity.QUIET, max_iter=1, optimizer='SLSQP') + run_aviary( + self.aircraft_definition_file, + local_phase_info, + verbosity=0, + max_iter=1, + optimizer='SLSQP', + ) if __name__ == '__main__': diff --git a/aviary/interface/test/test_interface_bugs.py b/aviary/interface/test/test_interface_bugs.py index 7a44a250e..dbab7e247 100644 --- a/aviary/interface/test/test_interface_bugs.py +++ b/aviary/interface/test/test_interface_bugs.py @@ -7,7 +7,6 @@ from aviary.subsystems.subsystem_builder_base import SubsystemBuilderBase from aviary.interface.default_phase_info.height_energy import phase_info as ph_in from aviary.variable_info.variables import Aircraft -from aviary.variable_info.enums import Verbosity from aviary.utils.functions import get_aviary_resource_path from openmdao.utils.testing_utils import use_tempdirs @@ -20,8 +19,9 @@ def setup(self): self.add_output(Aircraft.Canard.ASPECT_RATIO, 1.0, units='unitless') self.add_output('Tail', 1.0, units='unitless') - self.declare_partials(Aircraft.Canard.ASPECT_RATIO, - Aircraft.Engine.MASS, val=2.0) + self.declare_partials( + Aircraft.Canard.ASPECT_RATIO, Aircraft.Engine.MASS, val=2.0 + ) self.declare_partials('Tail', Aircraft.Engine.MASS, val=0.7) def compute(self, inputs, outputs): @@ -49,11 +49,15 @@ def build_post_mission(self, aviary_inputs): includes sizing, design, and other non-mission parameters. ''' wing_group = om.Group() - wing_group.add_subsystem("aerostructures", WingWeightSubsys(), - promotes_inputs=["aircraft:*"], - promotes_outputs=[Aircraft.Canard.ASPECT_RATIO, - ('Tail', - Aircraft.Canard.WETTED_AREA_SCALER)]) + wing_group.add_subsystem( + "aerostructures", + WingWeightSubsys(), + promotes_inputs=["aircraft:*"], + promotes_outputs=[ + Aircraft.Canard.ASPECT_RATIO, + ('Tail', Aircraft.Canard.WETTED_AREA_SCALER), + ], + ) return wing_group @@ -71,7 +75,8 @@ def test_post_mission_promotion(self): prob = AviaryProblem() csv_path = get_aviary_resource_path( - 'models/test_aircraft/aircraft_for_bench_GwFm.csv') + 'models/test_aircraft/aircraft_for_bench_GwFm.csv' + ) prob.load_inputs(csv_path, phase_info) # Preprocess inputs @@ -86,7 +91,7 @@ def test_post_mission_promotion(self): # Link phases and variables prob.link_phases() - prob.add_driver("SLSQP", verbosity=Verbosity.QUIET) + prob.add_driver("SLSQP", verbosity=0) prob.add_design_variables() diff --git a/aviary/interface/test/test_linkage_logic.py b/aviary/interface/test/test_linkage_logic.py index 77dceba37..781ef562c 100644 --- a/aviary/interface/test/test_linkage_logic.py +++ b/aviary/interface/test/test_linkage_logic.py @@ -5,7 +5,6 @@ from openmdao.core.problem import _clear_problem_names from aviary.interface.methods_for_level2 import AviaryProblem -from aviary.variable_info.enums import Verbosity @use_tempdirs @@ -133,7 +132,7 @@ def test_linkages(self): # Link phases and variables prob.link_phases() - prob.add_driver('SLSQP', verbosity=Verbosity.QUIET) + prob.add_driver('SLSQP', verbosity=0) prob.add_design_variables() diff --git a/aviary/interface/test/test_reserve_support.py b/aviary/interface/test/test_reserve_support.py index cee15d3ff..8219c7a6a 100644 --- a/aviary/interface/test/test_reserve_support.py +++ b/aviary/interface/test/test_reserve_support.py @@ -11,7 +11,7 @@ @use_tempdirs -class StaticGroupTest(unittest.TestCase): +class PreMissionGroupTest(unittest.TestCase): def test_post_mission_promotion(self): phase_info = deepcopy(ph_in_flops) diff --git a/aviary/mission/flops_based/ode/landing_eom.py b/aviary/mission/flops_based/ode/landing_eom.py index 1a2d7a61c..d1fb8a170 100644 --- a/aviary/mission/flops_based/ode/landing_eom.py +++ b/aviary/mission/flops_based/ode/landing_eom.py @@ -12,8 +12,7 @@ VelocityRate) from aviary.utils.aviary_values import AviaryValues from aviary.variable_info.functions import add_aviary_input -from aviary.variable_info.variables import Aircraft -from aviary.variable_info.variables import Dynamic, Mission +from aviary.variable_info.variables import Aircraft, Dynamic, Mission class FlareEOM(om.Group): diff --git a/aviary/mission/flops_based/ode/landing_ode.py b/aviary/mission/flops_based/ode/landing_ode.py index 1104e9af1..370d57507 100644 --- a/aviary/mission/flops_based/ode/landing_ode.py +++ b/aviary/mission/flops_based/ode/landing_ode.py @@ -6,6 +6,7 @@ FlareODE : the ODE for the flare phase of landing ''' import numpy as np + import openmdao.api as om from aviary.subsystems.atmosphere.atmosphere import Atmosphere @@ -13,9 +14,8 @@ from aviary.mission.flops_based.ode.takeoff_ode import TakeoffODE as _TakeoffODE from aviary.mission.gasp_based.ode.time_integration_base_classes import add_SGM_required_inputs from aviary.utils.aviary_values import AviaryValues -from aviary.utils.functions import set_aviary_initial_values, promote_aircraft_and_mission_vars +from aviary.utils.functions import promote_aircraft_and_mission_vars from aviary.variable_info.variables import Aircraft, Dynamic, Mission -from aviary.variable_info.variables_in import VariablesIn from aviary.variable_info.enums import AnalysisScheme @@ -84,10 +84,6 @@ def setup(self): } add_SGM_required_inputs(self, SGM_required_inputs) - self.add_subsystem( - 'input_port', VariablesIn(aviary_options=aviary_options), - promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem( name='atmosphere', subsys=Atmosphere(num_nodes=nn), promotes=['*'] ) @@ -183,5 +179,4 @@ def setup(self): self.set_input_defaults(Dynamic.Mission.ALTITUDE, np.zeros(nn), 'm') self.set_input_defaults(Dynamic.Mission.VELOCITY, np.zeros(nn), 'm/s') - - set_aviary_initial_values(self, aviary_options) + self.set_input_defaults(Aircraft.Wing.AREA, 1.0, 'm**2') diff --git a/aviary/mission/flops_based/ode/mission_EOM.py b/aviary/mission/flops_based/ode/mission_EOM.py index 353e3758e..6b46628a6 100644 --- a/aviary/mission/flops_based/ode/mission_EOM.py +++ b/aviary/mission/flops_based/ode/mission_EOM.py @@ -1,10 +1,10 @@ import openmdao.api as om from aviary.mission.ode.altitude_rate import AltitudeRate -from aviary.mission.flops_based.ode.range_rate import RangeRate from aviary.mission.ode.specific_energy_rate import SpecificEnergyRate -from aviary.variable_info.variables import Dynamic +from aviary.mission.flops_based.ode.range_rate import RangeRate from aviary.mission.flops_based.ode.required_thrust import RequiredThrust +from aviary.variable_info.variables import Dynamic class MissionEOM(om.Group): @@ -15,23 +15,32 @@ def initialize(self): def setup(self): nn = self.options['num_nodes'] - self.add_subsystem(name='required_thrust', - subsys=RequiredThrust(num_nodes=nn), - promotes_inputs=[Dynamic.Mission.DRAG, Dynamic.Mission.ALTITUDE_RATE, - Dynamic.Mission.VELOCITY, Dynamic.Mission.VELOCITY_RATE, Dynamic.Mission.MASS], - promotes_outputs=['thrust_required']) + self.add_subsystem( + name='required_thrust', + subsys=RequiredThrust(num_nodes=nn), + promotes_inputs=[Dynamic.Mission.DRAG, + Dynamic.Mission.ALTITUDE_RATE, + Dynamic.Mission.VELOCITY, + Dynamic.Mission.VELOCITY_RATE, + Dynamic.Mission.MASS], + promotes_outputs=['thrust_required']) - self.add_subsystem(name='groundspeed', - subsys=RangeRate(num_nodes=nn), - promotes_inputs=[ - Dynamic.Mission.ALTITUDE_RATE, Dynamic.Mission.VELOCITY], - promotes_outputs=[Dynamic.Mission.DISTANCE_RATE]) + self.add_subsystem( + name='groundspeed', + subsys=RangeRate(num_nodes=nn), + promotes_inputs=[ + Dynamic.Mission.ALTITUDE_RATE, + Dynamic.Mission.VELOCITY], + promotes_outputs=[Dynamic.Mission.DISTANCE_RATE]) - self.add_subsystem(name='excess_specific_power', - subsys=SpecificEnergyRate(num_nodes=nn), - promotes_inputs=[(Dynamic.Mission.THRUST_TOTAL, Dynamic.Mission.THRUST_MAX_TOTAL), - Dynamic.Mission.VELOCITY, Dynamic.Mission.MASS, Dynamic.Mission.DRAG], - promotes_outputs=[(Dynamic.Mission.SPECIFIC_ENERGY_RATE, Dynamic.Mission.SPECIFIC_ENERGY_RATE_EXCESS)]) + self.add_subsystem( + name='excess_specific_power', + subsys=SpecificEnergyRate(num_nodes=nn), + promotes_inputs=[ + (Dynamic.Mission.THRUST_TOTAL, Dynamic.Mission.THRUST_MAX_TOTAL), + Dynamic.Mission.VELOCITY, + Dynamic.Mission.MASS, Dynamic.Mission.DRAG], + promotes_outputs=[(Dynamic.Mission.SPECIFIC_ENERGY_RATE, Dynamic.Mission.SPECIFIC_ENERGY_RATE_EXCESS)]) self.add_subsystem( name=Dynamic.Mission.ALTITUDE_RATE_MAX, subsys=AltitudeRate( @@ -42,5 +51,4 @@ def setup(self): Dynamic.Mission.VELOCITY_RATE, Dynamic.Mission.VELOCITY], promotes_outputs=[ - (Dynamic.Mission.ALTITUDE_RATE, - Dynamic.Mission.ALTITUDE_RATE_MAX)]) + (Dynamic.Mission.ALTITUDE_RATE, Dynamic.Mission.ALTITUDE_RATE_MAX)]) diff --git a/aviary/mission/flops_based/ode/mission_ODE.py b/aviary/mission/flops_based/ode/mission_ODE.py index 8d3c094bb..4fb082d5d 100644 --- a/aviary/mission/flops_based/ode/mission_ODE.py +++ b/aviary/mission/flops_based/ode/mission_ODE.py @@ -10,7 +10,6 @@ from aviary.utils.functions import promote_aircraft_and_mission_vars from aviary.variable_info.variable_meta_data import _MetaData from aviary.variable_info.variables import Aircraft, Dynamic, Mission -from aviary.variable_info.variables_in import VariablesIn from aviary.variable_info.enums import AnalysisScheme, ThrottleAllocation, SpeedType @@ -76,14 +75,6 @@ def setup(self): } add_SGM_required_inputs(self, SGM_required_inputs) - self.add_subsystem( - 'input_port', - VariablesIn(aviary_options=aviary_options, - meta_data=self.options['meta_data'], - context='mission'), - promotes_inputs=['*'], - promotes_outputs=['*']) - self.add_subsystem( name='atmosphere', subsys=Atmosphere(num_nodes=nn, input_speed_type=SpeedType.MACH), diff --git a/aviary/mission/flops_based/ode/takeoff_ode.py b/aviary/mission/flops_based/ode/takeoff_ode.py index 85674f5b9..cb59311e6 100644 --- a/aviary/mission/flops_based/ode/takeoff_ode.py +++ b/aviary/mission/flops_based/ode/takeoff_ode.py @@ -8,9 +8,8 @@ from aviary.mission.flops_based.ode.takeoff_eom import StallSpeed, TakeoffEOM from aviary.mission.gasp_based.ode.time_integration_base_classes import add_SGM_required_inputs from aviary.utils.aviary_values import AviaryValues -from aviary.utils.functions import set_aviary_initial_values, promote_aircraft_and_mission_vars +from aviary.utils.functions import promote_aircraft_and_mission_vars from aviary.variable_info.variables import Aircraft, Dynamic, Mission -from aviary.variable_info.variables_in import VariablesIn from aviary.variable_info.enums import AnalysisScheme @@ -82,10 +81,6 @@ def setup(self): } add_SGM_required_inputs(self, SGM_required_inputs) - self.add_subsystem( - 'input_port', VariablesIn(aviary_options=aviary_options), - promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem( name='atmosphere', subsys=Atmosphere(num_nodes=nn), promotes=['*'] ) @@ -177,6 +172,4 @@ def setup(self): self.set_input_defaults(Dynamic.Mission.ALTITUDE, np.zeros(nn), 'm') self.set_input_defaults(Dynamic.Mission.VELOCITY, np.zeros(nn), 'm/s') - self.set_input_defaults(Mission.Summary.GROSS_MASS, val=1.0, units='kg') - - set_aviary_initial_values(self, aviary_options) + self.set_input_defaults(Aircraft.Wing.AREA, 1.0, 'm**2') diff --git a/aviary/mission/flops_based/ode/test/test_landing_eom.py b/aviary/mission/flops_based/ode/test/test_landing_eom.py index ef4903f4e..d03302463 100644 --- a/aviary/mission/flops_based/ode/test/test_landing_eom.py +++ b/aviary/mission/flops_based/ode/test/test_landing_eom.py @@ -1,17 +1,26 @@ import unittest +import numpy as np import openmdao.api as om +from openmdao.utils.assert_utils import (assert_check_partials, + assert_near_equal) -from aviary.mission.flops_based.ode.landing_eom import FlareEOM +from aviary.mission.flops_based.ode.landing_eom import ( + FlareEOM, GlideSlopeForces, FlareSumForces, GroundSumForces) from aviary.models.N3CC.N3CC_data import ( detailed_landing_flare, inputs) +from aviary.utils.test_utils.variable_test import assert_match_varnames from aviary.validation_cases.validation_tests import do_validation_test from aviary.variable_info.variables import Dynamic class FlareEOMTest(unittest.TestCase): - def test_case(self): - prob = om.Problem() + """ + Test against data of detailed_landing_flare from models/N3CC/N3CC_data.py + """ + + def setUp(self): + prob = self.prob = om.Problem() time, _ = detailed_landing_flare.get_item('time') nn = len(time) @@ -25,8 +34,10 @@ def test_case(self): prob.setup(check=False, force_alloc_complex=True) + def test_case(self): + do_validation_test( - prob, + self.prob, 'landing_flare_eom', input_validation_data=detailed_landing_flare, output_validation_data=detailed_landing_flare, @@ -43,6 +54,151 @@ def test_case(self): Dynamic.Mission.ALTITUDE_RATE], tol=1e-2, atol=1e-8, rtol=5e-10) + def test_IO(self): + exclude_inputs = { + 'angle_of_attack', 'acceleration_vertical', + 'forces_vertical', 'angle_of_attack_rate', + 'acceleration_horizontal', 'forces_horizontal'} + exclude_outputs = { + 'forces_vertical', 'acceleration_horizontal', + 'forces_perpendicular', 'acceleration_vertical', + 'net_alpha_rate', 'forces_horizontal', 'required_thrust' + } + assert_match_varnames(self.prob.model, + exclude_inputs=exclude_inputs, + exclude_outputs=exclude_outputs) + + +class OtherTest(unittest.TestCase): + + def test_GlideSlopeForces(self): + """ + test on single component GlideSlopeForces + """ + + tol = 1e-6 + aviary_options = inputs + prob = om.Problem() + + # use data from detailed_landing_flare in models/N3CC/N3CC_data.py + prob.model.add_subsystem( + "glide", GlideSlopeForces(num_nodes=2, aviary_options=aviary_options), promotes=["*"] + ) + prob.model.set_input_defaults( + Dynamic.Mission.MASS, np.array([106292, 106292]), units="lbm" + ) + prob.model.set_input_defaults( + Dynamic.Mission.DRAG, np.array([47447.13138523, 44343.01567596]), units="N" + ) + prob.model.set_input_defaults( + Dynamic.Mission.LIFT, np.array([482117.47027692, 568511.57097785]), units="N" + ) + prob.model.set_input_defaults( + "angle_of_attack", np.array([5.086, 6.834]), units="deg" + ) + prob.model.set_input_defaults( + Dynamic.Mission.FLIGHT_PATH_ANGLE, np.array([-3.0, -2.47]), units="deg" + ) + prob.setup(check=False, force_alloc_complex=True) + prob.run_model() + + assert_near_equal( + prob["forces_perpendicular"], np.array( + [135087.0, 832087.6]), tol + ) + assert_near_equal( + prob["required_thrust"], np.array( + [-44751.64, -391905.6]), tol + ) + + partial_data = prob.check_partials(out_stream=None, method="cs") + assert_check_partials(partial_data, atol=1e-8, rtol=1e-12) + + def test_FlareSumForces(self): + """ + test on single component FlareSumForces + """ + + tol = 1e-6 + aviary_options = inputs + prob = om.Problem() + prob.model.add_subsystem( + "flare", FlareSumForces(num_nodes=2, aviary_options=aviary_options), promotes=["*"] + ) + + # use data from detailed_landing_flare in models/N3CC/N3CC_data.py + prob.model.set_input_defaults( + Dynamic.Mission.MASS, np.array([106292, 106292]), units="lbm" + ) + prob.model.set_input_defaults( + Dynamic.Mission.DRAG, np.array([47447.13138523, 44343.01567596]), units="N" + ) + prob.model.set_input_defaults( + Dynamic.Mission.LIFT, np.array([482117.47027692, 568511.57097785]), units="N" + ) + prob.model.set_input_defaults( + Dynamic.Mission.THRUST_TOTAL, np.array([4980.3, 4102]), units="N" + ) + prob.model.set_input_defaults( + "angle_of_attack", np.array([5.086, 6.834]), units="deg" + ) + prob.model.set_input_defaults( + Dynamic.Mission.FLIGHT_PATH_ANGLE, np.array([-3., -2.47]), units="deg" + ) + prob.setup(check=False, force_alloc_complex=True) + prob.run_model() + + assert_near_equal( + prob["forces_horizontal"], np.array( + [17173.03, 15710.98]), tol + ) + assert_near_equal( + prob["forces_vertical"], np.array( + [11310.84, 97396.16]), tol + ) + + partial_data = prob.check_partials(out_stream=None, method="cs") + assert_check_partials(partial_data, atol=1e-9, rtol=1e-12) + + def test_GroundSumForces(self): + """ + test on single component GroundSumForces + """ + + tol = 1e-6 + prob = om.Problem() + prob.model.add_subsystem( + "ground", GroundSumForces(num_nodes=2, friction_coefficient=0.025), promotes=["*"] + ) + + # use data from detailed_landing_flare in models/N3CC/N3CC_data.py + prob.model.set_input_defaults( + Dynamic.Mission.MASS, np.array([106292, 106292]), units="lbm" + ) + prob.model.set_input_defaults( + Dynamic.Mission.DRAG, np.array([47447.13138523, 44343.01567596]), units="N" + ) + prob.model.set_input_defaults( + Dynamic.Mission.LIFT, np.array([482117.47027692, 568511.57097785]), units="N" + ) + prob.model.set_input_defaults( + Dynamic.Mission.THRUST_TOTAL, np.array([4980.3, 4102]), units="N" + ) + prob.setup(check=False, force_alloc_complex=True) + prob.run_model() + + assert_near_equal( + prob["forces_horizontal"], np.array( + [42466.83, 40241.02]), tol + ) + assert_near_equal( + prob["forces_vertical"], np.array( + [9307.098, 95701.199]), tol + ) + + partial_data = prob.check_partials(out_stream=None, method="cs") + assert_check_partials(partial_data, atol=1e-12, rtol=1e-12) + if __name__ == "__main__": unittest.main() diff --git a/aviary/mission/flops_based/ode/test/test_landing_ode.py b/aviary/mission/flops_based/ode/test/test_landing_ode.py index 1b1845342..0c863e6d5 100644 --- a/aviary/mission/flops_based/ode/test/test_landing_ode.py +++ b/aviary/mission/flops_based/ode/test/test_landing_ode.py @@ -3,15 +3,21 @@ import openmdao.api as om from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.aviary_values import get_items +from aviary.utils.functions import set_aviary_initial_values from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems from aviary.mission.flops_based.ode.landing_ode import FlareODE from aviary.models.N3CC.N3CC_data import ( detailed_landing_flare, inputs, landing_subsystem_options) from aviary.validation_cases.validation_tests import do_validation_test -from aviary.variable_info.variables import Dynamic +from aviary.variable_info.variables import Dynamic, Aircraft class FlareODETest(unittest.TestCase): + """ + Test against data of detailed_landing_flare from models/N3CC/N3CC_data.py + """ + def test_case(self): prob = om.Problem() time, _ = detailed_landing_flare.get_item('time') @@ -31,8 +37,12 @@ def test_case(self): promotes_inputs=['*'], promotes_outputs=['*']) + prob.model.set_input_defaults(Aircraft.Wing.AREA, val=1.0, units='ft**2') + prob.setup(check=False, force_alloc_complex=True) + set_aviary_initial_values(prob, aviary_options) + do_validation_test( prob, 'landing_flare_ode', diff --git a/aviary/mission/flops_based/ode/test/test_mission_eom.py b/aviary/mission/flops_based/ode/test/test_mission_eom.py new file mode 100644 index 000000000..3e39c527c --- /dev/null +++ b/aviary/mission/flops_based/ode/test/test_mission_eom.py @@ -0,0 +1,58 @@ +import unittest + +import numpy as np +import openmdao.api as om +from openmdao.utils.assert_utils import (assert_check_partials, + assert_near_equal) + +from aviary.mission.flops_based.ode.mission_EOM import MissionEOM +from aviary.utils.test_utils.variable_test import assert_match_varnames +from aviary.variable_info.variables import Dynamic + + +class MissionEOMTest(unittest.TestCase): + def setUp(self): + self.prob = prob = om.Problem() + prob.model.add_subsystem( + "mission", MissionEOM(num_nodes=3), promotes=["*"] + ) + prob.model.set_input_defaults( + Dynamic.Mission.MASS, np.array([81796.1389890711, 74616.9849763798, 65193.7423491884]), units="kg" + ) + prob.model.set_input_defaults( + Dynamic.Mission.DRAG, np.array([9978.32211087097, 8769.90342254821, 7235.03338269778]), units="lbf" + ) + prob.model.set_input_defaults( + Dynamic.Mission.ALTITUDE_RATE, np.array([29.8463233754212, -5.69941245767868E-09, -4.32644785970493]), units="ft/s" + ) + prob.model.set_input_defaults( + Dynamic.Mission.VELOCITY_RATE, np.array([0.558739800813549, 3.33665416459715E-17, -0.38372209277242]), units="m/s**2" + ) + prob.model.set_input_defaults( + Dynamic.Mission.VELOCITY, np.array([164.029012458452, 232.775306059091, 117.638805929526]), units="m/s" + ) + prob.model.set_input_defaults( + Dynamic.Mission.THRUST_MAX_TOTAL, np.array([40799.6009633346, 11500.32, 42308.2709683461]), units="lbf" + ) + prob.setup(check=False, force_alloc_complex=True) + + def test_case(self): + """ + test on mission EOM using data from validation_cases/validation_data/flops_data/full_mission_test_data.py + """ + + tol = 1e-6 + self.prob.run_model() + + assert_near_equal(self.prob.get_val(Dynamic.Mission.ALTITUDE_RATE_MAX, units='ft/min'), + np.array([3679.0525544843, 760.55416759, 6557.07891846677]), tol) + + partial_data = self.prob.check_partials(out_stream=None, method="cs") + assert_check_partials(partial_data, atol=1e-8, rtol=1e-12) + + def test_IO(self): + assert_match_varnames(self.prob.model, exclude_outputs={'thrust_required'}) + + +if __name__ == "__main__": + unittest.main() diff --git a/aviary/mission/flops_based/ode/test/test_range_rate.py b/aviary/mission/flops_based/ode/test/test_range_rate.py index e371e2212..3d6d3ab2a 100644 --- a/aviary/mission/flops_based/ode/test/test_range_rate.py +++ b/aviary/mission/flops_based/ode/test/test_range_rate.py @@ -12,6 +12,10 @@ class RangeRateTest(unittest.TestCase): def setUp(self): + """ + test using data from validation_cases/validation_data/flops_data/full_mission_test_data.py + """ + prob = self.prob = om.Problem() time, _ = data.get_item('time') diff --git a/aviary/mission/flops_based/ode/test/test_required_thrust.py b/aviary/mission/flops_based/ode/test/test_required_thrust.py new file mode 100644 index 000000000..c8f760e3b --- /dev/null +++ b/aviary/mission/flops_based/ode/test/test_required_thrust.py @@ -0,0 +1,56 @@ +import unittest + +import numpy as np +import openmdao.api as om +from openmdao.utils.assert_utils import (assert_check_partials, + assert_near_equal) + +from aviary.mission.flops_based.ode.required_thrust import RequiredThrust +from aviary.utils.test_utils.variable_test import assert_match_varnames +from aviary.variable_info.variables import Dynamic + + +class RequiredThrustTest(unittest.TestCase): + + def setUp(self): + prob = self.prob = om.Problem() + prob.model.add_subsystem( + "req_thrust", RequiredThrust(num_nodes=2), promotes=["*"] + ) + prob.model.set_input_defaults( + Dynamic.Mission.DRAG, np.array([47447.13138523, 44343.01567596]), units="N" + ) + prob.model.set_input_defaults( + Dynamic.Mission.MASS, np.array([106292, 106292]), units="lbm" + ) + prob.model.set_input_defaults( + Dynamic.Mission.ALTITUDE_RATE, np.array([1.72, 11.91]), units="m/s" + ) + prob.model.set_input_defaults( + Dynamic.Mission.VELOCITY_RATE, np.array([5.23, 2.7]), units="m/s**2" + ) + prob.model.set_input_defaults( + Dynamic.Mission.VELOCITY, np.array([160.99, 166.68]), units="m/s" + ) + + prob.setup(check=False, force_alloc_complex=True) + + def test_case(self): + + tol = 1e-6 + self.prob.run_model() + + assert_near_equal( + self.prob["thrust_required"], + np.array([304653.8, 208303.1]), tol + ) + + partial_data = self.prob.check_partials(out_stream=None, method="cs") + assert_check_partials(partial_data, atol=1e-10, rtol=1e-10) + + def test_IO(self): + assert_match_varnames(self.prob.model, exclude_outputs={'thrust_required'}) + + +if __name__ == "__main__": + unittest.main() diff --git a/aviary/mission/flops_based/ode/test/test_takeoff_eom.py b/aviary/mission/flops_based/ode/test/test_takeoff_eom.py index f92e04771..6c592f410 100644 --- a/aviary/mission/flops_based/ode/test/test_takeoff_eom.py +++ b/aviary/mission/flops_based/ode/test/test_takeoff_eom.py @@ -1,10 +1,16 @@ import unittest +import numpy as np import openmdao.api as om +from openmdao.utils.assert_utils import (assert_check_partials, + assert_near_equal) -from aviary.mission.flops_based.ode.takeoff_eom import TakeoffEOM +from aviary.mission.flops_based.ode.takeoff_eom import ( + TakeoffEOM, StallSpeed, DistanceRates, Accelerations, VelocityRate, + FlightPathAngleRate, SumForces, ClimbGradientForces) from aviary.models.N3CC.N3CC_data import ( detailed_takeoff_climbing, detailed_takeoff_ground, inputs) +from aviary.utils.test_utils.variable_test import assert_match_varnames from aviary.validation_cases.validation_tests import do_validation_test from aviary.variable_info.variables import Dynamic, Mission @@ -76,6 +82,316 @@ def _make_prob(climbing): return prob + def test_IO(self): + prob = self._make_prob(climbing=False) + exclude_inputs = { + 'angle_of_attack', 'acceleration_horizontal', + 'acceleration_vertical', 'forces_vertical', 'forces_horizontal'} + exclude_outputs = { + 'acceleration_horizontal', 'acceleration_vertical', + 'climb_gradient_forces_vertical', 'forces_horizontal', + 'forces_vertical', 'climb_gradient_forces_horizontal'} + assert_match_varnames(prob.model, + exclude_inputs=exclude_inputs, + exclude_outputs=exclude_outputs) + + def test_StallSpeed(self): + tol = 1e-6 + prob = om.Problem() + prob.model.add_subsystem( + "stall_speed", StallSpeed(num_nodes=2), promotes=["*"] + ) + prob.model.set_input_defaults( + Dynamic.Mission.DENSITY, np.array([1, 2]), units="kg/m**3" + ) + prob.model.set_input_defaults( + "area", 10, units="m**2" + ) + prob.model.set_input_defaults( + "lift_coefficient_max", 5000, units="unitless" + ) + + prob.setup(check=False, force_alloc_complex=True) + prob.run_model() + + assert_near_equal( + prob["stall_speed"], np.array( + [0.01980571, 0.01400475]), tol + ) + + partial_data = prob.check_partials(out_stream=None, method="cs") + assert_check_partials(partial_data, atol=1e-12, rtol=1e-12) + + def test_DistanceRates_1(self): + """ + climbing = True + """ + + tol = 1e-6 + prob = om.Problem() + prob.model.add_subsystem( + "dist_rates", DistanceRates(num_nodes=2, climbing=True), promotes=["*"] + ) + prob.model.set_input_defaults( + Dynamic.Mission.FLIGHT_PATH_ANGLE, np.array([0.612, 4.096]), units="rad" + ) + prob.model.set_input_defaults( + Dynamic.Mission.VELOCITY, np.array([5.23, 2.7]), units="m/s" + ) + + prob.setup(check=False, force_alloc_complex=True) + prob.run_model() + + assert_near_equal( + prob[Dynamic.Mission.DISTANCE_RATE], np.array( + [4.280758, -1.56085]), tol + ) + assert_near_equal( + prob[Dynamic.Mission.ALTITUDE_RATE], np.array( + [3.004664, -2.203122]), tol + ) + + partial_data = prob.check_partials(out_stream=None, method="cs") + assert_check_partials(partial_data, atol=1e-12, rtol=1e-12) + + def test_DistanceRates_2(self): + """ + climbing = False + """ + + tol = 1e-6 + prob = om.Problem() + prob.model.add_subsystem( + "dist_rates", DistanceRates(num_nodes=2, climbing=False), promotes=["*"] + ) + prob.model.set_input_defaults( + Dynamic.Mission.FLIGHT_PATH_ANGLE, np.array([0.0, 0.0]), units="rad" + ) + prob.model.set_input_defaults( + Dynamic.Mission.VELOCITY, np.array([1.0, 2.0]), units="m/s" + ) + + prob.setup(check=False, force_alloc_complex=True) + prob.run_model() + + assert_near_equal( + prob[Dynamic.Mission.DISTANCE_RATE], np.array([1.0, 2.0]), tol) + assert_near_equal( + prob[Dynamic.Mission.ALTITUDE_RATE], np.array([0.0, 0.0]), tol + ) + + partial_data = prob.check_partials(out_stream=None, method="cs") + assert_check_partials(partial_data, atol=1e-12, rtol=1e-12) + + def test_Accelerations(self): + tol = 1e-6 + prob = om.Problem() + prob.model.add_subsystem( + "acceleration", Accelerations(num_nodes=2), promotes=["*"] + ) + prob.model.set_input_defaults( + "forces_horizontal", [100.0, 200.0], units="N" + ) + prob.model.set_input_defaults( + "forces_vertical", [50.0, 100.0], units="N" + ) + + prob.setup(check=False, force_alloc_complex=True) + prob.run_model() + + assert_near_equal( + prob["acceleration_horizontal"], np.array( + [100.0, 200.0]), tol + ) + assert_near_equal( + prob["acceleration_vertical"], np.array( + [50.0, 100.0]), tol + ) + + partial_data = prob.check_partials(out_stream=None, method="cs") + assert_check_partials(partial_data, atol=1e-12, rtol=1e-12) + + def test_VelocityRate(self): + tol = 1e-6 + prob = om.Problem() + prob.model.add_subsystem( + "vel_rate", VelocityRate(num_nodes=2), promotes=["*"] + ) + prob.model.set_input_defaults( + "acceleration_horizontal", [100.0, 200.0], units="m/s**2" + ) + prob.model.set_input_defaults( + "acceleration_vertical", [50.0, 100.0], units="m/s**2" + ) + prob.model.set_input_defaults( + Dynamic.Mission.DISTANCE_RATE, [160.98, 166.25], units="m/s" + ) + prob.model.set_input_defaults( + Dynamic.Mission.ALTITUDE_RATE, [1.72, 11.91], units="m/s" + ) + + prob.setup(check=False, force_alloc_complex=True) + prob.run_model() + + assert_near_equal( + prob[Dynamic.Mission.VELOCITY_RATE], np.array( + [100.5284, 206.6343]), tol + ) + + partial_data = prob.check_partials(out_stream=None, method="cs") + assert_check_partials(partial_data, atol=1e-12, rtol=1e-12) + + def test_FlightPathAngleRate(self): + tol = 1e-6 + prob = om.Problem() + prob.model.add_subsystem( + "gamma_rate", FlightPathAngleRate(num_nodes=2), promotes=["*"] + ) + prob.model.set_input_defaults( + "acceleration_horizontal", [100.0, 200.0], units="m/s**2" + ) + prob.model.set_input_defaults( + "acceleration_vertical", [50.0, 100.0], units="m/s**2" + ) + prob.model.set_input_defaults( + Dynamic.Mission.DISTANCE_RATE, [160.98, 166.25], units="m/s" + ) + prob.model.set_input_defaults( + Dynamic.Mission.ALTITUDE_RATE, [1.72, 11.91], units="m/s" + ) + + prob.setup(check=False, force_alloc_complex=True) + prob.run_model() + + assert_near_equal( + prob[Dynamic.Mission.FLIGHT_PATH_ANGLE_RATE], np.array( + [0.3039257, 0.51269018]), tol + ) + + partial_data = prob.check_partials(out_stream=None, method="cs") + assert_check_partials(partial_data, atol=1e-12, rtol=1e-12) + + def test_SumForcese_1(self): + """ + climbing = True + """ + + tol = 1e-6 + prob = om.Problem() + aviary_options = inputs + prob.model.add_subsystem( + "sum1", SumForces(num_nodes=2, climbing=True, aviary_options=aviary_options), promotes=["*"] + ) + prob.model.set_input_defaults( + Dynamic.Mission.MASS, np.array([106292, 106292]), units="lbm" + ) + prob.model.set_input_defaults( + Dynamic.Mission.DRAG, np.array([47447.13138523, 44343.01567596]), units="N" + ) + prob.model.set_input_defaults( + Dynamic.Mission.LIFT, np.array([482117.47027692, 568511.57097785]), units="N" + ) + prob.model.set_input_defaults( + Dynamic.Mission.THRUST_TOTAL, np.array([4980.3, 4102]), units="N" + ) + + prob.setup(check=False, force_alloc_complex=True) + prob.run_model() + + assert_near_equal( + prob["forces_horizontal"], np.array( + [-42466.83, -40241.02]), tol + ) + assert_near_equal( + prob["forces_vertical"], np.array( + [9307.0983, 95701.1990]), tol + ) + + partial_data = prob.check_partials(out_stream=None, method="cs") + assert_check_partials(partial_data, atol=1e-10, rtol=1e-10) + + def test_SumForcese_2(self): + """ + climbing = False + """ + + tol = 1e-6 + prob = om.Problem() + aviary_options = inputs + prob.model.add_subsystem( + "sum2", SumForces(num_nodes=2, climbing=False, aviary_options=aviary_options), promotes=["*"] + ) + prob.model.set_input_defaults( + Dynamic.Mission.MASS, np.array([106292, 106292]), units="lbm" + ) + prob.model.set_input_defaults( + Dynamic.Mission.DRAG, np.array([47447.13138523, 44343.01567596]), units="N" + ) + prob.model.set_input_defaults( + Dynamic.Mission.LIFT, np.array([482117.47027692, 568511.57097785]), units="N" + ) + prob.model.set_input_defaults( + Dynamic.Mission.THRUST_TOTAL, np.array([4980.3, 4102]), units="N" + ) + + prob.setup(check=False, force_alloc_complex=True) + prob.run_model() + + assert_near_equal( + prob["forces_horizontal"], np.array([-42234.154, -37848.486]), tol + ) + assert_near_equal( + prob["forces_vertical"], np.array([0.0, 0.0]), tol + ) + + partial_data = prob.check_partials(out_stream=None, method="cs") + assert_check_partials(partial_data, atol=1e-10, rtol=1e-10) + + def test_ClimbGradientForces(self): + """ + climbing = False + """ + + tol = 1e-6 + prob = om.Problem() + aviary_options = inputs + prob.model.add_subsystem( + "climb_grad", ClimbGradientForces(num_nodes=2, aviary_options=aviary_options), promotes=["*"] + ) + prob.model.set_input_defaults( + Dynamic.Mission.MASS, np.array([106292, 106292]), units="lbm" + ) + prob.model.set_input_defaults( + Dynamic.Mission.DRAG, np.array([47447.13138523, 44343.01567596]), units="N" + ) + prob.model.set_input_defaults( + Dynamic.Mission.LIFT, np.array([482117.47027692, 568511.57097785]), units="N" + ) + prob.model.set_input_defaults( + Dynamic.Mission.THRUST_TOTAL, np.array([4980.3, 4102]), units="N" + ) + prob.model.set_input_defaults( + Dynamic.Mission.FLIGHT_PATH_ANGLE, np.array([0.612, 4.096]), units="rad" + ) + prob.model.set_input_defaults( + "angle_of_attack", np.array([5.086, 6.834]), units="rad" + ) + + prob.setup(check=False, force_alloc_complex=True) + prob.run_model() + + assert_near_equal( + prob["climb_gradient_forces_horizontal"], + np.array([-317261.63, 344951.97]), tol + ) + assert_near_equal( + prob["climb_gradient_forces_vertical"], + np.array([90485.14, 843986.59]), tol + ) + + partial_data = prob.check_partials(out_stream=None, method="cs") + assert_check_partials(partial_data, atol=1e-10, rtol=1e-10) + if __name__ == "__main__": unittest.main() diff --git a/aviary/mission/flops_based/ode/test/test_takeoff_ode.py b/aviary/mission/flops_based/ode/test/test_takeoff_ode.py index a7c0c9cf7..1dd0cc762 100644 --- a/aviary/mission/flops_based/ode/test/test_takeoff_ode.py +++ b/aviary/mission/flops_based/ode/test/test_takeoff_ode.py @@ -4,12 +4,13 @@ import openmdao.api as om from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.functions import set_aviary_initial_values from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems from aviary.mission.flops_based.ode.takeoff_ode import TakeoffODE from aviary.models.N3CC.N3CC_data import ( detailed_takeoff_climbing, detailed_takeoff_ground, takeoff_subsystem_options, inputs) from aviary.validation_cases.validation_tests import do_validation_test -from aviary.variable_info.variables import Dynamic, Mission +from aviary.variable_info.variables import Dynamic, Mission, Aircraft takeoff_subsystem_options = deepcopy(takeoff_subsystem_options) @@ -86,8 +87,12 @@ def _make_prob(climbing): promotes_inputs=['*'], promotes_outputs=['*']) + prob.model.set_input_defaults(Aircraft.Wing.AREA, val=1.0, units='ft**2') + prob.setup(check=False, force_alloc_complex=True) + set_aviary_initial_values(prob, aviary_options) + return prob diff --git a/aviary/mission/flops_based/phases/build_landing.py b/aviary/mission/flops_based/phases/build_landing.py index ece3fb2b5..47c1dbcc3 100644 --- a/aviary/mission/flops_based/phases/build_landing.py +++ b/aviary/mission/flops_based/phases/build_landing.py @@ -64,22 +64,6 @@ def build_phase(self, use_detailed=False): Aircraft.Wing.AREA, val=self.ref_wing_area, units="ft**2" ) landing.set_input_defaults( - Mission.Landing.LIFT_COEFFICIENT_MAX, val=self.Cl_max_ldg, - units='unitless') + Mission.Landing.LIFT_COEFFICIENT_MAX, val=self.Cl_max_ldg, units='unitless') return landing - - -if __name__ == "__main__": - landing_options = Landing( - ref_wing_area=1370.0, # ft**2 - Cl_max_ldg=3, # no units - ) - - landing = landing_options.build_phase(False) - - prob = om.Problem() - prob.model = landing - prob.setup(force_alloc_complex=True) - prob.run_model() - prob.check_partials(method="cs", compact_print=True) diff --git a/aviary/mission/flops_based/phases/build_takeoff.py b/aviary/mission/flops_based/phases/build_takeoff.py index 7a8def8d3..10a31962f 100644 --- a/aviary/mission/flops_based/phases/build_takeoff.py +++ b/aviary/mission/flops_based/phases/build_takeoff.py @@ -51,7 +51,7 @@ def build_phase(self, use_detailed=False): a group in OpenMDAO """ - if use_detailed: + if use_detailed: # TODO raise om.AnalysisError( "Must set takeoff method to `use_detailed=False`, detailed takeoff is" " not currently enabled." diff --git a/aviary/mission/flops_based/phases/detailed_landing_phases.py b/aviary/mission/flops_based/phases/detailed_landing_phases.py index 68a285e9f..4dffa24e7 100644 --- a/aviary/mission/flops_based/phases/detailed_landing_phases.py +++ b/aviary/mission/flops_based/phases/detailed_landing_phases.py @@ -34,9 +34,12 @@ _init_initial_guess_meta_data from aviary.mission.phase_builder_base import PhaseBuilderBase from aviary.mission.initial_guess_builders import InitialGuessControl, InitialGuessParameter, InitialGuessPolynomialControl, InitialGuessState, InitialGuessIntegrationVariable +from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder from aviary.utils.aviary_values import AviaryValues +from aviary.variable_info.enums import LegacyCode from aviary.variable_info.functions import setup_trajectory_params from aviary.variable_info.variables import Dynamic, Mission +from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData @_init_initial_guess_meta_data @@ -1248,7 +1251,32 @@ def build_trajectory( if model is not None: phase_names = self.get_phase_names() - setup_trajectory_params(model, traj, aviary_options, phase_names) + # This is a level 3 method that uses the default subsystems. + # We need to create parameters for just the inputs we have. + # They mostly come from the low-speed aero subsystem. + + aero = CoreAerodynamicsBuilder('core_aerodynamics', + BaseMetaData, + LegacyCode('FLOPS')) + + phase_info = {'subsystem_options': { + 'core_aerodynamics': {'method': 'low_speed'}} + } + + params = aero.get_parameters(aviary_options, phase_info) + + # takeoff introduces this one. + params[Mission.Landing.LIFT_COEFFICIENT_MAX] = { + 'shape': (1, ), + 'static_target': True, + } + + ext_params = {} + for phase in self._phases.keys(): + ext_params[phase] = params + + setup_trajectory_params(model, traj, aviary_options, + phase_names, external_parameters=ext_params) return traj diff --git a/aviary/mission/flops_based/phases/detailed_takeoff_phases.py b/aviary/mission/flops_based/phases/detailed_takeoff_phases.py index 103587173..5c5d716c9 100644 --- a/aviary/mission/flops_based/phases/detailed_takeoff_phases.py +++ b/aviary/mission/flops_based/phases/detailed_takeoff_phases.py @@ -48,9 +48,12 @@ from aviary.mission.flops_based.ode.takeoff_ode import TakeoffODE from aviary.mission.phase_builder_base import PhaseBuilderBase from aviary.mission.initial_guess_builders import InitialGuessControl, InitialGuessParameter, InitialGuessPolynomialControl, InitialGuessState, InitialGuessIntegrationVariable +from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder from aviary.utils.aviary_values import AviaryValues +from aviary.variable_info.enums import LegacyCode from aviary.variable_info.functions import setup_trajectory_params from aviary.variable_info.variables import Dynamic, Mission +from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData def _init_initial_guess_meta_data(cls: PhaseBuilderBase): @@ -2405,7 +2408,33 @@ def build_trajectory( if model is not None: phase_names = self.get_phase_names() - setup_trajectory_params(model, traj, aviary_options, phase_names) + # This is a level 3 method that uses the default subsystems. + # We need to create parameters for just the inputs we have. + # They mostly come from the low-speed aero subsystem. + + aero = CoreAerodynamicsBuilder('core_aerodynamics', + BaseMetaData, + LegacyCode('FLOPS')) + + phase_info = {} + phase_info['subsystem_options'] = {} + phase_info['subsystem_options']['core_aerodynamics'] = {} + phase_info['subsystem_options']['core_aerodynamics']['method'] = 'low_speed' + + params = aero.get_parameters(aviary_options, phase_info) + + # takeoff introduces this one. + params[Mission.Takeoff.LIFT_COEFFICIENT_MAX] = { + 'shape': (1, ), + 'static_target': True, + } + + ext_params = {} + for phase in self._phases.keys(): + ext_params[phase] = params + + setup_trajectory_params(model, traj, aviary_options, + phase_names, external_parameters=ext_params) return traj diff --git a/aviary/mission/flops_based/phases/test/test_building_landing.py b/aviary/mission/flops_based/phases/test/test_building_landing.py new file mode 100644 index 000000000..903221a34 --- /dev/null +++ b/aviary/mission/flops_based/phases/test/test_building_landing.py @@ -0,0 +1,37 @@ +import unittest + +import openmdao.api as om +from openmdao.utils.assert_utils import assert_check_partials, assert_near_equal + +from aviary.mission.flops_based.phases.build_landing import Landing +from aviary.variable_info.variables import Mission + + +class LandingPhaseTest(unittest.TestCase): + def test_case1(self): + landing_options = Landing( + ref_wing_area=1370.0, # ft**2 + Cl_max_ldg=3, # no units + ) + + use_detailed = False + landing = landing_options.build_phase(use_detailed=use_detailed) + + prob = om.Problem() + prob.model = landing + prob.setup(force_alloc_complex=True) + prob.run_model() + partial_data = prob.check_partials( + out_stream=None, method="cs", compact_print=False, excludes=["*atmosphere*"]) + assert_check_partials(partial_data, atol=1e-12, rtol=1e-12) + + tol = 1e-6 + assert_near_equal( + prob[Mission.Landing.GROUND_DISTANCE], 6331.781, tol + ) + assert_near_equal( + prob[Mission.Landing.INITIAL_VELOCITY], 134.9752, tol) + + +if __name__ == "__main__": + unittest.main() diff --git a/aviary/mission/flops_based/phases/test/test_building_takeoff.py b/aviary/mission/flops_based/phases/test/test_building_takeoff.py new file mode 100644 index 000000000..a8ac32bb1 --- /dev/null +++ b/aviary/mission/flops_based/phases/test/test_building_takeoff.py @@ -0,0 +1,40 @@ +import unittest +import numpy as np + +import openmdao.api as om +from openmdao.utils.assert_utils import assert_check_partials, assert_near_equal + +from aviary.mission.flops_based.phases.build_takeoff import Takeoff +from aviary.variable_info.variables import Aircraft, Mission + + +class TakeoffPhaseTest(unittest.TestCase): + def test_case1(self): + takeoff_options = Takeoff( + airport_altitude=0, # ft + ramp_mass=181200.0, # lbm + num_engines=2, # no units + ) + + use_detailed = False + takeoff = takeoff_options.build_phase(use_detailed=use_detailed) + + prob = om.Problem() + prob.model = takeoff + prob.model.set_input_defaults( + Aircraft.Wing.AREA, 1370.3, units="ft**2" + ) + prob.setup(force_alloc_complex=True) + prob.run_model() + partial_data = prob.check_partials( + out_stream=None, method="cs", compact_print=False, excludes=["*atmosphere*"]) + assert_check_partials(partial_data, atol=1e-12, rtol=1e-12) + + tol = 1e-6 + assert_near_equal( + prob[Mission.Takeoff.GROUND_DISTANCE], 2811.442, tol + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/aviary/mission/flops_based/phases/test/test_time_integration_phases.py b/aviary/mission/flops_based/phases/test/test_time_integration_phases.py index daec19787..8edb218f4 100644 --- a/aviary/mission/flops_based/phases/test/test_time_integration_phases.py +++ b/aviary/mission/flops_based/phases/test/test_time_integration_phases.py @@ -6,10 +6,8 @@ from aviary.mission.flops_based.phases.time_integration_phases import \ SGMHeightEnergy, SGMDetailedTakeoff, SGMDetailedLanding from aviary.subsystems.premission import CorePreMission -from aviary.utils.functions import set_aviary_initial_values from aviary.variable_info.enums import EquationsOfMotion from aviary.variable_info.variables import Aircraft, Dynamic, Mission, Settings -from aviary.variable_info.variables_in import VariablesIn from aviary.interface.default_phase_info.height_energy_fiti import add_default_sgm_args from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems @@ -104,19 +102,8 @@ def setup_prob(self, phases) -> om.Problem: prob.model.add_objective(Mission.Objectives.FUEL, ref=1e4) - prob.model.add_subsystem( - 'input_sink', - VariablesIn(aviary_options=self.aviary_inputs, - meta_data=BaseMetaData), - promotes_inputs=['*'], - promotes_outputs=['*']) - with warnings.catch_warnings(): - # Set initial default values for all LEAPS aircraft variables. - set_aviary_initial_values( - prob.model, self.aviary_inputs, meta_data=BaseMetaData) - warnings.simplefilter("ignore", om.PromotionWarning) prob.setup() diff --git a/aviary/mission/gasp_based/idle_descent_estimation.py b/aviary/mission/gasp_based/idle_descent_estimation.py index 67e282e67..d1beb1dea 100644 --- a/aviary/mission/gasp_based/idle_descent_estimation.py +++ b/aviary/mission/gasp_based/idle_descent_estimation.py @@ -6,113 +6,7 @@ from aviary.mission.gasp_based.phases.time_integration_traj import FlexibleTraj from aviary.variable_info.variables import Aircraft, Mission, Dynamic from aviary.variable_info.enums import Verbosity -from aviary.utils.functions import set_aviary_initial_values, promote_aircraft_and_mission_vars -from aviary.variable_info.variables_in import VariablesIn -from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData - - -def descent_range_and_fuel( - phases=None, - ode_args=None, - initial_mass=154e3, - cruise_alt=35e3, - cruise_mach=.8, - empty_weight=85e3, - payload_weight=30800, - reserve_fuel=4998, -): - warnings.warn("`descent_range_and_fuel` has been replaced with `add_descent_estimation_as_submodel`," - " due to it's better integration with the other subsystems." - "\ndescent_range_and_fuel will be removed in a future release.", DeprecationWarning) - - prob = om.Problem() - prob.driver = om.pyOptSparseDriver() - prob.driver.options["optimizer"] = 'IPOPT' - prob.driver.opt_settings['tol'] = 1.0E-6 - prob.driver.opt_settings['mu_init'] = 1e-5 - prob.driver.opt_settings['max_iter'] = 50 - prob.driver.opt_settings['print_level'] = 5 - - if phases is None: - phases = create_2dof_based_descent_phases( - ode_args, - cruise_mach=cruise_mach, - ) - - traj = FlexibleTraj( - Phases=phases, - traj_final_state_output=[ - Dynamic.Mission.MASS, - Dynamic.Mission.DISTANCE, - Dynamic.Mission.ALTITUDE, - ], - traj_initial_state_input=[ - Dynamic.Mission.MASS, - Dynamic.Mission.DISTANCE, - Dynamic.Mission.ALTITUDE, - ], - ) - prob.model = om.Group() - prob.model.add_subsystem('traj', traj) - - prob.model.add_subsystem( - "fuel_obj", - om.ExecComp( - "reg_objective = overall_fuel/10000", - reg_objective={"val": 0.0, "units": "unitless"}, - overall_fuel={"units": "lbm"}, - ), - promotes_inputs=[ - ("overall_fuel", Mission.Summary.TOTAL_FUEL_MASS), - ], - promotes_outputs=[("reg_objective", Mission.Objectives.FUEL)], - ) - - prob.model.add_objective(Mission.Objectives.FUEL, ref=1e4) - - prob.setup() - prob.set_val("traj.altitude_initial", val=cruise_alt, units="ft") - prob.set_val("traj.mass_initial", val=initial_mass, units="lbm") - prob.set_val("traj.distance_initial", val=0, units="NM") - - # prevent UserWarning that is displayed when an event is triggered - warnings.filterwarnings('ignore', category=UserWarning) - prob.run_model() - warnings.filterwarnings('default', category=UserWarning) - - final_range = prob.get_val('traj.distance_final', units='NM')[0] - final_mass = prob.get_val('traj.mass_final', units='lbm')[0] - descent_fuel = initial_mass - final_mass - print('final range: ', final_range) - print('fuel burned: ', descent_fuel) - - initial_mass2 = empty_weight + payload_weight + reserve_fuel + descent_fuel - prob.set_val("traj.mass_initial", val=initial_mass2, units="lbm") - - # prevent UserWarning that is displayed when an event is triggered - warnings.filterwarnings('ignore', category=UserWarning) - prob.run_model() - warnings.filterwarnings('default', category=UserWarning) - - final_range2 = prob.get_val('traj.distance_final', units='NM')[0] - final_mass2 = prob.get_val('traj.mass_final', units='lbm')[0] - descent_fuel2 = initial_mass2 - final_mass2 - print('initial mass: ', initial_mass2) - print('final range: ', final_range2) - print('fuel burned: ', initial_mass2-final_mass2) - - results = { - 'initial_guess': { - 'distance_flown': final_range, - 'fuel_burned': descent_fuel - }, - 'refined_guess': { - 'distance_flown': final_range2, - 'fuel_burned': descent_fuel2 - } - } - - return results +from aviary.utils.functions import promote_aircraft_and_mission_vars def add_descent_estimation_as_submodel( @@ -200,7 +94,7 @@ def add_descent_estimation_as_submodel( ], promotes_outputs=[('actual_fuel_burn', 'descent_fuel')]) - if verbosity.value >= 1: + if verbosity >= Verbosity.BRIEF: from aviary.utils.functions import create_printcomp dummy_comp = create_printcomp( all_inputs=[ diff --git a/aviary/mission/gasp_based/ode/climb_ode.py b/aviary/mission/gasp_based/ode/climb_ode.py index 2da4157c3..42c233736 100644 --- a/aviary/mission/gasp_based/ode/climb_ode.py +++ b/aviary/mission/gasp_based/ode/climb_ode.py @@ -215,3 +215,6 @@ def setup(self): val=174000 * np.ones(nn), units='lbm') self.set_input_defaults(Dynamic.Mission.MACH, val=0 * np.ones(nn), units="unitless") + + from aviary.variable_info.variables import Aircraft + self.set_input_defaults(Aircraft.Wing.AREA, val=1.0, units="ft**2") diff --git a/aviary/mission/gasp_based/ode/constraints/flight_constraints.py b/aviary/mission/gasp_based/ode/constraints/flight_constraints.py index 3057ebffc..7cc0039e1 100644 --- a/aviary/mission/gasp_based/ode/constraints/flight_constraints.py +++ b/aviary/mission/gasp_based/ode/constraints/flight_constraints.py @@ -188,6 +188,10 @@ def compute_partials(self, inputs, J): class ClimbAtTopOfClimb(om.ExplicitComponent): + """ + This class is not used nor tested. + """ + def setup(self): self.add_input(Dynamic.Mission.VELOCITY, units="ft/s", val=-200) self.add_input( diff --git a/aviary/mission/gasp_based/ode/constraints/test/test_climb_constraints.py b/aviary/mission/gasp_based/ode/constraints/test/test_climb_constraints.py index 1b675c514..adc2166bb 100644 --- a/aviary/mission/gasp_based/ode/constraints/test/test_climb_constraints.py +++ b/aviary/mission/gasp_based/ode/constraints/test/test_climb_constraints.py @@ -12,6 +12,10 @@ class SpeedConstraintTestCase1(unittest.TestCase): + """ + MACH = 0.6 + """ + def setUp(self): self.prob = om.Problem() @@ -44,6 +48,10 @@ def test_case1(self): class SpeedConstraintTestCase2(unittest.TestCase): + """ + MACH = 0.9 + """ + def setUp(self): self.prob = om.Problem() diff --git a/aviary/mission/gasp_based/ode/constraints/test/test_flight_constraints.py b/aviary/mission/gasp_based/ode/constraints/test/test_flight_constraints.py index 724905409..4c0d4c285 100644 --- a/aviary/mission/gasp_based/ode/constraints/test/test_flight_constraints.py +++ b/aviary/mission/gasp_based/ode/constraints/test/test_flight_constraints.py @@ -47,6 +47,9 @@ def test_case1(self): assert_near_equal( self.prob["TAS_violation"], np.array([-99.39848181, -99.39848181]), tol ) # note: output value isn't in GASP + assert_near_equal( + self.prob["TAS_min"], np.array([325.9296, 325.9296]), tol + ) partial_data = self.prob.check_partials(out_stream=None, method="cs") assert_check_partials(partial_data, atol=3e-11, rtol=1e-12) diff --git a/aviary/mission/gasp_based/ode/descent_ode.py b/aviary/mission/gasp_based/ode/descent_ode.py index 0c8cf37eb..9f9b3ab1e 100644 --- a/aviary/mission/gasp_based/ode/descent_ode.py +++ b/aviary/mission/gasp_based/ode/descent_ode.py @@ -11,7 +11,7 @@ from aviary.mission.gasp_based.ode.constraints.speed_constraints import SpeedConstraints from aviary.variable_info.enums import AnalysisScheme, AlphaModes, SpeedType -from aviary.variable_info.variables import Mission, Dynamic +from aviary.variable_info.variables import Aircraft, Mission, Dynamic from aviary.subsystems.aerodynamics.aerodynamics_builder import AerodynamicsBuilderBase from aviary.subsystems.propulsion.propulsion_builder import PropulsionBuilderBase from aviary.mission.gasp_based.ode.time_integration_base_classes import add_SGM_required_inputs @@ -232,3 +232,5 @@ def setup(self): val=0 * np.ones(nn), units="unitless") self.set_input_defaults(Dynamic.Mission.THROTTLE, val=0 * np.ones(nn), units="unitless") + + self.set_input_defaults(Aircraft.Wing.AREA, val=1.0, units="ft**2") diff --git a/aviary/mission/gasp_based/ode/flight_path_eom.py b/aviary/mission/gasp_based/ode/flight_path_eom.py index f68891fbd..718e8a607 100644 --- a/aviary/mission/gasp_based/ode/flight_path_eom.py +++ b/aviary/mission/gasp_based/ode/flight_path_eom.py @@ -1,8 +1,8 @@ import numpy as np import openmdao.api as om -from aviary.variable_info.functions import add_aviary_input, add_aviary_output -from aviary.variable_info.variables import Aircraft, Mission, Dynamic +from aviary.variable_info.functions import add_aviary_input +from aviary.variable_info.variables import Aircraft, Dynamic from aviary.constants import GRAV_ENGLISH_GASP, GRAV_ENGLISH_LBM, MU_TAKEOFF from aviary.variable_info.functions import add_aviary_input diff --git a/aviary/mission/gasp_based/ode/groundroll_eom.py b/aviary/mission/gasp_based/ode/groundroll_eom.py index fbe4a6702..c838bc55c 100644 --- a/aviary/mission/gasp_based/ode/groundroll_eom.py +++ b/aviary/mission/gasp_based/ode/groundroll_eom.py @@ -84,7 +84,7 @@ def setup(self): "alpha_rate", val=np.ones(nn), desc="angle of attack rate", units="deg/s" ) - self.declare_partials("alpha_rate", ["*"], val=0.0) + self.declare_partials("alpha_rate", ["*"]) def compute(self, inputs, outputs): analysis_scheme = self.options["analysis_scheme"] diff --git a/aviary/mission/gasp_based/ode/groundroll_ode.py b/aviary/mission/gasp_based/ode/groundroll_ode.py index d6e4c6124..a4dcb145b 100644 --- a/aviary/mission/gasp_based/ode/groundroll_ode.py +++ b/aviary/mission/gasp_based/ode/groundroll_ode.py @@ -4,11 +4,10 @@ from aviary.mission.gasp_based.ode.base_ode import BaseODE from aviary.mission.gasp_based.ode.groundroll_eom import GroundrollEOM from aviary.mission.gasp_based.ode.params import ParamPort -from aviary.variable_info.variables import Aircraft, Dynamic, Mission +from aviary.variable_info.variables import Aircraft, Dynamic from aviary.variable_info.enums import AnalysisScheme from aviary.subsystems.aerodynamics.aerodynamics_builder import AerodynamicsBuilderBase from aviary.variable_info.variable_meta_data import _MetaData -from aviary.variable_info.variables_in import VariablesIn from aviary.mission.gasp_based.ode.time_integration_base_classes import add_SGM_required_inputs @@ -47,12 +46,6 @@ def setup(self): # TODO: paramport self.add_subsystem("params", ParamPort(), promotes=["*"]) - self.add_subsystem( - 'input_port', - VariablesIn(aviary_options=aviary_options), - promotes_inputs=['*'], - promotes_outputs=['*']) - self.add_atmosphere(nn) # broadcast scalar i_wing to alpha for aero @@ -143,3 +136,5 @@ def setup(self): self.set_input_defaults(Dynamic.Mission.VELOCITY, val=np.zeros(nn), units="kn") self.set_input_defaults(Dynamic.Mission.VELOCITY_RATE, val=np.zeros(nn), units="kn/s") + + self.set_input_defaults(Aircraft.Wing.INCIDENCE, val=1.0, units="deg") diff --git a/aviary/mission/gasp_based/ode/params.py b/aviary/mission/gasp_based/ode/params.py index 8dfcf9ac4..e2f35ea04 100644 --- a/aviary/mission/gasp_based/ode/params.py +++ b/aviary/mission/gasp_based/ode/params.py @@ -6,75 +6,9 @@ class ParamPort(om.ExplicitComponent): param_data = { - Aircraft.Wing.AREA: dict(units="ft**2", val=1370.3), Aircraft.Wing.INCIDENCE: dict(units="deg", val=0), - Aircraft.Wing.HEIGHT: dict(units="ft", val=8), - Aircraft.Wing.SPAN: dict(units="ft", val=117.8), - Mission.Design.GROSS_MASS: dict(units="lbm", val=175400), - Mission.Summary.GROSS_MASS: dict(units="lbm", val=175400), - Mission.Summary.FUEL_FLOW_SCALER: dict(units="unitless", val=1.0), - Mission.Takeoff.AIRPORT_ALTITUDE: dict(units="ft", val=0), - Mission.Landing.AIRPORT_ALTITUDE: dict(units="ft", val=0), Aircraft.Wing.FLAP_DEFLECTION_TAKEOFF: dict(units="deg", val=10), Aircraft.Wing.FLAP_DEFLECTION_LANDING: dict(units="deg", val=40), - Aircraft.Wing.AVERAGE_CHORD: dict(units="ft", val=12.615), - Aircraft.Fuselage.AVG_DIAMETER: dict(units="inch", val=12 * 13.100), - Aircraft.HorizontalTail.AVERAGE_CHORD: dict(units="ft", val=9.577), - Aircraft.HorizontalTail.AREA: dict(units="ft**2", val=375.880), - Aircraft.HorizontalTail.SPAN: dict(units="ft", val=42.254), - Aircraft.VerticalTail.AVERAGE_CHORD: dict(units="ft", val=16.832), - Aircraft.VerticalTail.AREA: dict(units="ft**2", val=469.318), - Aircraft.VerticalTail.SPAN: dict(units="ft", val=27.996), - Aircraft.Fuselage.LENGTH: dict(units="ft", val=129.4), - Aircraft.Nacelle.AVG_LENGTH: dict(units="ft", val=14.5), - Aircraft.Fuselage.WETTED_AREA: dict(units="ft**2", val=4000), - Aircraft.Nacelle.SURFACE_AREA: dict(units="ft**2", val=659.23 / 2), - Aircraft.Wing.THICKNESS_TO_CHORD_UNWEIGHTED: dict(units="unitless", val=0.1397), - Aircraft.Strut.CHORD: dict( - units="ft", val=0 - ), # only available if Aviary_option Aircraft.Wing.HAS_STRUT - Aircraft.Wing.ASPECT_RATIO: dict(units="unitless", val=10.13), - Aircraft.Wing.TAPER_RATIO: dict(units="unitless", val=0.33), - Aircraft.Wing.THICKNESS_TO_CHORD_ROOT: dict(units="unitless", val=0.15), - Aircraft.Wing.THICKNESS_TO_CHORD_TIP: dict(units="unitless", val=0.12), - Aircraft.HorizontalTail.VERTICAL_TAIL_FRACTION: dict(units="unitless", val=0), - Aircraft.Wing.SWEEP: dict(units="deg", val=25), - Aircraft.HorizontalTail.SWEEP: dict(units="deg", val=25), - Aircraft.HorizontalTail.MOMENT_RATIO: dict(units="unitless", val=0.2307), - Aircraft.Wing.MOUNTING_TYPE: dict(units="unitless", val=0), - Aircraft.Design.STATIC_MARGIN: dict(units="unitless", val=0.03), - Aircraft.Design.CG_DELTA: dict(units="unitless", val=0.25), - Aircraft.Wing.FORM_FACTOR: dict(units="unitless", val=1.25), - Aircraft.Fuselage.FORM_FACTOR: dict(units="unitless", val=1.25), - Aircraft.Nacelle.FORM_FACTOR: dict(units="unitless", val=1.5), - Aircraft.VerticalTail.FORM_FACTOR: dict(units="unitless", val=1.25), - Aircraft.HorizontalTail.FORM_FACTOR: dict(units="unitless", val=1.25), - Aircraft.Wing.FUSELAGE_INTERFERENCE_FACTOR: dict(units="unitless", val=1.1), - Aircraft.Strut.FUSELAGE_INTERFERENCE_FACTOR: dict(units="unitless", val=0), - Aircraft.Design.DRAG_COEFFICIENT_INCREMENT: dict(units="unitless", val=0.00175), - Aircraft.Fuselage.FLAT_PLATE_AREA_INCREMENT: dict(units="ft**2", val=0.25), - Aircraft.Wing.CENTER_DISTANCE: dict(units="unitless", val=0.463), - Aircraft.Wing.MIN_PRESSURE_LOCATION: dict(units="unitless", val=0.3), - Aircraft.Wing.MAX_THICKNESS_LOCATION: dict(units="unitless", val=0.4), - Aircraft.Strut.AREA_RATIO: dict(units="unitless", val=0), - Aircraft.Wing.ZERO_LIFT_ANGLE: dict(units="deg", val=-1.2), - Aircraft.Design.SUPERCRITICAL_DIVERGENCE_SHIFT: dict(units="unitless", val=0.033), - Aircraft.Wing.FLAP_CHORD_RATIO: dict(units="unitless", val=0.3), - Mission.Design.LIFT_COEFFICIENT_MAX_FLAPS_UP: dict(units="unitless", val=1.2596), - Mission.Takeoff.LIFT_COEFFICIENT_MAX: dict(units="unitless", val=2.1886), - Mission.Landing.LIFT_COEFFICIENT_MAX: dict(units="unitless", val=2.8155), - Mission.Takeoff.LIFT_COEFFICIENT_FLAP_INCREMENT: dict( - units="unitless", val=0.4182 - ), - Mission.Landing.LIFT_COEFFICIENT_FLAP_INCREMENT: dict( - units="unitless", val=1.0293 - ), - Mission.Takeoff.DRAG_COEFFICIENT_FLAP_INCREMENT: dict( - units="unitless", val=0.0085 - ), - Mission.Landing.DRAG_COEFFICIENT_FLAP_INCREMENT: dict( - units="unitless", val=0.0406 - ), } def setup(self): @@ -149,3 +83,98 @@ def promote_params(sys, trajs=None, phases=None): static_target=True, ) sys.promotes(phasename, inputs=proms) + + +params_for_unit_tests = { + Aircraft.Wing.AREA: dict(units="ft**2", val=1370.3), + Aircraft.Wing.HEIGHT: dict(units="ft", val=8), + Aircraft.Wing.SPAN: dict(units="ft", val=117.8), + Mission.Design.GROSS_MASS: dict(units="lbm", val=175400), + Mission.Summary.GROSS_MASS: dict(units="lbm", val=175400), + Mission.Summary.FUEL_FLOW_SCALER: dict(units="unitless", val=1.0), + Mission.Takeoff.AIRPORT_ALTITUDE: dict(units="ft", val=0), + Mission.Landing.AIRPORT_ALTITUDE: dict(units="ft", val=0), + Aircraft.Wing.AVERAGE_CHORD: dict(units="ft", val=12.615), + Aircraft.Fuselage.AVG_DIAMETER: dict(units="inch", val=12 * 13.100), + Aircraft.HorizontalTail.AVERAGE_CHORD: dict(units="ft", val=9.577), + Aircraft.HorizontalTail.AREA: dict(units="ft**2", val=375.880), + Aircraft.HorizontalTail.SPAN: dict(units="ft", val=42.254), + Aircraft.VerticalTail.AVERAGE_CHORD: dict(units="ft", val=16.832), + Aircraft.VerticalTail.AREA: dict(units="ft**2", val=469.318), + Aircraft.VerticalTail.SPAN: dict(units="ft", val=27.996), + Aircraft.Fuselage.LENGTH: dict(units="ft", val=129.4), + Aircraft.Nacelle.AVG_LENGTH: dict(units="ft", val=14.5), + Aircraft.Fuselage.WETTED_AREA: dict(units="ft**2", val=4000), + Aircraft.Nacelle.SURFACE_AREA: dict(units="ft**2", val=659.23 / 2), + Aircraft.Wing.THICKNESS_TO_CHORD_UNWEIGHTED: dict(units="unitless", val=0.1397), + Aircraft.Strut.CHORD: dict( + units="ft", val=0 + ), # only available if Aviary_option Aircraft.Wing.HAS_STRUT + Aircraft.Wing.ASPECT_RATIO: dict(units="unitless", val=10.13), + Aircraft.Wing.TAPER_RATIO: dict(units="unitless", val=0.33), + Aircraft.Wing.THICKNESS_TO_CHORD_ROOT: dict(units="unitless", val=0.15), + Aircraft.Wing.THICKNESS_TO_CHORD_TIP: dict(units="unitless", val=0.12), + Aircraft.HorizontalTail.VERTICAL_TAIL_FRACTION: dict(units="unitless", val=0), + Aircraft.Wing.SWEEP: dict(units="deg", val=25), + Aircraft.HorizontalTail.SWEEP: dict(units="deg", val=25), + Aircraft.HorizontalTail.MOMENT_RATIO: dict(units="unitless", val=0.2307), + Aircraft.Wing.MOUNTING_TYPE: dict(units="unitless", val=0), + Aircraft.Design.STATIC_MARGIN: dict(units="unitless", val=0.03), + Aircraft.Design.CG_DELTA: dict(units="unitless", val=0.25), + Aircraft.Wing.FORM_FACTOR: dict(units="unitless", val=1.25), + Aircraft.Fuselage.FORM_FACTOR: dict(units="unitless", val=1.25), + Aircraft.Nacelle.FORM_FACTOR: dict(units="unitless", val=1.5), + Aircraft.VerticalTail.FORM_FACTOR: dict(units="unitless", val=1.25), + Aircraft.HorizontalTail.FORM_FACTOR: dict(units="unitless", val=1.25), + Aircraft.Wing.FUSELAGE_INTERFERENCE_FACTOR: dict(units="unitless", val=1.1), + Aircraft.Strut.FUSELAGE_INTERFERENCE_FACTOR: dict(units="unitless", val=0), + Aircraft.Design.DRAG_COEFFICIENT_INCREMENT: dict(units="unitless", val=0.00175), + Aircraft.Fuselage.FLAT_PLATE_AREA_INCREMENT: dict(units="ft**2", val=0.25), + Aircraft.Wing.CENTER_DISTANCE: dict(units="unitless", val=0.463), + Aircraft.Wing.MIN_PRESSURE_LOCATION: dict(units="unitless", val=0.3), + Aircraft.Wing.MAX_THICKNESS_LOCATION: dict(units="unitless", val=0.4), + Aircraft.Strut.AREA_RATIO: dict(units="unitless", val=0), + Aircraft.Wing.ZERO_LIFT_ANGLE: dict(units="deg", val=-1.2), + Aircraft.Design.SUPERCRITICAL_DIVERGENCE_SHIFT: dict(units="unitless", val=0.033), + Aircraft.Wing.FLAP_CHORD_RATIO: dict(units="unitless", val=0.3), + Mission.Design.LIFT_COEFFICIENT_MAX_FLAPS_UP: dict(units="unitless", val=1.2596), + Mission.Takeoff.LIFT_COEFFICIENT_MAX: dict(units="unitless", val=2.1886), + Mission.Landing.LIFT_COEFFICIENT_MAX: dict(units="unitless", val=2.8155), + Mission.Takeoff.LIFT_COEFFICIENT_FLAP_INCREMENT: dict( + units="unitless", val=0.4182 + ), + Mission.Landing.LIFT_COEFFICIENT_FLAP_INCREMENT: dict( + units="unitless", val=1.0293 + ), + Mission.Takeoff.DRAG_COEFFICIENT_FLAP_INCREMENT: dict( + units="unitless", val=0.0085 + ), + Mission.Landing.DRAG_COEFFICIENT_FLAP_INCREMENT: dict( + units="unitless", val=0.0406 + ), +} + + +def set_params_for_unit_tests(prob): + """ + Helper function to set parameters for several ode tests with the 2DOF method. + + This is needed because the Paramport used to contain default values for some + variables. + + Parameters + ---------- + prob : Problem + OpenMDAO problem that has been setup. + + Returns + ------- + Problem + """ + for key, val in params_for_unit_tests.items(): + try: + prob.set_val(key, val['val'], units=val['units']) + except: + pass + + return prob diff --git a/aviary/mission/gasp_based/ode/rotation_eom.py b/aviary/mission/gasp_based/ode/rotation_eom.py index bef250755..fb169bf9e 100644 --- a/aviary/mission/gasp_based/ode/rotation_eom.py +++ b/aviary/mission/gasp_based/ode/rotation_eom.py @@ -55,7 +55,7 @@ def setup(self): "alpha_rate", val=np.ones(nn), desc="angle of attack rate", units="deg/s" ) - self.declare_partials("alpha_rate", ["*"], val=0) + self.declare_partials("alpha_rate", ["*"]) def setup_partials(self): arange = np.arange(self.options["num_nodes"]) diff --git a/aviary/mission/gasp_based/ode/test/test_accel_eom.py b/aviary/mission/gasp_based/ode/test/test_accel_eom.py index 64fd87a45..3afa37907 100644 --- a/aviary/mission/gasp_based/ode/test/test_accel_eom.py +++ b/aviary/mission/gasp_based/ode/test/test_accel_eom.py @@ -1,5 +1,4 @@ import unittest -import os import numpy as np import openmdao.api as om diff --git a/aviary/mission/gasp_based/ode/test/test_accel_ode.py b/aviary/mission/gasp_based/ode/test/test_accel_ode.py index f9b1a0cc4..a8d5b56d2 100644 --- a/aviary/mission/gasp_based/ode/test/test_accel_ode.py +++ b/aviary/mission/gasp_based/ode/test/test_accel_ode.py @@ -1,15 +1,15 @@ import unittest -import os import openmdao.api as om from openmdao.utils.assert_utils import assert_check_partials from aviary.mission.gasp_based.ode.accel_ode import AccelODE -from aviary.variable_info.options import get_option_defaults -from aviary.utils.test_utils.IO_test_util import check_prob_outputs -from aviary.variable_info.variables import Dynamic +from aviary.mission.gasp_based.ode.params import set_params_for_unit_tests from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems +from aviary.utils.test_utils.IO_test_util import check_prob_outputs +from aviary.variable_info.options import get_option_defaults +from aviary.variable_info.variables import Dynamic class AccelerationODETestCase(unittest.TestCase): @@ -36,13 +36,15 @@ def test_accel(self): self.prob.set_val(Dynamic.Mission.VELOCITY, [185, 252], units="kn") self.prob.set_val(Dynamic.Mission.MASS, [174974, 174878], units="lbm") + set_params_for_unit_tests(self.prob) + self.prob.run_model() testvals = { Dynamic.Mission.LIFT: [174974, 174878], Dynamic.Mission.FUEL_FLOW_RATE_NEGATIVE_TOTAL: [ - -13189.24129984, -13490.78047417] # lbm/h + -13262.73, -13567.53] # lbm/h } - check_prob_outputs(self.prob, testvals, rtol=1e-2) + check_prob_outputs(self.prob, testvals, rtol=1e-6) partial_data = self.prob.check_partials( method="cs", out_stream=None, excludes=["*params*", "*aero*"] diff --git a/aviary/mission/gasp_based/ode/test/test_ascent_eom.py b/aviary/mission/gasp_based/ode/test/test_ascent_eom.py index a553baf4c..22ecbea86 100644 --- a/aviary/mission/gasp_based/ode/test/test_ascent_eom.py +++ b/aviary/mission/gasp_based/ode/test/test_ascent_eom.py @@ -1,9 +1,8 @@ import unittest -import os import numpy as np import openmdao.api as om -from openmdao.utils.assert_utils import assert_check_partials +from openmdao.utils.assert_utils import assert_check_partials, assert_near_equal from aviary.mission.gasp_based.ode.ascent_eom import AscentEOM from aviary.variable_info.variables import Aircraft, Dynamic @@ -38,6 +37,15 @@ def test_case1(self): tol = 1e-6 self.prob.run_model() + assert_near_equal( + self.prob[Dynamic.Mission.VELOCITY_RATE], np.array( + [2.202965, 2.202965]), tol + ) + assert_near_equal( + self.prob[Dynamic.Mission.FLIGHT_PATH_ANGLE_RATE], np.array( + [-3.216328, -3.216328]), tol + ) + partial_data = self.prob.check_partials(out_stream=None, method="cs") assert_check_partials(partial_data, atol=1e-12, rtol=1e-12) diff --git a/aviary/mission/gasp_based/ode/test/test_ascent_ode.py b/aviary/mission/gasp_based/ode/test/test_ascent_ode.py index cdadcac9c..153f190cf 100644 --- a/aviary/mission/gasp_based/ode/test/test_ascent_ode.py +++ b/aviary/mission/gasp_based/ode/test/test_ascent_ode.py @@ -1,9 +1,11 @@ import unittest +import numpy as np import openmdao.api as om -from openmdao.utils.assert_utils import assert_check_partials +from openmdao.utils.assert_utils import assert_check_partials, assert_near_equal from aviary.mission.gasp_based.ode.ascent_ode import AscentODE +from aviary.mission.gasp_based.ode.params import set_params_for_unit_tests from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems from aviary.variable_info.options import get_option_defaults @@ -29,8 +31,36 @@ def test_ascent_partials(self): self.prob.set_val(Dynamic.Mission.VELOCITY, [100, 100], units="kn") self.prob.set_val("t_curr", [1, 2], units="s") + set_params_for_unit_tests(self.prob) + self.prob.run_model() + tol = tol = 1e-6 + assert_near_equal( + self.prob[Dynamic.Mission.VELOCITY_RATE], np.array( + [641174.75, 641174.75]), tol) + assert_near_equal( + self.prob[Dynamic.Mission.FLIGHT_PATH_ANGLE_RATE], np.array( + [2260.644, 2260.644]), tol) + assert_near_equal( + self.prob[Dynamic.Mission.ALTITUDE_RATE], np.array( + [0.0, 0.0]), tol) + assert_near_equal( + self.prob[Dynamic.Mission.DISTANCE_RATE], np.array( + [168.781, 168.781]), tol) + assert_near_equal( + self.prob["alpha_rate"], np.array( + [0.0, 0.0]), tol) + assert_near_equal( + self.prob["normal_force"], np.array( + [0.0, 0.0]), tol) + assert_near_equal( + self.prob["fuselage_pitch"], np.array( + [0.0, 0.0]), tol) + assert_near_equal( + self.prob["load_factor"], np.array( + [11850.494, 11850.494]), tol) + partial_data = self.prob.check_partials( out_stream=None, method="cs", excludes=["*params*", "*aero*"] ) diff --git a/aviary/mission/gasp_based/ode/test/test_breguet_cruise_ode.py b/aviary/mission/gasp_based/ode/test/test_breguet_cruise_ode.py new file mode 100644 index 000000000..0539e8e93 --- /dev/null +++ b/aviary/mission/gasp_based/ode/test/test_breguet_cruise_ode.py @@ -0,0 +1,66 @@ +import unittest + +import numpy as np +import openmdao.api as om +from openmdao.utils.assert_utils import assert_check_partials, assert_near_equal + +from aviary.mission.gasp_based.ode.breguet_cruise_ode import BreguetCruiseODESolution +from aviary.mission.gasp_based.ode.params import set_params_for_unit_tests +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems +from aviary.variable_info.options import get_option_defaults +from aviary.variable_info.variables import Dynamic + + +class CruiseODETestCase(unittest.TestCase): + def setUp(self): + self.prob = om.Problem() + + aviary_options = get_option_defaults() + default_mission_subsystems = get_default_mission_subsystems( + 'GASP', build_engine_deck(aviary_options)) + + self.prob.model = BreguetCruiseODESolution( + num_nodes=2, + aviary_options=aviary_options, + core_subsystems=default_mission_subsystems) + + self.prob.model.set_input_defaults( + Dynamic.Mission.MACH, np.array([0, 0]), units="unitless" + ) + + def test_cruise(self): + """Test partial derivatives""" + self.prob.setup(check=False, force_alloc_complex=True) + + self.prob.set_val(Dynamic.Mission.MACH, [0.7, 0.7], units="unitless") + + set_params_for_unit_tests(self.prob) + + self.prob.run_model() + + tol = tol = 1e-6 + assert_near_equal( + self.prob[Dynamic.Mission.VELOCITY_RATE], np.array( + [1.0, 1.0]), tol) + assert_near_equal( + self.prob[Dynamic.Mission.DISTANCE], np.array( + [0.0, 881.8116]), tol) + assert_near_equal( + self.prob["time"], np.array( + [0, 7906.83]), tol) + assert_near_equal( + self.prob[Dynamic.Mission.SPECIFIC_ENERGY_RATE_EXCESS], np.array( + [3.429719, 4.433518]), tol) + assert_near_equal( + self.prob[Dynamic.Mission.ALTITUDE_RATE_MAX], np.array( + [-17.63194, -16.62814]), tol) + + partial_data = self.prob.check_partials( + out_stream=None, method="cs", excludes=["*USatm*", "*params*", "*aero*"] + ) + assert_check_partials(partial_data, atol=1e-8, rtol=1e-8) + + +if __name__ == "__main__": + unittest.main() diff --git a/aviary/mission/gasp_based/ode/test/test_climb_eom.py b/aviary/mission/gasp_based/ode/test/test_climb_eom.py index a4fb3c0b9..66ceb0031 100644 --- a/aviary/mission/gasp_based/ode/test/test_climb_eom.py +++ b/aviary/mission/gasp_based/ode/test/test_climb_eom.py @@ -1,5 +1,4 @@ import unittest -import os import numpy as np import openmdao.api as om diff --git a/aviary/mission/gasp_based/ode/test/test_climb_ode.py b/aviary/mission/gasp_based/ode/test/test_climb_ode.py index 8802dcb5d..9d8d5ebe9 100644 --- a/aviary/mission/gasp_based/ode/test/test_climb_ode.py +++ b/aviary/mission/gasp_based/ode/test/test_climb_ode.py @@ -1,16 +1,16 @@ import unittest -import os import numpy as np import openmdao.api as om from openmdao.utils.assert_utils import assert_check_partials from aviary.mission.gasp_based.ode.climb_ode import ClimbODE +from aviary.mission.gasp_based.ode.params import set_params_for_unit_tests +from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.test_utils.IO_test_util import check_prob_outputs +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems from aviary.variable_info.options import get_option_defaults from aviary.variable_info.variables import Aircraft, Dynamic -from aviary.subsystems.propulsion.utils import build_engine_deck -from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems class ClimbODETestCase(unittest.TestCase): @@ -44,20 +44,22 @@ def test_start_of_climb(self): # slightly greater than zero to help check partials self.prob.set_val(Aircraft.Wing.INCIDENCE, 0.0000001, units="deg") + set_params_for_unit_tests(self.prob) + self.prob.run_model() testvals = { - "alpha": 5.19, - "CL": 0.5975, - "CD": 0.0307, - Dynamic.Mission.ALTITUDE_RATE: 3186 / 60, - # TAS (kts -> ft/s) * cos(gamma) - Dynamic.Mission.DISTANCE_RATE: (254 * 1.68781) * np.cos(np.deg2rad(7.12)), - Dynamic.Mission.FUEL_FLOW_RATE_NEGATIVE_TOTAL: -13505, # lbm/h - "theta": np.deg2rad(12.31), - Dynamic.Mission.FLIGHT_PATH_ANGLE: np.deg2rad(7.12), + "alpha": 5.16398, + "CL": 0.59766664, + "CD": 0.03070836, + Dynamic.Mission.ALTITUDE_RATE: 3414.63 / 60, # ft/s + # TAS (kts -> ft/s) * cos(gamma), 253.6827 * 1.68781 * cos(0.13331060446181708) + Dynamic.Mission.DISTANCE_RATE: 424.36918705874785, # ft/s + Dynamic.Mission.FUEL_FLOW_RATE_NEGATIVE_TOTAL: -13448.29, # lbm/h + "theta": 0.22343879616956605, # rad (12.8021 deg) + Dynamic.Mission.FLIGHT_PATH_ANGLE: 0.13331060446181708, # rad (7.638135 deg) } - check_prob_outputs(self.prob, testvals, rtol=1e-1) # TODO tighten + check_prob_outputs(self.prob, testvals, rtol=1e-6) self.prob.setup(check=False, force_alloc_complex=True) partial_data = self.prob.check_partials( @@ -80,24 +82,23 @@ def test_end_of_climb(self): self.prob.set_val(Dynamic.Mission.MASS, np.array([174149, 171592]), units="lbm") self.prob.set_val("EAS", np.array([270, 270]), units="kn") + set_params_for_unit_tests(self.prob) + self.prob.run_model() testvals = { - "alpha": [4.08, 4.05], - "CL": [0.5119, 0.6113], - "CD": [0.0270, 0.0326], - Dynamic.Mission.ALTITUDE_RATE: [3054 / 60, 453 / 60], - # TAS (kts -> ft/s) * cos(gamma) - Dynamic.Mission.DISTANCE_RATE: [ - (319 * 1.68781) * np.cos(np.deg2rad(5.42)), - (459 * 1.68781) * np.cos(np.deg2rad(0.56)), - ], - Dynamic.Mission.FUEL_FLOW_RATE_NEGATIVE_TOTAL: [-11422, -6039], - "theta": np.deg2rad([9.5, 4.61]), - Dynamic.Mission.FLIGHT_PATH_ANGLE: np.deg2rad([5.42, 0.56]), - Dynamic.Mission.THRUST_TOTAL: [25610, 10790], + "alpha": [4.05559, 4.08245], + "CL": [0.512629, 0.617725], + "CD": [0.02692764, 0.03311237], + Dynamic.Mission.ALTITUDE_RATE: [3053.754 / 60, 429.665 / 60], # ft/s + # TAS (kts -> ft/s) * cos(gamma), [319, 459] kts + Dynamic.Mission.DISTANCE_RATE: [536.2835, 774.4118], # ft/s + Dynamic.Mission.FUEL_FLOW_RATE_NEGATIVE_TOTAL: [-11420.05, -6050.26], + "theta": [0.16540479, 0.08049912], # rad ([9.47699, 4.61226] deg), + Dynamic.Mission.FLIGHT_PATH_ANGLE: [0.09462135, 0.00924686], # rad, gamma + Dynamic.Mission.THRUST_TOTAL: [25560.51, 10784.25], } - check_prob_outputs(self.prob, testvals, 1e-1) # TODO tighten + check_prob_outputs(self.prob, testvals, 1e-6) if __name__ == "__main__": diff --git a/aviary/mission/gasp_based/ode/test/test_descent_eom.py b/aviary/mission/gasp_based/ode/test/test_descent_eom.py index f0cd6c70a..f5f937d33 100644 --- a/aviary/mission/gasp_based/ode/test/test_descent_eom.py +++ b/aviary/mission/gasp_based/ode/test/test_descent_eom.py @@ -1,5 +1,4 @@ import unittest -import os import numpy as np import openmdao.api as om diff --git a/aviary/mission/gasp_based/ode/test/test_descent_ode.py b/aviary/mission/gasp_based/ode/test/test_descent_ode.py index 3bf7832f2..1c0fd0a1c 100644 --- a/aviary/mission/gasp_based/ode/test/test_descent_ode.py +++ b/aviary/mission/gasp_based/ode/test/test_descent_ode.py @@ -1,5 +1,4 @@ import unittest -import os import numpy as np import openmdao @@ -8,12 +7,13 @@ from packaging import version from aviary.mission.gasp_based.ode.descent_ode import DescentODE -from aviary.variable_info.options import get_option_defaults +from aviary.mission.gasp_based.ode.params import set_params_for_unit_tests +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems from aviary.utils.test_utils.IO_test_util import check_prob_outputs from aviary.variable_info.enums import SpeedType +from aviary.variable_info.options import get_option_defaults from aviary.variable_info.variables import Dynamic -from aviary.subsystems.propulsion.utils import build_engine_deck -from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems class DescentODETestCase(unittest.TestCase): @@ -44,26 +44,26 @@ def test_high_alt(self): self.prob.set_val(Dynamic.Mission.ALTITUDE, np.array([36500, 14500]), units="ft") self.prob.set_val(Dynamic.Mission.MASS, np.array([147661, 147572]), units="lbm") + set_params_for_unit_tests(self.prob) + self.prob.run_model() testvals = { - "alpha": np.array([3.2, 1.21]), - "CL": np.array([0.5123, 0.2583]), - "CD": np.array([0.0279, 0.0197]), - Dynamic.Mission.ALTITUDE_RATE: np.array([-2385, -3076]) / 60, - # TAS (ft/s) * cos(gamma) - Dynamic.Mission.DISTANCE_RATE: [ - (459 * 1.68781) * np.cos(np.deg2rad(-2.94)), - (437 * 1.68781) * np.cos(np.deg2rad(-3.98)), - ], + "alpha": np.array([3.23388, 1.203234]), + "CL": np.array([0.51849367, 0.25908653]), + "CD": np.array([0.02794324, 0.01862946]), + # ft/s + Dynamic.Mission.ALTITUDE_RATE: np.array([-2356.7705, -2877.9606]) / 60, + # TAS (ft/s) * cos(gamma), [458.67774, 437.62297] kts + Dynamic.Mission.DISTANCE_RATE: [773.1637, 737.0653], # ft/s # lbm/h - Dynamic.Mission.FUEL_FLOW_RATE_NEGATIVE_TOTAL: np.array([-452, -996]), - "EAS": np.array([249, 350]) * 1.68781, # kts -> ft/s - Dynamic.Mission.MACH: np.array([0.8, 0.696]), - Dynamic.Mission.FLIGHT_PATH_ANGLE: np.deg2rad([-2.94, -3.98]), + Dynamic.Mission.FUEL_FLOW_RATE_NEGATIVE_TOTAL: np.array([-451.0239, -997.1514]), + "EAS": [417.87419406, 590.73344937], # ft/s ([247.58367, 349.99997] kts) + Dynamic.Mission.MACH: [0.8, 0.697266], + # gamma, rad ([-2.908332, -3.723388] deg) + Dynamic.Mission.FLIGHT_PATH_ANGLE: [-0.05075997, -0.06498538], } - - check_prob_outputs(self.prob, testvals, rtol=1e-1) # TODO tighten + check_prob_outputs(self.prob, testvals, rtol=1e-6) partial_data = self.prob.check_partials( method="cs", out_stream=None, excludes=["*params*", "*aero*"] @@ -82,19 +82,21 @@ def test_low_alt(self): self.prob.set_val(Dynamic.Mission.MASS, 147410, units="lbm") self.prob.set_val("EAS", 250, units="kn") + set_params_for_unit_tests(self.prob) + self.prob.run_model() testvals = { - "alpha": 4.21, - "CL": 0.5063, - "CD": 0.0271, - Dynamic.Mission.ALTITUDE_RATE: -1158 / 60, - # TAS (ft/s) * cos(gamma) - Dynamic.Mission.DISTANCE_RATE: (255 * 1.68781) * np.cos(np.deg2rad(-2.56)), - Dynamic.Mission.FUEL_FLOW_RATE_NEGATIVE_TOTAL: -1294, - Dynamic.Mission.FLIGHT_PATH_ANGLE: np.deg2rad(-2.56), + "alpha": 4.19956, + "CL": 0.507578, + "CD": 0.0268404, + Dynamic.Mission.ALTITUDE_RATE: -1138.583 / 60, + # TAS (ft/s) * cos(gamma) = 255.5613 * 1.68781 * cos(-0.0440083) + Dynamic.Mission.DISTANCE_RATE: 430.9213, + Dynamic.Mission.FUEL_FLOW_RATE_NEGATIVE_TOTAL: -1295.11, + Dynamic.Mission.FLIGHT_PATH_ANGLE: -0.0440083, # rad (-2.52149 deg) } - check_prob_outputs(self.prob, testvals, rtol=1e-1) # TODO tighten + check_prob_outputs(self.prob, testvals, rtol=1e-6) partial_data = self.prob.check_partials( out_stream=None, method="cs", excludes=["*params*", "*aero*"] diff --git a/aviary/mission/gasp_based/ode/test/test_flight_path_eom.py b/aviary/mission/gasp_based/ode/test/test_flight_path_eom.py new file mode 100644 index 000000000..3c7735a1a --- /dev/null +++ b/aviary/mission/gasp_based/ode/test/test_flight_path_eom.py @@ -0,0 +1,83 @@ +import unittest + +import numpy as np +import openmdao.api as om +from openmdao.utils.assert_utils import assert_check_partials, assert_near_equal + +from aviary.mission.gasp_based.ode.flight_path_eom import FlightPathEOM +from aviary.variable_info.variables import Dynamic + + +class FlightPathEOMTestCase(unittest.TestCase): + def setUp(self): + self.ground_roll = False + self.prob = om.Problem() + self.fp = self.prob.model.add_subsystem( + "group", FlightPathEOM(num_nodes=2, ground_roll=self.ground_roll), promotes=["*"] + ) + self.prob.setup(check=False, force_alloc_complex=True) + + def test_case1(self): + """ + ground_roll = False (the aircraft is not confined to the ground) + """ + tol = 1e-6 + self.prob.run_model() + + assert_near_equal( + self.prob[Dynamic.Mission.VELOCITY_RATE], np.array( + [-27.10027, -27.10027]), tol) + assert_near_equal( + self.prob[Dynamic.Mission.DISTANCE_RATE], np.array( + [0.5403023, 0.5403023]), tol) + assert_near_equal( + self.prob["normal_force"], np.array( + [-0.0174524, -0.0174524]), tol) + assert_near_equal( + self.prob["fuselage_pitch"], np.array( + [58.2958, 58.2958]), tol) + assert_near_equal( + self.prob["load_factor"], np.array( + [1.883117, 1.883117]), tol) + assert_near_equal( + self.prob[Dynamic.Mission.ALTITUDE_RATE], np.array( + [0.841471, 0.841471]), tol) + assert_near_equal( + self.prob[Dynamic.Mission.FLIGHT_PATH_ANGLE_RATE], np.array( + [15.36423, 15.36423]), tol) + + partial_data = self.prob.check_partials(out_stream=None, method="cs") + assert_check_partials(partial_data, atol=1e-12, rtol=1e-12) + + def test_case2(self): + """ + ground_roll = True (the aircraft is confined to the ground) + """ + self.fp.options["ground_roll"] = True + self.prob.setup(force_alloc_complex=True) + + tol = 1e-6 + self.prob.run_model() + + assert_near_equal( + self.prob[Dynamic.Mission.VELOCITY_RATE], np.array( + [-27.09537, -27.09537]), tol) + assert_near_equal( + self.prob[Dynamic.Mission.DISTANCE_RATE], np.array( + [0.5403023, 0.5403023]), tol) + assert_near_equal( + self.prob["normal_force"], np.array( + [-0.0, -0.0]), tol) + assert_near_equal( + self.prob["fuselage_pitch"], np.array( + [57.29578, 57.29578]), tol) + assert_near_equal( + self.prob["load_factor"], np.array( + [1.850816, 1.850816]), tol) + + partial_data = self.prob.check_partials(out_stream=None, method="cs") + assert_check_partials(partial_data, atol=1e-12, rtol=1e-12) + + +if __name__ == "__main__": + unittest.main() diff --git a/aviary/mission/gasp_based/ode/test/test_flight_path_ode.py b/aviary/mission/gasp_based/ode/test/test_flight_path_ode.py new file mode 100644 index 000000000..a1c188f73 --- /dev/null +++ b/aviary/mission/gasp_based/ode/test/test_flight_path_ode.py @@ -0,0 +1,96 @@ +import unittest + +import numpy as np +import openmdao.api as om +from openmdao.utils.assert_utils import assert_check_partials, assert_near_equal + +from aviary.mission.gasp_based.ode.flight_path_ode import FlightPathODE +from aviary.mission.gasp_based.ode.params import set_params_for_unit_tests +from aviary.variable_info.options import get_option_defaults +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems +from aviary.utils.test_utils.IO_test_util import check_prob_outputs +from aviary.variable_info.variables import Dynamic + + +class FlightPathODETestCase(unittest.TestCase): + def setUp(self): + self.prob = om.Problem() + + aviary_options = get_option_defaults() + default_mission_subsystems = get_default_mission_subsystems( + 'GASP', build_engine_deck(aviary_options)) + + self.fp = self.prob.model = FlightPathODE(num_nodes=2, + aviary_options=get_option_defaults(), + core_subsystems=default_mission_subsystems) + + def test_case1(self): + """ + ground_roll = False (the aircraft is not confined to the ground) + """ + self.prob.setup(check=False, force_alloc_complex=True) + + set_params_for_unit_tests(self.prob) + + self.prob.set_val(Dynamic.Mission.VELOCITY, [100, 100], units="kn") + self.prob.set_val(Dynamic.Mission.MASS, [100000, 100000], units="lbm") + self.prob.set_val(Dynamic.Mission.ALTITUDE, [500, 500], units="ft") + + self.prob.run_model() + testvals = { + Dynamic.Mission.VELOCITY_RATE: [14.0673, 14.0673], + Dynamic.Mission.FLIGHT_PATH_ANGLE_RATE: [-0.1429133, -0.1429133], + Dynamic.Mission.ALTITUDE_RATE: [0.0, 0.0], + Dynamic.Mission.DISTANCE_RATE: [168.781, 168.781], + "normal_force": [74910.12, 74910.12], + "fuselage_pitch": [0.0, 0.0], + "load_factor": [0.2508988, 0.2508988], + Dynamic.Mission.ALTITUDE_RATE: [0.0, 0.0], + Dynamic.Mission.ALTITUDE_RATE_MAX: [-0.01812796, -0.01812796], + } + check_prob_outputs(self.prob, testvals, rtol=1e-6) + + tol = 1e-6 + assert_near_equal( + self.prob[Dynamic.Mission.ALTITUDE_RATE], np.array( + [0, 0]), tol + ) + + partial_data = self.prob.check_partials( + out_stream=None, method="cs", excludes=["*USatm*", "*params*", "*aero*"] + ) + assert_check_partials(partial_data, atol=1e-8, rtol=1e-8) + + def test_case2(self): + """ + ground_roll = True (the aircraft is confined to the ground) + """ + self.fp.options["ground_roll"] = True + self.prob.setup(check=False, force_alloc_complex=True) + + set_params_for_unit_tests(self.prob) + + self.prob.set_val(Dynamic.Mission.VELOCITY, [100, 100], units="kn") + self.prob.set_val(Dynamic.Mission.MASS, [100000, 100000], units="lbm") + self.prob.set_val(Dynamic.Mission.ALTITUDE, [500, 500], units="ft") + + self.prob.run_model() + testvals = { + Dynamic.Mission.VELOCITY_RATE: [13.58489, 13.58489], + Dynamic.Mission.DISTANCE_RATE: [168.781, 168.781], + "normal_force": [74910.12, 74910.12], + "fuselage_pitch": [0.0, 0.0], + "load_factor": [0.2508988, 0.2508988], + Dynamic.Mission.ALTITUDE_RATE_MAX: [0.7532356, 0.7532356], + } + check_prob_outputs(self.prob, testvals, rtol=1e-6) + + partial_data = self.prob.check_partials( + out_stream=None, method="cs", excludes=["*USatm*", "*params*", "*aero*"] + ) + assert_check_partials(partial_data, atol=1e-8, rtol=1e-8) + + +if __name__ == "__main__": + unittest.main() diff --git a/aviary/mission/gasp_based/ode/test/test_groundroll_eom.py b/aviary/mission/gasp_based/ode/test/test_groundroll_eom.py index 03e1917fe..22a6da34b 100644 --- a/aviary/mission/gasp_based/ode/test/test_groundroll_eom.py +++ b/aviary/mission/gasp_based/ode/test/test_groundroll_eom.py @@ -1,9 +1,8 @@ import unittest -import os import numpy as np import openmdao.api as om -from openmdao.utils.assert_utils import assert_check_partials +from openmdao.utils.assert_utils import assert_check_partials, assert_near_equal from aviary.mission.gasp_based.ode.groundroll_eom import GroundrollEOM from aviary.variable_info.variables import Aircraft, Dynamic @@ -40,6 +39,24 @@ def test_case1(self): tol = 1e-6 self.prob.run_model() + assert_near_equal( + self.prob[Dynamic.Mission.VELOCITY_RATE], np.array( + [1.5597, 1.5597]), tol) + assert_near_equal( + self.prob[Dynamic.Mission.FLIGHT_PATH_ANGLE_RATE], np.array( + [0.0, 0.0]), tol) + assert_near_equal( + self.prob[Dynamic.Mission.ALTITUDE_RATE], np.array( + [0.0, 0.0]), tol) + assert_near_equal( + self.prob[Dynamic.Mission.DISTANCE_RATE], np.array( + [10.0, 10.0]), tol) + assert_near_equal( + self.prob["normal_force"], np.array( + [175200., 175200.]), tol) + assert_near_equal( + self.prob["fuselage_pitch"], np.array( + [0.0, 0.0]), tol) partial_data = self.prob.check_partials(out_stream=None, method="cs") assert_check_partials(partial_data, atol=1e-12, rtol=1e-12) diff --git a/aviary/mission/gasp_based/ode/test/test_groundroll_ode.py b/aviary/mission/gasp_based/ode/test/test_groundroll_ode.py index ffdf4c597..d204fc4c6 100644 --- a/aviary/mission/gasp_based/ode/test/test_groundroll_ode.py +++ b/aviary/mission/gasp_based/ode/test/test_groundroll_ode.py @@ -1,13 +1,16 @@ import unittest +import numpy as np import openmdao.api as om from openmdao.utils.assert_utils import assert_check_partials from aviary.mission.gasp_based.ode.groundroll_ode import GroundrollODE -from aviary.variable_info.options import get_option_defaults +from aviary.mission.gasp_based.ode.params import set_params_for_unit_tests from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems +from aviary.utils.test_utils.IO_test_util import check_prob_outputs from aviary.variable_info.variables import Dynamic +from aviary.variable_info.options import get_option_defaults class GroundrollODETestCase(unittest.TestCase): @@ -26,11 +29,25 @@ def test_groundroll_partials(self): """Check partial derivatives""" self.prob.setup(check=False, force_alloc_complex=True) + set_params_for_unit_tests(self.prob) + self.prob.set_val(Dynamic.Mission.VELOCITY, [100, 100], units="kn") self.prob.set_val("t_curr", [1, 2], units="s") + self.prob.set_val("aircraft:wing:incidence", 0, units="deg") self.prob.run_model() + testvals = { + Dynamic.Mission.VELOCITY_RATE: [1413548.36, 1413548.36], + Dynamic.Mission.FLIGHT_PATH_ANGLE_RATE: [0.0, 0.0], + Dynamic.Mission.ALTITUDE_RATE: [0.0, 0.0], + Dynamic.Mission.DISTANCE_RATE: [168.781, 168.781], + "normal_force": [0.0, 0.0], + "fuselage_pitch": [0.0, 0.0], + "dmass_dv": [-5.03252493e-06, -5.03252493e-06], + } + check_prob_outputs(self.prob, testvals, rtol=1e-6) + partial_data = self.prob.check_partials( out_stream=None, method="cs", excludes=["*params*", "*aero*"] ) diff --git a/aviary/mission/gasp_based/ode/test/test_rotation_eom.py b/aviary/mission/gasp_based/ode/test/test_rotation_eom.py index 41ac6a896..e0ca3294d 100644 --- a/aviary/mission/gasp_based/ode/test/test_rotation_eom.py +++ b/aviary/mission/gasp_based/ode/test/test_rotation_eom.py @@ -3,7 +3,7 @@ import numpy as np import openmdao.api as om -from openmdao.utils.assert_utils import assert_check_partials +from openmdao.utils.assert_utils import assert_check_partials, assert_near_equal from aviary.mission.gasp_based.ode.rotation_eom import RotationEOM from aviary.variable_info.variables import Aircraft, Dynamic @@ -38,6 +38,25 @@ def test_case1(self): tol = 1e-6 self.prob.run_model() + assert_near_equal( + self.prob[Dynamic.Mission.VELOCITY_RATE], np.array( + [1.5597, 1.5597]), tol) + assert_near_equal( + self.prob[Dynamic.Mission.FLIGHT_PATH_ANGLE_RATE], np.array( + [0.0, 0.0]), tol) + assert_near_equal( + self.prob[Dynamic.Mission.ALTITUDE_RATE], np.array( + [0.0, 0.0]), tol) + assert_near_equal( + self.prob[Dynamic.Mission.DISTANCE_RATE], np.array( + [10., 10.]), tol) + assert_near_equal( + self.prob["normal_force"], np.array( + [175200., 175200.]), tol) + assert_near_equal( + self.prob["fuselage_pitch"], np.array( + [0.0, 0.0]), tol) + partial_data = self.prob.check_partials(out_stream=None, method="cs") assert_check_partials(partial_data, atol=1e-12, rtol=1e-12) diff --git a/aviary/mission/gasp_based/ode/test/test_rotation_ode.py b/aviary/mission/gasp_based/ode/test/test_rotation_ode.py index 85e02e16a..b2f71860d 100644 --- a/aviary/mission/gasp_based/ode/test/test_rotation_ode.py +++ b/aviary/mission/gasp_based/ode/test/test_rotation_ode.py @@ -1,13 +1,15 @@ import unittest +import numpy as np import openmdao.api as om -from openmdao.utils.assert_utils import assert_check_partials +from openmdao.utils.assert_utils import assert_check_partials, assert_near_equal from aviary.mission.gasp_based.ode.rotation_ode import RotationODE -from aviary.variable_info.options import get_option_defaults -from aviary.variable_info.variables import Aircraft, Dynamic +from aviary.mission.gasp_based.ode.params import set_params_for_unit_tests from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems +from aviary.variable_info.options import get_option_defaults +from aviary.variable_info.variables import Aircraft, Dynamic class RotationODETestCase(unittest.TestCase): @@ -32,8 +34,30 @@ def test_rotation_partials(self): self.prob.set_val(Dynamic.Mission.VELOCITY, [100, 100], units="kn") self.prob.set_val("t_curr", [1, 2], units="s") + set_params_for_unit_tests(self.prob) + self.prob.run_model() + tol = 1e-6 + assert_near_equal( + self.prob[Dynamic.Mission.VELOCITY_RATE], np.array( + [13.66655, 13.66655]), tol) + assert_near_equal( + self.prob[Dynamic.Mission.FLIGHT_PATH_ANGLE_RATE], np.array( + [0.0, 0.0]), tol) + assert_near_equal( + self.prob[Dynamic.Mission.ALTITUDE_RATE], np.array( + [0.0, 0.0]), tol) + assert_near_equal( + self.prob[Dynamic.Mission.DISTANCE_RATE], np.array( + [168.781, 168.781]), tol) + assert_near_equal( + self.prob["normal_force"], np.array( + [66932.7, 66932.7]), tol) + assert_near_equal( + self.prob["fuselage_pitch"], np.array( + [0.0, 0.0]), tol) + partial_data = self.prob.check_partials( out_stream=None, method="cs", excludes=["*params*", "*aero*"] ) diff --git a/aviary/mission/gasp_based/ode/time_integration_base_classes.py b/aviary/mission/gasp_based/ode/time_integration_base_classes.py index dd381dba1..85ab1e42b 100644 --- a/aviary/mission/gasp_based/ode/time_integration_base_classes.py +++ b/aviary/mission/gasp_based/ode/time_integration_base_classes.py @@ -1,7 +1,9 @@ import numpy as np +from scipy import interpolate + import openmdao.api as om from openmdao.utils import units -from scipy import interpolate + from simupy.block_diagram import DEFAULT_INTEGRATOR_OPTIONS, SimulationMixin from simupy.systems import DynamicalSystem @@ -108,6 +110,14 @@ def __init__( self.prob = prob prob.setup(check=False, force_alloc_complex=True) + + # TODO - This is a hack to mimic the behavior of the old paramport, which + # contains some initial default values. It is unclear how actual "parameter" + # values are supposed to propagate from the pre-mission and top ivcs into + # the SGM phases. + from aviary.mission.gasp_based.ode.params import set_params_for_unit_tests + set_params_for_unit_tests(prob) + prob.final_setup() if triggers is None: @@ -229,7 +239,7 @@ def __init__( self.dim_parameters = len(parameters) # TODO: add defensive checks to make sure dimensions match in both setup and # calls - if verbosity.value >= 2: + if verbosity >= Verbosity.VERBOSE: if problem_name: problem_name = '_'+problem_name om.n2(prob, outfile="n2_simupy_problem" + @@ -490,6 +500,16 @@ def setup_params( continue traj_promote_initial_input[prom_name] = data + # TODO - This is a hack to mimic the behavior of the old paramport, which + # contains some initial default values. It is unclear how actual "parameter" + # values are supposed to propagate from the pre-mission and top ivcs into + # the SGM phases. + from aviary.mission.gasp_based.ode.params import params_for_unit_tests + traj_promote_initial_input = { + **params_for_unit_tests, + **traj_promote_initial_input + } + self.traj_promote_initial_input = { **self.options["param_dict"], **traj_promote_initial_input} for name, kwargs in self.traj_promote_initial_input.items(): @@ -599,7 +619,7 @@ def compute_params(self, inputs): try: ode.set_val(input, inputs[input]) except KeyError: - if self.verbosity.value >= 2: + if self.verbosity >= Verbosity.VERBOSE: print( "*** Input not found:", ode, @@ -608,7 +628,7 @@ def compute_params(self, inputs): pass def compute_traj_loop(self, first_problem, inputs, outputs, t0=0., state0=None): - if self.verbosity.value >= 2: + if self.verbosity >= Verbosity.VERBOSE: print("initializing compute_traj_loop") sim_results = [] sim_problems = [first_problem] @@ -649,7 +669,7 @@ def compute_traj_loop(self, first_problem, inputs, outputs, t0=0., state0=None): try: try_next_problem = (yield current_problem, sim_result) except GeneratorExit: - if self.verbosity.value >= 2: + if self.verbosity >= 2: print("stop iteration 1") break @@ -659,11 +679,11 @@ def compute_traj_loop(self, first_problem, inputs, outputs, t0=0., state0=None): try: next_problem = (yield current_problem, sim_result) except GeneratorExit: - if self.verbosity.value >= 2: + if self.verbosity >= Verbosity.VERBOSE: print("stop iteration 2") break - if self.verbosity.value >= 2: + if self.verbosity >= Verbosity.VERBOSE: print(" was on problem:", current_problem, "\n got back:", next_problem) # compute the output at the final condition to make sure all outputs are current @@ -676,7 +696,7 @@ def compute_traj_loop(self, first_problem, inputs, outputs, t0=0., state0=None): ).squeeze() sim_problems.append(next_problem) - if self.verbosity.value >= 2: + if self.verbosity >= Verbosity.VERBOSE: print("ended loop") # wrap main loop @@ -935,7 +955,7 @@ def compute_partials(self, inputs, J): else: df_dparams.append(None) - if self.verbosity is Verbosity.DEBUG: + if self.verbosity == Verbosity.DEBUG: print("data....") print("dgs", dg_dxs) print("f-", f_minuses) @@ -954,7 +974,7 @@ def compute_partials(self, inputs, J): lamda_dot_plus = np.zeros_like(costate) # self.sim_results[-1].x[-1, next_prob.state_names.index(output)] - if self.verbosity.value >= 2: + if self.verbosity >= Verbosity.VERBOSE: print("\nstarting partial for %s" % output, costate) dg_dt = 0. @@ -1004,7 +1024,7 @@ def compute_partials(self, inputs, J): if channel_name != prob.t_name: lamda_dot = df_dx(res.t[-1]) @ costate # lamda_dot_plus = lamda_dot - if self.verbosity is Verbosity.DEBUG: + if self.verbosity == Verbosity.DEBUG: if np.any(state_disc): print("update is non-zero!", prob, prob.state_names, state_disc, costate, lamda_dot) @@ -1036,7 +1056,7 @@ def compute_partials(self, inputs, J): in self.traj_event_trigger_input ): event_trigger_name = self.traj_event_trigger_input[event_key]["name"] - if self.verbosity.value >= 2: + if self.verbosity >= Verbosity.VERBOSE: print("setting event trigger data", event_trigger_name) J[output_name, event_trigger_name] = ( + costate[None, :] @ (f_minus - f_plus) / @@ -1053,7 +1073,7 @@ def compute_partials(self, inputs, J): def co_state_rate(t, costate, *args): return df_dx(t) @ costate - if self.verbosity.value >= 2: + if self.verbosity >= Verbosity.VERBOSE: print('dim_state:', prob.dim_state, "ic:", costate) costate_sys = DynamicalSystem(state_equation_function=co_state_rate, diff --git a/aviary/mission/gasp_based/ode/unsteady_solved/gamma_comp.py b/aviary/mission/gasp_based/ode/unsteady_solved/gamma_comp.py index 527a049d8..50755d448 100644 --- a/aviary/mission/gasp_based/ode/unsteady_solved/gamma_comp.py +++ b/aviary/mission/gasp_based/ode/unsteady_solved/gamma_comp.py @@ -47,5 +47,5 @@ def compute_partials(self, inputs, partials): d2h_dr2 = inputs["d2h_dr2"] partials[Dynamic.Mission.FLIGHT_PATH_ANGLE, "dh_dr"] = 1. / (dh_dr**2 + 1) - partials["dgam_dr", "dh_dr"] = -d2h_dr2 * dh_dr**2 / (dh_dr**2 + 1)**2 + partials["dgam_dr", "dh_dr"] = -d2h_dr2 * dh_dr * 2 / (dh_dr**2 + 1)**2 partials["dgam_dr", "d2h_dr2"] = 1. / (dh_dr**2 + 1) diff --git a/aviary/mission/gasp_based/ode/unsteady_solved/test/test_gamma_comp.py b/aviary/mission/gasp_based/ode/unsteady_solved/test/test_gamma_comp.py index f128ca150..9e0e21921 100644 --- a/aviary/mission/gasp_based/ode/unsteady_solved/test/test_gamma_comp.py +++ b/aviary/mission/gasp_based/ode/unsteady_solved/test/test_gamma_comp.py @@ -5,6 +5,7 @@ from openmdao.utils.assert_utils import (assert_check_partials, assert_near_equal) +from aviary.mission.gasp_based.ode.unsteady_solved.gamma_comp import GammaComp from aviary.mission.gasp_based.ode.unsteady_solved.unsteady_solved_eom import \ UnsteadySolvedEOM from aviary.variable_info.variables import Aircraft, Dynamic, Mission @@ -92,6 +93,31 @@ def test_unsteady_flight_eom(self): with self.subTest(msg=f"ground_roll={ground_roll}"): self._test_unsteady_flight_eom(ground_roll=ground_roll) + def test_gamma_comp(self): + nn = 2 + + p = om.Problem() + p.model.add_subsystem("gamma", + GammaComp(num_nodes=nn), + promotes_inputs=[ + "dh_dr", + "d2h_dr2"], + promotes_outputs=[ + Dynamic.Mission.FLIGHT_PATH_ANGLE, + "dgam_dr"]) + p.setup(force_alloc_complex=True) + p.run_model() + + assert_near_equal( + p[Dynamic.Mission.FLIGHT_PATH_ANGLE], [0.78539816, 0.78539816], + tolerance=1.0E-6) + assert_near_equal( + p["dgam_dr"], [0.5, 0.5], + tolerance=1.0E-6) + + partial_data = p.check_partials(out_stream=None, method="cs") + assert_check_partials(partial_data, atol=1e-12, rtol=1e-12) + if __name__ == '__main__': unittest.main() diff --git a/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_alpha_thrust_iter_group.py b/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_alpha_thrust_iter_group.py index 4cc49383c..aec538e3a 100644 --- a/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_alpha_thrust_iter_group.py +++ b/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_alpha_thrust_iter_group.py @@ -1,11 +1,12 @@ import unittest import numpy as np + import openmdao.api as om -from openmdao.utils.assert_utils import assert_near_equal +from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials from aviary.constants import GRAV_ENGLISH_LBM -from aviary.mission.gasp_based.ode.params import ParamPort +from aviary.mission.gasp_based.ode.params import set_params_for_unit_tests from aviary.mission.gasp_based.ode.unsteady_solved.unsteady_control_iter_group import \ UnsteadyControlIterGroup from aviary.mission.gasp_based.ode.unsteady_solved.unsteady_solved_flight_conditions import \ @@ -27,11 +28,6 @@ def _test_unsteady_alpha_thrust_iter_group(self, ground_roll=False): p = om.Problem() - # TODO: paramport - param_port = ParamPort() - - p.model.add_subsystem("params", param_port, promotes=["*"]) - fc = UnsteadySolvedFlightConditions(num_nodes=nn, input_speed_type=SpeedType.TAS, ground_roll=ground_roll) @@ -46,16 +42,18 @@ def _test_unsteady_alpha_thrust_iter_group(self, ground_roll=False): aviary_options=get_option_defaults(), core_subsystems=[aero]) - p.model.add_subsystem("iter_group", - subsys=g, - promotes_inputs=["*"], - promotes_outputs=["*"]) + ig = p.model.add_subsystem("iter_group", + subsys=g, + promotes_inputs=["*"], + promotes_outputs=["*"]) - for key, data in param_port.param_data.items(): - p.model.set_input_defaults(key, **data) + if ground_roll: + ig.set_input_defaults("alpha", np.zeros(nn), units="deg") p.setup(force_alloc_complex=True) + set_params_for_unit_tests(p) + p.final_setup() p.set_val(Dynamic.Mission.SPEED_OF_SOUND, 968.076 * np.ones(nn), units="ft/s") @@ -97,7 +95,12 @@ def _test_unsteady_alpha_thrust_iter_group(self, ground_roll=False): # 2. Test that forces balance normal to the velocity axis assert_near_equal(lift + thrust_req * s_alphai, weight * c_gamma) + cpd = p.check_partials(out_stream=None, method="cs", step=1.01e-40, + excludes=["*params*", "*aero*"]) + assert_check_partials(cpd, atol=1e-10, rtol=1e-10) + def test_iter_group(self): + # issue #494: why not ground_roll in [True] ? for ground_roll in [False]: with self.subTest(msg=f"ground_roll={ground_roll}"): self._test_unsteady_alpha_thrust_iter_group(ground_roll=ground_roll) @@ -105,5 +108,3 @@ def test_iter_group(self): if __name__ == '__main__': unittest.main() - # test = TestUnsteadyAlphaThrustIterGroup() - # test._test_unsteady_alpha_thrust_iter_group() diff --git a/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_flight_conditions.py b/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_flight_conditions.py index 00a92d027..bba12a4c8 100644 --- a/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_flight_conditions.py +++ b/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_flight_conditions.py @@ -73,8 +73,6 @@ def _test_unsteady_flight_conditions(self, ground_roll=False, input_speed_type=S assert_near_equal(eas, tas * np.sqrt(rho / rho_sl)) assert_near_equal(dTAS_dt_approx, np.zeros(nn)) - p.run_model() - with np.printoptions(linewidth=1024): cpd = p.check_partials(method="cs") assert_check_partials(cpd) diff --git a/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_solved_ode.py b/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_solved_ode.py index 24a28fe5b..8089fe272 100644 --- a/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_solved_ode.py +++ b/aviary/mission/gasp_based/ode/unsteady_solved/test/test_unsteady_solved_ode.py @@ -1,11 +1,12 @@ import unittest import numpy as np + import openmdao.api as om -from openmdao.utils.assert_utils import assert_near_equal +from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials from aviary.constants import GRAV_ENGLISH_LBM -from aviary.mission.gasp_based.ode.params import ParamPort +from aviary.mission.gasp_based.ode.params import set_params_for_unit_tests from aviary.mission.gasp_based.ode.unsteady_solved.unsteady_solved_ode import \ UnsteadySolvedODE from aviary.variable_info.options import get_option_defaults @@ -36,14 +37,15 @@ def _test_unsteady_solved_ode(self, ground_roll=False, input_speed_type=SpeedTyp p.model.add_subsystem("ode", ode, promotes=["*"]) - # TODO: paramport - param_port = ParamPort() - for key, data in param_port.param_data.items(): - p.model.set_input_defaults(key, **data) p.model.set_input_defaults(Dynamic.Mission.MACH, 0.8 * np.ones(nn)) + if ground_roll: + p.model.set_input_defaults(Dynamic.Mission.MACH, 0.1 * np.ones(nn)) + ode.set_input_defaults("alpha", np.zeros(nn), units="deg") p.setup(force_alloc_complex=True) + set_params_for_unit_tests(p) + p.final_setup() p.set_val(Dynamic.Mission.SPEED_OF_SOUND, 968.076 * np.ones(nn), units="ft/s") @@ -100,8 +102,16 @@ def _test_unsteady_solved_ode(self, ground_roll=False, input_speed_type=SpeedTyp # 5. Test that fuelflow (lbf/s) * dt_dr (s/ft) is equal to dmass_dr assert_near_equal(fuelflow * dt_dr, dmass_dr, tolerance=1.0E-12) + cpd = p.check_partials(out_stream=None, method="cs", + excludes=["*params*", "*aero*"]) + # issue #495 + # dTAS_dt_approx wrt flight_path_angle | abs | fwd-fd | 1.8689625335382314 + # dTAS_dt_approx wrt flight_path_angle | rel | fwd-fd | 1.0 + # assert_check_partials(cpd, atol=1e-6, rtol=1e-6) + def test_steady_level_flight(self): + # issue #494: why not ground_roll in [True] ? for ground_roll in [False]: with self.subTest(msg=f"ground_roll={ground_roll}"): self._test_unsteady_solved_ode(ground_roll=ground_roll) diff --git a/aviary/mission/gasp_based/ode/unsteady_solved/unsteady_solved_ode.py b/aviary/mission/gasp_based/ode/unsteady_solved/unsteady_solved_ode.py index 62602fd48..fcc5c10ce 100644 --- a/aviary/mission/gasp_based/ode/unsteady_solved/unsteady_solved_ode.py +++ b/aviary/mission/gasp_based/ode/unsteady_solved/unsteady_solved_ode.py @@ -11,26 +11,28 @@ from aviary.mission.gasp_based.ode.unsteady_solved.unsteady_solved_eom import UnsteadySolvedEOM from aviary.variable_info.enums import SpeedType, LegacyCode from aviary.variable_info.variables import Dynamic -from aviary.variable_info.variables_in import VariablesIn from aviary.subsystems.aerodynamics.aerodynamics_builder import AerodynamicsBuilderBase from aviary.subsystems.propulsion.propulsion_builder import PropulsionBuilderBase from aviary.variable_info.variable_meta_data import _MetaData class UnsteadySolvedODE(BaseODE): - """ This 2D aircraft ODE provides the rate of change of time per unit range covered. + """ + This 2D aircraft ODE provides the rate of change of time per unit range covered. - Altitude and velocity at various points along the trajectory are provided, along with their corresponding - rates of change. The latter are automatically generated by differentiating the interpolating polynomials - fit to the values. Range is the integration variable for this ODE. + Altitude and velocity at various points along the trajectory are provided, along with + their corresponding rates of change. The latter are automatically generated by + differentiating the interpolating polynomials fit to the values. Range is the + integration variable for this ODE. - For the given altitude/velocity trajectory, this ODE then provides the alpha and thrust history that make that - given trajectory physically realizable. + For the given altitude/velocity trajectory, this ODE then provides the alpha and + thrust history that make that given trajectory physically realizable. - Thrust is allowed to take on physically-nonsensical values (negative, or extremely large magnitudes) to provide - robust convergence of the nonlinear solver. This discrepancy is then resolved by imposing a path constraint that the - thrust needed to perform the trajectory equals the thrust generated by the propulsion system with its given - settings. + Thrust is allowed to take on physically-nonsensical values (negative, or extremely + large magnitudes) to provide robust convergence of the nonlinear solver. This + discrepancy is then resolved by imposing a path constraint that the thrust needed to + perform the trajectory equals the thrust generated by the propulsion system with its + given settings. """ def initialize(self): @@ -39,27 +41,34 @@ def initialize(self): "ground_roll", types=bool, default=False, - desc="True if the aircraft is confined to the ground. Removes altitude rate as an " - "output and adjusts the TAS rate equation.") + desc="True if the aircraft is confined to the ground. Removes altitude rate " + "as an output and adjusts the TAS rate equation.", + ) self.options.declare( "clean", types=bool, default=False, - desc="If true then no flaps or gear are included. Useful for high-speed flight phases.") + desc="If true then no flaps or gear are included. Useful for high-speed " + "flight phases.", + ) self.options.declare( "include_param_comp", types=bool, default=True, - desc="If true then add a ParamComp to this ODE. Useful for smaller usages of this ODE not within a full trajectory or a static analysis group.") + desc="If true then add a ParamComp to this ODE. Useful for smaller usages " + "of this ODE not within a full trajectory or a pre-mission group.", + ) self.options.declare( "input_speed_type", default=SpeedType.TAS, types=SpeedType, desc="Airspeed type specified as input.") self.options.declare( - 'throttle_enforcement', default='path_constraint', + 'throttle_enforcement', + default='path_constraint', values=['path_constraint', 'boundary_constraint', 'bounded', None], - desc='flag to enforce throttle constraints on the path or at the segment boundaries or using solver bounds' + desc='flag to enforce throttle constraints on the path or at the segment ' + 'boundaries or using solver bounds', ) self.options.declare( 'external_subsystems', default=[], @@ -81,12 +90,6 @@ def setup(self): # TODO: paramport self.add_subsystem("params", ParamPort(), promotes=["*"]) - self.add_subsystem( - 'input_port', - VariablesIn(aviary_options=aviary_options), - promotes_inputs=['*'], - promotes_outputs=['*']) - self.add_subsystem( name='atmosphere', subsys=Atmosphere(num_nodes=nn, output_dsos_dh=True), @@ -121,23 +124,25 @@ def setup(self): promotes_inputs=["*"], promotes_outputs=["*"]) - # Also need to change the run script and the iter group solver when using this; just testing for now + # Also need to change the run script and the iter group solver when using this; + # just testing for now throttle_balance_group = self.add_subsystem("throttle_balance_group", om.Group(), promotes=["*"]) throttle_balance_comp = om.BalanceComp() - throttle_balance_comp.add_balance(Dynamic.Mission.THROTTLE, - units="unitless", - val=np.ones(nn) * 0.5, - lhs_name=Dynamic.Mission.THRUST_TOTAL, - rhs_name="thrust_req", - eq_units="lbf", - normalize=False, - lower=0.0 if throttle_enforcement == 'bounded' else None, - upper=1.0 if throttle_enforcement == 'bounded' else None, - res_ref=1.0e6, - ) + throttle_balance_comp.add_balance( + Dynamic.Mission.THROTTLE, + units="unitless", + val=np.ones(nn) * 0.5, + lhs_name=Dynamic.Mission.THRUST_TOTAL, + rhs_name="thrust_req", + eq_units="lbf", + normalize=False, + lower=0.0 if throttle_enforcement == 'bounded' else None, + upper=1.0 if throttle_enforcement == 'bounded' else None, + res_ref=1.0e6, + ) throttle_balance_group.add_subsystem("throttle_balance_comp", subsys=throttle_balance_comp, promotes_inputs=["*"], diff --git a/aviary/mission/gasp_based/phases/landing_group.py b/aviary/mission/gasp_based/phases/landing_group.py index 2ece2fd72..b3c3b74fe 100644 --- a/aviary/mission/gasp_based/phases/landing_group.py +++ b/aviary/mission/gasp_based/phases/landing_group.py @@ -197,7 +197,9 @@ def setup(self): ) ParamPort.set_default_vals(self) + self.set_input_defaults(Mission.Landing.INITIAL_MACH, val=0.1) + # landing doesn't change flap or gear position self.set_input_defaults("t_init_flaps_app", val=1e10) self.set_input_defaults("t_init_gear_app", val=1e10) @@ -207,3 +209,5 @@ def setup(self): self.set_input_defaults('aero_ramps.gear_factor:final_val', val=1.) self.set_input_defaults('aero_ramps.flap_factor:initial_val', val=0.) self.set_input_defaults('aero_ramps.gear_factor:initial_val', val=0.) + + self.set_input_defaults(Aircraft.Wing.AREA, val=1.0, units="ft**2") diff --git a/aviary/mission/gasp_based/phases/test/test_breguet.py b/aviary/mission/gasp_based/phases/test/test_breguet.py index 0f5e208d7..673ffd411 100644 --- a/aviary/mission/gasp_based/phases/test/test_breguet.py +++ b/aviary/mission/gasp_based/phases/test/test_breguet.py @@ -1,5 +1,4 @@ import unittest -import os import numpy as np import openmdao.api as om diff --git a/aviary/mission/gasp_based/phases/test/test_landing_components.py b/aviary/mission/gasp_based/phases/test/test_landing_components.py index 1507bc7d4..83049bd75 100644 --- a/aviary/mission/gasp_based/phases/test/test_landing_components.py +++ b/aviary/mission/gasp_based/phases/test/test_landing_components.py @@ -1,5 +1,4 @@ import unittest -import os import openmdao.api as om from openmdao.utils.assert_utils import (assert_check_partials, diff --git a/aviary/mission/gasp_based/phases/test/test_landing_group.py b/aviary/mission/gasp_based/phases/test/test_landing_group.py index 3389023c9..f12714b0f 100644 --- a/aviary/mission/gasp_based/phases/test/test_landing_group.py +++ b/aviary/mission/gasp_based/phases/test/test_landing_group.py @@ -1,19 +1,19 @@ +from packaging import version import unittest -import os import numpy as np + import openmdao import openmdao.api as om from openmdao.utils.assert_utils import assert_check_partials -from packaging import version +from aviary.mission.gasp_based.ode.params import set_params_for_unit_tests from aviary.mission.gasp_based.phases.landing_group import LandingSegment -from aviary.variable_info.options import get_option_defaults +from aviary.subsystems.propulsion.utils import build_engine_deck +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems from aviary.utils.test_utils.IO_test_util import check_prob_outputs +from aviary.variable_info.options import get_option_defaults from aviary.variable_info.variables import Dynamic, Mission -from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems -from aviary.subsystems.aerodynamics.aerodynamics_builder import AerodynamicsBuilderBase -from aviary.subsystems.propulsion.utils import build_engine_deck class DLandTestCase(unittest.TestCase): @@ -32,6 +32,8 @@ def setUp(self): def test_dland(self): self.prob.setup(check=False, force_alloc_complex=True) + set_params_for_unit_tests(self.prob) + self.prob.set_val(Mission.Landing.AIRPORT_ALTITUDE, 0, units="ft") self.prob.set_val(Mission.Landing.INITIAL_MACH, 0.1, units="unitless") self.prob.set_val("alpha", 0, units="deg") # doesn't matter @@ -47,9 +49,9 @@ def test_dland(self): self.prob.run_model() testvals = { - Mission.Landing.INITIAL_VELOCITY: 142.74 * 1.68781, - "TAS_touchdown": 126.27 * 1.68781, - "theta": np.deg2rad(3.57), + Mission.Landing.INITIAL_VELOCITY: 240.9179994, # ft/s (142.74 knot) + "TAS_touchdown": 213.1197687, # ft/s (126.27 knot) + "theta": 0.06230825, # rad (3.57 deg) "flare_alt": 20.8, "ground_roll_distance": 1798, Mission.Landing.GROUND_DISTANCE: 2980, diff --git a/aviary/mission/gasp_based/phases/test/test_taxi_component.py b/aviary/mission/gasp_based/phases/test/test_taxi_component.py index 01339e7be..e5b8187e3 100644 --- a/aviary/mission/gasp_based/phases/test/test_taxi_component.py +++ b/aviary/mission/gasp_based/phases/test/test_taxi_component.py @@ -1,5 +1,4 @@ import unittest -import os import openmdao.api as om from openmdao.utils.assert_utils import (assert_check_partials, diff --git a/aviary/mission/gasp_based/phases/test/test_taxi_group.py b/aviary/mission/gasp_based/phases/test/test_taxi_group.py index 32df5ed26..950f9f350 100644 --- a/aviary/mission/gasp_based/phases/test/test_taxi_group.py +++ b/aviary/mission/gasp_based/phases/test/test_taxi_group.py @@ -1,18 +1,21 @@ +from packaging import version import unittest -import os import numpy as np + import openmdao import openmdao.api as om from openmdao.utils.assert_utils import assert_check_partials -from packaging import version +from aviary.mission.gasp_based.ode.params import set_params_for_unit_tests from aviary.mission.gasp_based.phases.landing_group import LandingSegment -from aviary.variable_info.options import get_option_defaults -from aviary.utils.test_utils.IO_test_util import check_prob_outputs -from aviary.variable_info.variables import Dynamic, Mission from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems +from aviary.utils.test_utils.IO_test_util import check_prob_outputs +from aviary.variable_info.options import get_option_defaults +from aviary.variable_info.variables import Dynamic, Mission + +# TODO - Why does this test landing instead of taxi? class DLandTestCase(unittest.TestCase): @@ -31,6 +34,8 @@ def setUp(self): def test_dland(self): self.prob.setup(check=False, force_alloc_complex=True) + set_params_for_unit_tests(self.prob) + self.prob.set_val(Mission.Landing.AIRPORT_ALTITUDE, 0, units="ft") self.prob.set_val(Mission.Landing.INITIAL_MACH, 0.1, units="unitless") self.prob.set_val("alpha", 0, units="deg") # doesn't matter @@ -46,9 +51,9 @@ def test_dland(self): self.prob.run_model() testvals = { - Mission.Landing.INITIAL_VELOCITY: 142.74 * 1.68781, - "TAS_touchdown": 126.27 * 1.68781, - "theta": np.deg2rad(3.57), + Mission.Landing.INITIAL_VELOCITY: 240.9179994, # ft/s (142.74 knot) + "TAS_touchdown": 213.1197687, # ft/s (126.27 knot) + "theta": 0.06230825, # rad (3.57 deg) "flare_alt": 20.8, "ground_roll_distance": 1798, Mission.Landing.GROUND_DISTANCE: 2980, diff --git a/aviary/mission/gasp_based/phases/time_integration_phases.py b/aviary/mission/gasp_based/phases/time_integration_phases.py index 25ab7eda5..ca8a10f39 100644 --- a/aviary/mission/gasp_based/phases/time_integration_phases.py +++ b/aviary/mission/gasp_based/phases/time_integration_phases.py @@ -8,7 +8,7 @@ from aviary.mission.gasp_based.ode.groundroll_ode import GroundrollODE from aviary.mission.gasp_based.ode.rotation_ode import RotationODE from aviary.mission.gasp_based.ode.time_integration_base_classes import SimuPyProblem -from aviary.variable_info.enums import AlphaModes, AnalysisScheme, SpeedType +from aviary.variable_info.enums import AlphaModes, AnalysisScheme, SpeedType, Verbosity from aviary.variable_info.variables import Aircraft, Dynamic, Mission @@ -164,11 +164,11 @@ def update_equation_function(self, t, x, event_channels=None): self.output_nan = True return x elif 1 in event_channels: - if self.verbosity.value >= 2: + if self.verbosity >= Verbosity.VERBOSE: print("flaps!", t) self.set_val("t_init_flaps", t) elif 2 in event_channels: - if self.verbosity.value >= 2: + if self.verbosity >= Verbosity.VERBOSE: print("gear!", t) self.set_val("t_init_gear", t) else: diff --git a/aviary/mission/gasp_based/phases/time_integration_traj.py b/aviary/mission/gasp_based/phases/time_integration_traj.py index 548170ae8..87cd177ba 100644 --- a/aviary/mission/gasp_based/phases/time_integration_traj.py +++ b/aviary/mission/gasp_based/phases/time_integration_traj.py @@ -1,6 +1,7 @@ import numpy as np from aviary.mission.gasp_based.ode.time_integration_base_classes import SGMTrajBase from aviary.mission.gasp_based.phases.time_integration_phases import SGMGroundroll, SGMRotation +from aviary.variable_info.enums import Verbosity class TimeIntegrationTrajBase(SGMTrajBase): @@ -72,7 +73,7 @@ def compute(self, inputs, outputs): ode_index = 0 sim_gen = self.compute_traj_loop(self.ODEs[0], inputs, outputs) - if self.verbosity.value >= 1: + if self.verbosity >= Verbosity.BRIEF: print('*'*40) print('Starting: '+self.ODEs[ode_index].phase_name) for current_problem, sim_result in sim_gen: @@ -87,7 +88,7 @@ def compute(self, inputs, outputs): except IndexError: next_problem = None - if self.verbosity.value >= 1: + if self.verbosity >= Verbosity.BRIEF: print('Finished: '+current_problem.phase_name) if next_problem is not None: @@ -96,16 +97,16 @@ def compute(self, inputs, outputs): elif type(current_problem) is SGMRotation: next_problem.rotation.set_val("start_rotation", t_start_rotation) - if self.verbosity.value >= 1: + if self.verbosity >= Verbosity.BRIEF: print('Starting: '+next_problem.phase_name) sim_gen.send(next_problem) else: - if self.verbosity.value >= 1: + if self.verbosity >= Verbosity.BRIEF: print('Reached the end of the Trajectory!') sim_gen.close() break - if self.verbosity.value >= 1: + if self.verbosity >= Verbosity.BRIEF: print('t_final', t_final) print('x_final', x_final) print(self.ODEs[-1].states) diff --git a/aviary/mission/gasp_based/polynomial_fit.py b/aviary/mission/gasp_based/polynomial_fit.py index 9d5c21071..194222b64 100644 --- a/aviary/mission/gasp_based/polynomial_fit.py +++ b/aviary/mission/gasp_based/polynomial_fit.py @@ -5,6 +5,7 @@ class PolynomialFit(om.ImplicitComponent): def initialize(self): + self.options.declare("N_cp", types=int) def setup(self): @@ -24,8 +25,8 @@ def setup(self): # these are the coefficients of the polynomial function you are fitting self.add_output("A", np.zeros(4)) # assuming a 5th order polynomial - # analytic derivatives are left as an exercize for the Daniel # using CS here will give accurate partials, but will miss the sparsity pattern + # issue #497 self.declare_partials("*", "*", method="cs") self.linear_solver = om.DirectSolver() diff --git a/aviary/mission/gasp_based/test/test_idle_descent_estimation.py b/aviary/mission/gasp_based/test/test_idle_descent_estimation.py index 4871a3e0a..a8746b1f8 100644 --- a/aviary/mission/gasp_based/test/test_idle_descent_estimation.py +++ b/aviary/mission/gasp_based/test/test_idle_descent_estimation.py @@ -3,19 +3,17 @@ import importlib import openmdao.api as om -from aviary.interface.default_phase_info.two_dof_fiti_deprecated import create_2dof_based_descent_phases -from aviary.interface.default_phase_info.two_dof_fiti import descent_phases, add_default_sgm_args +from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials -from openmdao.utils.assert_utils import assert_near_equal +from aviary.interface.default_phase_info.two_dof_fiti import descent_phases, add_default_sgm_args -from aviary.subsystems.propulsion.utils import build_engine_deck -from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems -from aviary.mission.gasp_based.idle_descent_estimation import descent_range_and_fuel, add_descent_estimation_as_submodel +from aviary.mission.gasp_based.idle_descent_estimation import add_descent_estimation_as_submodel +from aviary.mission.gasp_based.ode.params import set_params_for_unit_tests from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.variable_info.variables import Aircraft, Dynamic, Settings -from aviary.variable_info.enums import Verbosity from aviary.utils.process_input_decks import create_vehicle from aviary.utils.preprocessors import preprocess_propulsion +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems @unittest.skipUnless(importlib.util.find_spec("pyoptsparse") is not None, "pyoptsparse is not installed") @@ -23,7 +21,7 @@ class IdleDescentTestCase(unittest.TestCase): def setUp(self): input_deck = 'models/large_single_aisle_1/large_single_aisle_1_GwGm.csv' aviary_inputs, _ = create_vehicle(input_deck) - aviary_inputs.set_val(Settings.VERBOSITY, Verbosity.QUIET) + aviary_inputs.set_val(Settings.VERBOSITY, 0) aviary_inputs.set_val(Aircraft.Engine.SCALED_SLS_THRUST, val=28690, units="lbf") aviary_inputs.set_val(Dynamic.Mission.THROTTLE, val=0, units="unitless") @@ -43,14 +41,6 @@ def setUp(self): add_default_sgm_args(descent_phases, self.ode_args) self.phases = descent_phases - def test_case1(self): - - results = descent_range_and_fuel(phases=self.phases)['refined_guess'] - - # Values obtained by running idle_descent_estimation - assert_near_equal(results['distance_flown'], 91.8911599691433, self.tol) - assert_near_equal(results['fuel_burned'], 236.73893823639082, self.tol) - def test_subproblem(self): prob = om.Problem() prob.model = om.Group() @@ -70,6 +60,8 @@ def test_subproblem(self): prob.setup() + set_params_for_unit_tests(prob) + warnings.filterwarnings('ignore', category=UserWarning) prob.run_model() warnings.filterwarnings('default', category=UserWarning) @@ -78,6 +70,10 @@ def test_subproblem(self): assert_near_equal(prob.get_val('descent_range', 'NM'), 98.38026813, self.tol) assert_near_equal(prob.get_val('descent_fuel', 'lbm'), 250.84809336, self.tol) + # TODO: check_partials() call results in runtime error: Jacobian in 'ODE_group' is not full rank. + # partial_data = prob.check_partials(out_stream=None, method="cs") + # assert_check_partials(partial_data, atol=0.0005, rtol=1e-9) + if __name__ == "__main__": unittest.main() diff --git a/aviary/mission/gasp_based/test/test_polynomial_fit.py b/aviary/mission/gasp_based/test/test_polynomial_fit.py new file mode 100644 index 000000000..dd31f9c1b --- /dev/null +++ b/aviary/mission/gasp_based/test/test_polynomial_fit.py @@ -0,0 +1,56 @@ +import unittest + +import openmdao.api as om +from openmdao.utils.assert_utils import assert_near_equal + +from aviary.mission.gasp_based.polynomial_fit import PolynomialFit + +X_cp = [45.0, 47.21906891, 50.28093109, 51.25, 51.25, 53.46906891, 56.53093109, 57.5, + 57.5, 59.71906891, 62.78093109, 63.75, 63.75, 65.96906891, 69.03093109, 70.0] +Y_cp = [0.0, 44.38137822, 105.61862178, 125.0, 125.0, 169.38137822, 230.61862178, 250.0, + 250.0, 294.38137822, 355.61862178, 375.0, 375.0, 419.38137822, 480.61862178, 500.0] + + +class PolynomialFitTest(unittest.TestCase): + def setUp(self): + + self.prob = om.Problem() + self.prob.model.add_subsystem("polyfit", PolynomialFit(N_cp=16), promotes=["*"]) + + self.prob.model.set_input_defaults("time_cp", val=X_cp, units="s") + self.prob.model.set_input_defaults("h_cp", val=Y_cp, units="ft") + self.prob.model.set_input_defaults("t_init_gear", val=[15.0000001], units="s") + self.prob.model.set_input_defaults("t_init_flaps", val=[32.5000001], units="s") + + newton = self.prob.model.nonlinear_solver = om.NewtonSolver() + newton.options["atol"] = 1e-6 + newton.options["rtol"] = 1e-6 + newton.options["iprint"] = 2 + newton.options["maxiter"] = 15 + newton.options["solve_subsystems"] = True + newton.options["max_sub_solves"] = 10 + newton.options["err_on_non_converge"] = True + newton.options["reraise_child_analysiserror"] = False + newton.linesearch = om.BoundsEnforceLS() + newton.linesearch.options["bound_enforcement"] = "scalar" + newton.linesearch.options["iprint"] = -1 + newton.options["err_on_non_converge"] = False + + self.prob.model.linear_solver = om.DirectSolver(assemble_jac=True) + + self.prob.setup(check=False, force_alloc_complex=True) + + def test_case1(self): + """ + It checks output values. It will not check partials because cs is used to compute partials already. + """ + + self.prob.run_model() + + tol = 5e-4 + assert_near_equal(self.prob["h_init_gear"], -600, tol) + assert_near_equal(self.prob["h_init_flaps"], -250, tol) + + +if __name__ == "__main__": + unittest.main() diff --git a/aviary/mission/ode/test/test_specific_energy_rate.py b/aviary/mission/ode/test/test_specific_energy_rate.py index 19a2eca32..3618e4c29 100644 --- a/aviary/mission/ode/test/test_specific_energy_rate.py +++ b/aviary/mission/ode/test/test_specific_energy_rate.py @@ -38,7 +38,6 @@ def test_case1(self): output_keys=Dynamic.Mission.SPECIFIC_ENERGY_RATE, tol=1e-12) - # TODO IO test will fail until mission variable hirerarchy implemented def test_IO(self): assert_match_varnames(self.prob.model) diff --git a/aviary/models/engines/turboprop_1120hp.deck b/aviary/models/engines/turboshaft_1120hp.deck similarity index 99% rename from aviary/models/engines/turboprop_1120hp.deck rename to aviary/models/engines/turboshaft_1120hp.deck index 64cc8376a..c83f813b4 100644 --- a/aviary/models/engines/turboprop_1120hp.deck +++ b/aviary/models/engines/turboshaft_1120hp.deck @@ -1,4 +1,4 @@ -# GASP_TP-derived engine deck converted from MAPS_1120hp.eng +# GASP_TS-derived engine deck converted from MAPS_1120hp.eng # t4max: 50.0 # t4cruise: 46.67 # t4climb: 48.33 diff --git a/aviary/models/engines/turboprop_1120hp_no_tailpipe.deck b/aviary/models/engines/turboshaft_1120hp_no_tailpipe.deck similarity index 99% rename from aviary/models/engines/turboprop_1120hp_no_tailpipe.deck rename to aviary/models/engines/turboshaft_1120hp_no_tailpipe.deck index 5f5df2f58..937c6726f 100644 --- a/aviary/models/engines/turboprop_1120hp_no_tailpipe.deck +++ b/aviary/models/engines/turboshaft_1120hp_no_tailpipe.deck @@ -1,4 +1,4 @@ -# GASP_TP-derived engine deck converted from MAPS_1120hp.eng +# GASP_TS-derived engine deck converted from MAPS_1120hp.eng # t4max: 50.0 # t4cruise: 46.67 # t4climb: 48.33 diff --git a/aviary/models/engines/turboprop_4465hp.deck b/aviary/models/engines/turboshaft_4465hp.deck similarity index 99% rename from aviary/models/engines/turboprop_4465hp.deck rename to aviary/models/engines/turboshaft_4465hp.deck index df8b91ebd..23f1ddba8 100644 --- a/aviary/models/engines/turboprop_4465hp.deck +++ b/aviary/models/engines/turboshaft_4465hp.deck @@ -1,4 +1,4 @@ -# GASP_TP-derived engine deck converted from turboprop_4465hp.eng +# GASP_TS-derived engine deck converted from turboshaft_4465hp.eng # t4max: 50.0 # t4cruise: 45.0 # t4climb: 50.0 diff --git a/aviary/models/engines/turboprop_4465hp.eng b/aviary/models/engines/turboshaft_4465hp.eng similarity index 100% rename from aviary/models/engines/turboprop_4465hp.eng rename to aviary/models/engines/turboshaft_4465hp.eng diff --git a/aviary/models/multi_engine_single_aisle/multi_engine_single_aisle_data.py b/aviary/models/multi_engine_single_aisle/multi_engine_single_aisle_data.py index 1966ed2af..000d8098e 100644 --- a/aviary/models/multi_engine_single_aisle/multi_engine_single_aisle_data.py +++ b/aviary/models/multi_engine_single_aisle/multi_engine_single_aisle_data.py @@ -2,7 +2,7 @@ from aviary.utils.aviary_values import AviaryValues from aviary.utils.functions import get_path -from aviary.variable_info.enums import EquationsOfMotion, LegacyCode, Verbosity +from aviary.variable_info.enums import EquationsOfMotion, LegacyCode from aviary.variable_info.variables import Aircraft, Mission, Settings MultiEngineSingleAisle = {} @@ -283,7 +283,7 @@ # --------------------------- inputs.set_val(Settings.EQUATIONS_OF_MOTION, EquationsOfMotion.HEIGHT_ENERGY) inputs.set_val(Settings.MASS_METHOD, LegacyCode.FLOPS) -inputs.set_val(Settings.VERBOSITY, Verbosity.BRIEF) +inputs.set_val(Settings.VERBOSITY, 0) # --------------------------- # OUTPUTS diff --git a/aviary/models/test_aircraft/aircraft_for_bench_FwGm.csv b/aviary/models/test_aircraft/aircraft_for_bench_FwGm.csv index 1e67d1ee5..5e04bd9ab 100644 --- a/aviary/models/test_aircraft/aircraft_for_bench_FwGm.csv +++ b/aviary/models/test_aircraft/aircraft_for_bench_FwGm.csv @@ -2,7 +2,6 @@ aircraft:controls:cockpit_control_mass_scaler,1,unitless aircraft:controls:control_mass_increment,0,lbm aircraft:controls:stability_augmentation_system_mass,0,lbm aircraft:controls:stability_augmentation_system_mass_scaler,1,unitless -aircraft:crew_and_payload:cargo_mass,10040,lbm aircraft:crew_and_payload:num_passengers,169,unitless aircraft:crew_and_payload:passenger_mass_with_bags,200,lbm aircraft:design:cg_delta,0.25,unitless @@ -13,14 +12,13 @@ aircraft:design:part25_structural_category,3,unitless aircraft:design:reserve_fuel_additional,3000.,lbm aircraft:design:static_margin,0.03,unitless aircraft:design:structural_mass_increment,0,lbm -aircraft:design:supercritical_drag_shift,0.025,unitless +aircraft:design:supercritical_drag_shift,0.033,unitless aircraft:engine:constant_fuel_consumption,0.,lbm/h -aircraft:engine:data_file,models/engines/turbofan_28k.deck,unitless +aircraft:engine:data_file,models/engines/turbofan_23k_1.deck,unitless aircraft:engine:enable_sizing,False,unitless aircraft:engine:mass_specific,0.21366,lbm/lbf aircraft:engine:fuel_flow_scaler_constant_term,0.,unitless aircraft:engine:fuel_flow_scaler_linear_term,0.,unitless -aircraft:engine:generate_flight_idle,True,unitless aircraft:engine:geopotential_alt,False,unitless aircraft:engine:has_propellers,False,unitless aircraft:engine:ignore_negative_thrust,False,unitless @@ -36,6 +34,7 @@ aircraft:engine:pylon_factor,1.25,unitless aircraft:engine:reference_diameter,5.8,ft aircraft:engine:reference_mass,7400,lbm aircraft:engine:reference_sls_thrust,28928.1,lbf +aircraft:engine:scale_factor,1.0,unitless aircraft:engine:scale_mass,True,unitless aircraft:engine:scale_performance,True,unitless aircraft:engine:scaled_sls_thrust,28928.1,lbf @@ -154,6 +153,8 @@ mission:summary:fuel_flow_scaler,1.0,unitless mission:takeoff:decision_speed_increment,10,kn mission:takeoff:rotation_speed_increment,5,kn mission:taxi:duration,0.1677,h + + aircraft:air_conditioning:mass_scaler,1.0,unitless aircraft:anti_icing:mass_scaler,1.0,unitless aircraft:apu:mass_scaler,1.1,unitless @@ -183,9 +184,6 @@ aircraft:design:supersonic_drag_coeff_factor,1.0,unitless aircraft:design:use_alt_mass,False,unitless aircraft:design:zero_lift_drag_coeff_factor,0.930890028006548,unitless aircraft:electrical:mass_scaler,1.25,unitless -aircraft:engine:flight_idle_thrust_fraction,0.0,unitless -aircraft:engine:flight_idle_max_fraction,1.0,unitless -aircraft:engine:flight_idle_min_fraction,0.08,unitless aircraft:fins:area,0.0,ft**2 aircraft:fins:mass_scaler,1.0,unitless aircraft:fins:mass,0.0,lbm @@ -205,11 +203,7 @@ aircraft:fuselage:military_cargo_floor,False,unitless aircraft:fuselage:num_fuselages,1,unitless aircraft:fuselage:passenger_compartment_length,85.5,ft aircraft:fuselage:planform_area,1578.24,ft**2 -aircraft:fuselage:wetted_area_scaler,1.0,unitless -aircraft:fuselage:wetted_area,4158.62,ft**2 aircraft:horizontal_tail:mass_scaler,1.2,unitless -aircraft:horizontal_tail:wetted_area_scaler,1.0,unitless -aircraft:horizontal_tail:wetted_area,592.65,ft**2 aircraft:hydraulics:mass_scaler,1.0,unitless aircraft:hydraulics:system_pressure,3000.0,psi aircraft:instruments:mass_scaler,1.25,unitless @@ -219,14 +213,11 @@ aircraft:landing_gear:main_gear_oleo_length,102.0,inch aircraft:landing_gear:nose_gear_mass_scaler,1.0,unitless aircraft:landing_gear:nose_gear_oleo_length,67.0,inch aircraft:nacelle:mass_scaler,1.0,unitless -aircraft:nacelle:wetted_area_scaler,1.0,unitless aircraft:paint:mass_per_unit_area,0.037,lbm/ft**2 aircraft:propulsion:engine_oil_mass_scaler,1.0,unitless aircraft:propulsion:misc_mass_scaler,1.0,unitless aircraft:vertical_tail:mass_scaler,1.0,unitless aircraft:vertical_tail:num_tails,1,unitless -aircraft:vertical_tail:wetted_area_scaler,1.0,unitless -aircraft:vertical_tail:wetted_area,581.13,ft**2 aircraft:wing:aeroelastic_tailoring_factor,0.0,unitless aircraft:wing:airfoil_technology,1.92669766647637,unitless aircraft:wing:bending_mass_scaler,1.0,unitless @@ -251,12 +242,10 @@ aircraft:wing:thickness_to_chord_dist,0.145,0.115,0.104,unitless aircraft:wing:thickness_to_chord,0.13,unitless aircraft:wing:ultimate_load_factor,3.75,unitless aircraft:wing:var_sweep_mass_penalty,0.0,unitless -aircraft:wing:wetted_area_scaler,1.0,unitless -aircraft:wing:wetted_area,2396.56,ft**2 -mission:constraints:max_mach,0.785,unitless +mission:constraints:max_mach,0.8,unitless mission:design:thrust_takeoff_per_eng,28928.1,lbf mission:landing:lift_coefficient_max,2.0,unitless -mission:summary:cruise_mach,0.785,unitless +mission:summary:cruise_mach,0.8,unitless mission:takeoff:fuel_simple,577,lbm mission:takeoff:lift_coefficient_max,3.0,unitless mission:takeoff:lift_over_drag,17.354,unitless diff --git a/aviary/subsystems/aerodynamics/aerodynamics_builder.py b/aviary/subsystems/aerodynamics/aerodynamics_builder.py index f978576da..9d4dfe375 100644 --- a/aviary/subsystems/aerodynamics/aerodynamics_builder.py +++ b/aviary/subsystems/aerodynamics/aerodynamics_builder.py @@ -7,9 +7,12 @@ CoreAerodynamicsBuilder : the interface for Aviary's core aerodynamics subsystem builder """ +from itertools import chain + import numpy as np import openmdao.api as om +from dymos.utils.misc import _unspecified from aviary.variable_info.variables import Aircraft, Mission, Dynamic @@ -339,14 +342,76 @@ def get_parameters(self, aviary_inputs=None, phase_info=None): params[Aircraft.Design.DRAG_POLAR] = drag_opts if method == 'computed': - param_vars = [Aircraft.Nacelle.CHARACTERISTIC_LENGTH, - Aircraft.Nacelle.FINENESS, - Aircraft.Nacelle.LAMINAR_FLOW_LOWER, - Aircraft.Nacelle.LAMINAR_FLOW_UPPER, - Aircraft.Nacelle.WETTED_AREA] - for var in param_vars: + + for var in COMPUTED_CORE_INPUTS: + + meta = _MetaData[var] + + val = meta['default_value'] + if val is None: + val = _unspecified + units = meta['units'] + + if var in aviary_inputs: + try: + val = aviary_inputs.get_val(var, units) + except TypeError: + val = aviary_inputs.get_val(var) + + params[var] = {'val': val, + 'static_target': True} + + for var in ENGINE_SIZED_INPUTS: params[var] = {'shape': (num_engine_type, ), 'static_target': True} + elif method == "low_speed": + + for var in LOW_SPEED_CORE_INPUTS: + + meta = _MetaData[var] + + val = meta['default_value'] + if val is None: + val = _unspecified + units = meta['units'] + + if var in aviary_inputs: + try: + val = aviary_inputs.get_val(var, units) + except TypeError: + val = aviary_inputs.get_val(var) + + params[var] = {'val': val, + 'static_target': True} + + else: + + # TODO: 2DOF/Gasp decided on phases based on phase names. We used + # a saved phase_name to determine the correct aero variables to + # promote. Ideally, this should all be refactored. + if phase_info['phase_type'] in ['ascent', 'groundroll', 'rotation']: + all_vars = (AERO_2DOF_INPUTS, AERO_LS_2DOF_INPUTS) + else: + all_vars = (AERO_2DOF_INPUTS, AERO_CLEAN_2DOF_INPUTS) + + for var in chain.from_iterable(all_vars): + + meta = _MetaData[var] + + val = meta['default_value'] + if val is None: + val = _unspecified + units = meta['units'] + + if var in aviary_inputs: + try: + val = aviary_inputs.get_val(var, units) + except TypeError: + val = aviary_inputs.get_val(var) + + params[var] = {'val': val, + 'static_target': True} + return params def report(self, prob, reports_folder, **kwargs): @@ -366,3 +431,119 @@ def report(self, prob, reports_folder, **kwargs): elif self.code_origin is GASP: # GASP aero report goes here return + + +# Parameters for drag computation. +COMPUTED_CORE_INPUTS = [ + 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.HorizontalTail.CHARACTERISTIC_LENGTH, + Aircraft.HorizontalTail.FINENESS, + Aircraft.HorizontalTail.LAMINAR_FLOW_LOWER, + Aircraft.HorizontalTail.LAMINAR_FLOW_UPPER, + Aircraft.HorizontalTail.WETTED_AREA, + Aircraft.VerticalTail.CHARACTERISTIC_LENGTH, + Aircraft.VerticalTail.FINENESS, + Aircraft.VerticalTail.LAMINAR_FLOW_LOWER, + Aircraft.VerticalTail.LAMINAR_FLOW_UPPER, + Aircraft.VerticalTail.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, +] + +# Parameters for low speed aero. +LOW_SPEED_CORE_INPUTS = [ + Aircraft.Wing.AREA, + Aircraft.Wing.ASPECT_RATIO, + Aircraft.Wing.HEIGHT, + Aircraft.Wing.SPAN, + Mission.Takeoff.DRAG_COEFFICIENT_MIN, +] + +# These parameters are sized by number of engine models. +ENGINE_SIZED_INPUTS = [ + Aircraft.Nacelle.CHARACTERISTIC_LENGTH, + Aircraft.Nacelle.FINENESS, + Aircraft.Nacelle.LAMINAR_FLOW_LOWER, + Aircraft.Nacelle.LAMINAR_FLOW_UPPER, + Aircraft.Nacelle.WETTED_AREA +] + +AERO_2DOF_INPUTS = [ + Aircraft.Design.CG_DELTA, + Aircraft.Design.DRAG_COEFFICIENT_INCREMENT, # drag increment? + Aircraft.Design.STATIC_MARGIN, + Aircraft.Fuselage.AVG_DIAMETER, + Aircraft.Fuselage.FLAT_PLATE_AREA_INCREMENT, + Aircraft.Fuselage.FORM_FACTOR, + Aircraft.Fuselage.LENGTH, + Aircraft.Fuselage.WETTED_AREA, + Aircraft.HorizontalTail.AREA, + Aircraft.HorizontalTail.AVERAGE_CHORD, + Aircraft.HorizontalTail.FORM_FACTOR, + Aircraft.HorizontalTail.MOMENT_RATIO, + Aircraft.HorizontalTail.SPAN, + Aircraft.HorizontalTail.SWEEP, + Aircraft.HorizontalTail.VERTICAL_TAIL_FRACTION, + Aircraft.Nacelle.AVG_LENGTH, + Aircraft.Nacelle.FORM_FACTOR, + Aircraft.Nacelle.SURFACE_AREA, + Aircraft.Strut.AREA_RATIO, + Aircraft.Strut.CHORD, + Aircraft.Strut.FUSELAGE_INTERFERENCE_FACTOR, + Aircraft.VerticalTail.AREA, + Aircraft.VerticalTail.AVERAGE_CHORD, + Aircraft.VerticalTail.FORM_FACTOR, + Aircraft.VerticalTail.SPAN, + Aircraft.Wing.AVERAGE_CHORD, + Aircraft.Wing.AREA, + Aircraft.Wing.ASPECT_RATIO, + Aircraft.Wing.CENTER_DISTANCE, + Aircraft.Wing.FORM_FACTOR, + Aircraft.Wing.FUSELAGE_INTERFERENCE_FACTOR, + Aircraft.Wing.MAX_THICKNESS_LOCATION, + Aircraft.Wing.MIN_PRESSURE_LOCATION, + Aircraft.Wing.MOUNTING_TYPE, + Aircraft.Wing.SPAN, + Aircraft.Wing.SWEEP, + Aircraft.Wing.TAPER_RATIO, + Aircraft.Wing.THICKNESS_TO_CHORD_TIP, + Aircraft.Wing.THICKNESS_TO_CHORD_ROOT, + Aircraft.Wing.THICKNESS_TO_CHORD_UNWEIGHTED, + Aircraft.Wing.ZERO_LIFT_ANGLE, +] + +AERO_LS_2DOF_INPUTS = [ + Mission.Takeoff.DRAG_COEFFICIENT_FLAP_INCREMENT, + Mission.Takeoff.LIFT_COEFFICIENT_FLAP_INCREMENT, + Mission.Takeoff.LIFT_COEFFICIENT_MAX, +] + +AERO_CLEAN_2DOF_INPUTS = [ + Aircraft.Design.SUPERCRITICAL_DIVERGENCE_SHIFT, # super drag shift? + Mission.Design.LIFT_COEFFICIENT_MAX_FLAPS_UP, +] diff --git a/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py b/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py index 0f61386cf..f7cbd7741 100644 --- a/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py +++ b/aviary/subsystems/aerodynamics/flops_based/test/test_computed_aero_group.py @@ -5,11 +5,9 @@ from openmdao.utils.assert_utils import assert_near_equal from aviary.subsystems.premission import CorePreMission -from aviary.utils.aviary_values import get_items from aviary.utils.functions import set_aviary_initial_values from aviary.validation_cases.validation_tests import get_flops_inputs, get_flops_outputs from aviary.variable_info.variables import Aircraft, Dynamic, Settings -from aviary.variable_info.variables_in import VariablesIn from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems from aviary.utils.preprocessors import preprocess_options @@ -67,7 +65,7 @@ def test_basic_large_single_aisle_1(self): prob = om.Problem() model = prob.model - # Upstream static analysis for aero + # Upstream pre-mission analysis for aero prob.model.add_subsystem( 'pre_mission', CorePreMission(aviary_options=flops_inputs, @@ -82,26 +80,6 @@ def test_basic_large_single_aisle_1(self): promotes=['*'] ) - prob.model.add_subsystem( - 'input_sink', - VariablesIn(aviary_options=flops_inputs), - promotes_inputs=['*'], - promotes_outputs=['*'] - ) - - # keys = [Aircraft.Engine.SCALED_SLS_THRUST, Aircraft.Nacelle.WETTED_AREA, - # Aircraft.Nacelle.FINENESS, Aircraft.Nacelle.CHARACTERISTIC_LENGTH] - - # for key in keys: - # val, units = flops_inputs.get_item(key) - # # TODO temp line to ignore dynamic mission variables, will not work - # # if names change to 'dynamic:mission:*' - # if ':' not in key: - # continue - # prob.model.pre_mission.set_input_defaults(key, val, units) - - set_aviary_initial_values(prob.model, flops_inputs) - prob.setup(force_alloc_complex=True) prob.set_solver_print(level=2) @@ -111,13 +89,7 @@ def test_basic_large_single_aisle_1(self): prob.set_val(Dynamic.Mission.TEMPERATURE, val=T, units='degR') prob.set_val(Dynamic.Mission.MASS, val=mass, units='lbm') - for (key, (val, units)) in get_items(flops_inputs): - try: - prob.set_val(key, val, units) - - except: - # Should be an option or an overridden output. - continue + set_aviary_initial_values(prob, flops_inputs) prob.run_model() @@ -203,7 +175,7 @@ def test_n3cc_drag(self): prob = om.Problem() model = prob.model - # Upstream static analysis for aero + # Upstream pre-mission analysis for aero prob.model.add_subsystem( 'pre_mission', CorePreMission(aviary_options=flops_inputs, @@ -218,15 +190,6 @@ def test_n3cc_drag(self): promotes=['*'] ) - prob.model.add_subsystem( - 'input_sink', - VariablesIn(aviary_options=flops_inputs), - promotes_inputs=['*'], - promotes_outputs=['*'] - ) - - set_aviary_initial_values(prob.model, flops_inputs) - prob.setup() # Mission params @@ -235,13 +198,7 @@ def test_n3cc_drag(self): prob.set_val(Dynamic.Mission.TEMPERATURE, val=T, units='degR') prob.set_val(Dynamic.Mission.MASS, val=mass, units='lbm') - for (key, (val, units)) in get_items(flops_inputs): - try: - prob.set_val(key, val, units) - - except: - # Should be an option or an overridden output. - continue + set_aviary_initial_values(prob, flops_inputs) prob.run_model() @@ -327,7 +284,7 @@ def test_large_single_aisle_2_drag(self): prob = om.Problem() model = prob.model - # Upstream static analysis for aero + # Upstream pre-mission analysis for aero prob.model.add_subsystem( 'pre_mission', CorePreMission(aviary_options=flops_inputs, @@ -342,19 +299,6 @@ def test_large_single_aisle_2_drag(self): promotes=['*'] ) - prob.model.add_subsystem( - 'input_sink', - VariablesIn(aviary_options=flops_inputs), - promotes_inputs=['*'], - promotes_outputs=['*'] - ) - - set_aviary_initial_values(prob.model, flops_inputs) - - # key = Aircraft.Engine.SCALED_SLS_THRUST - # val, units = flops_inputs.get_item(key) - # prob.model.pre_mission.set_input_defaults(key, val, units) - prob.setup() # Mission params @@ -363,13 +307,7 @@ def test_large_single_aisle_2_drag(self): prob.set_val(Dynamic.Mission.TEMPERATURE, val=T, units='degR') prob.set_val(Dynamic.Mission.MASS, val=mass, units='lbm') - for (key, (val, units)) in get_items(flops_inputs): - try: - prob.set_val(key, val, units) - - except: - # Should be an option or an overridden output. - continue + set_aviary_initial_values(prob, flops_inputs) prob.run_model() diff --git a/aviary/subsystems/aerodynamics/flops_based/test/test_design.py b/aviary/subsystems/aerodynamics/flops_based/test/test_design.py index a4b45e447..bdf785b48 100644 --- a/aviary/subsystems/aerodynamics/flops_based/test/test_design.py +++ b/aviary/subsystems/aerodynamics/flops_based/test/test_design.py @@ -33,17 +33,12 @@ def test_derivs_supersonic1(self): prob.set_val(Aircraft.Wing.ASPECT_RATIO, val=11.05) prob.set_val(Aircraft.Wing.MAX_CAMBER_AT_70_SEMISPAN, val=1.0) prob.set_val(Aircraft.Wing.SWEEP, val=2.0191) - prob.set_val(Aircraft.Wing.THICKNESS_TO_CHORD, val=0.04) + prob.set_val(Aircraft.Wing.THICKNESS_TO_CHORD, val=0.04000001) prob.run_model() - prob.check_partials(compact_print=True, method="cs") derivs = prob.check_partials(out_stream=None, method="cs") - # assert_check_partials(derivs, atol=1e-12, rtol=1e-12) - # TODO - # mission:design:mach wrt aircraft:wing:thickness_to_chord seems to be wrong. - # need further investigation - assert_check_partials(derivs, atol=0.05, rtol=0.02) + assert_check_partials(derivs, atol=1e-12, rtol=1e-12) assert_near_equal( prob.get_val(Mission.Design.MACH), [0.753238], 1e-6) diff --git a/aviary/subsystems/aerodynamics/flops_based/test/test_drag.py b/aviary/subsystems/aerodynamics/flops_based/test/test_drag.py index 6148fee98..476c5cac3 100644 --- a/aviary/subsystems/aerodynamics/flops_based/test/test_drag.py +++ b/aviary/subsystems/aerodynamics/flops_based/test/test_drag.py @@ -2,20 +2,26 @@ import numpy as np import openmdao.api as om -from openmdao.utils.assert_utils import (assert_check_partials, - assert_near_equal) +from openmdao.utils.assert_utils import assert_check_partials, assert_near_equal from parameterized import parameterized from aviary.subsystems.aerodynamics.flops_based.computed_aero_group import ComputedDrag -from aviary.subsystems.aerodynamics.flops_based.drag import SimpleDrag, SimpleCD, TotalDrag +from aviary.subsystems.aerodynamics.flops_based.drag import ( + SimpleDrag, + SimpleCD, + TotalDrag, +) from aviary.utils.aviary_values import AviaryValues -from aviary.validation_cases.validation_tests import (get_flops_case_names, - get_flops_inputs, - print_case) +from aviary.validation_cases.validation_tests import ( + get_flops_case_names, + get_flops_inputs, + print_case, +) from aviary.variable_info.variables import Aircraft, Dynamic data_sets = get_flops_case_names( - only=['LargeSingleAisle1FLOPS', 'LargeSingleAisle2FLOPS', 'N3CC']) + only=['LargeSingleAisle1FLOPS', 'LargeSingleAisle2FLOPS', 'N3CC'] +) class SimpleDragTest(unittest.TestCase): @@ -24,23 +30,28 @@ class SimpleDragTest(unittest.TestCase): def test_case(self, case_name): flops_inputs = get_flops_inputs(case_name) - dynamics_data: AviaryValues = dynamics_test_data[case_name] + mission_data: AviaryValues = mission_test_data[case_name] # area = 4 digits precision # FCDSUB - 2 digits precision # FCDSUP - 2 digits precision inputs_keys = ( - Aircraft.Wing.AREA, Aircraft.Design.SUBSONIC_DRAG_COEFF_FACTOR, - Aircraft.Design.SUPERSONIC_DRAG_COEFF_FACTOR + Aircraft.Wing.AREA, + Aircraft.Design.SUBSONIC_DRAG_COEFF_FACTOR, + Aircraft.Design.SUPERSONIC_DRAG_COEFF_FACTOR, ) # dynamic pressure = 4 digits precision # drag coefficient = 5 digits precision - dynamics_keys = (Dynamic.Mission.DYNAMIC_PRESSURE, - 'CD_prescaled', 'CD', Dynamic.Mission.MACH) + mission_keys = ( + Dynamic.Mission.DYNAMIC_PRESSURE, + 'CD_prescaled', + 'CD', + Dynamic.Mission.MACH, + ) # drag = 4 digits precision - outputs_keys = (Dynamic.Mission.DRAG, ) + outputs_keys = (Dynamic.Mission.DRAG,) # using lowest precision from all available data should "always" work # - will a higher precision comparison work? find a practical tolerance that fits @@ -50,7 +61,7 @@ def test_case(self, case_name): prob = om.Problem() model = prob.model - q, _ = dynamics_data.get_item(Dynamic.Mission.DYNAMIC_PRESSURE) + q, _ = mission_data.get_item(Dynamic.Mission.DYNAMIC_PRESSURE) nn = len(q) model.add_subsystem('simple_drag', SimpleDrag(num_nodes=nn), promotes=['*']) model.add_subsystem('simple_cd', SimpleCD(num_nodes=nn), promotes=['*']) @@ -61,15 +72,15 @@ def test_case(self, case_name): val, units = flops_inputs.get_item(key) prob.set_val(key, val, units) - for key in dynamics_keys: - val, units = dynamics_data.get_item(key) + for key in mission_keys: + val, units = mission_data.get_item(key) prob.set_val(key, val, units) prob.run_model() for key in outputs_keys: try: - desired, units = dynamics_data.get_item(key) + desired, units = mission_data.get_item(key) actual = prob.get_val(key, units) assert_near_equal(actual, desired, tol) @@ -82,10 +93,10 @@ def test_case(self, case_name): data = prob.check_partials(out_stream=None, method="cs") assert_check_partials(data, atol=2.5e-10, rtol=1e-12) + assert_near_equal(prob.get_val("CD"), mission_simple_CD[case_name], 1e-6) assert_near_equal( - prob.get_val("CD"), dynamics_simple_CD[case_name], 1e-6) - assert_near_equal( - prob.get_val(Dynamic.Mission.DRAG), dynamics_simple_drag[case_name], 1e-6) + prob.get_val(Dynamic.Mission.DRAG), mission_simple_drag[case_name], 1e-6 + ) class TotalDragTest(unittest.TestCase): @@ -94,21 +105,27 @@ class TotalDragTest(unittest.TestCase): def test_case(self, case_name): flops_inputs = get_flops_inputs(case_name) - dynamics_data: AviaryValues = dynamics_test_data[case_name] + mission_data: AviaryValues = mission_test_data[case_name] # area = 4 digits precision # FCDSUB - 2 digits precision # FCDSUP - 2 digits precision inputs_keys = ( - Aircraft.Wing.AREA, Aircraft.Design.SUBSONIC_DRAG_COEFF_FACTOR, + Aircraft.Wing.AREA, + Aircraft.Design.SUBSONIC_DRAG_COEFF_FACTOR, Aircraft.Design.SUPERSONIC_DRAG_COEFF_FACTOR, Aircraft.Design.ZERO_LIFT_DRAG_COEFF_FACTOR, - Aircraft.Design.LIFT_DEPENDENT_DRAG_COEFF_FACTOR) + Aircraft.Design.LIFT_DEPENDENT_DRAG_COEFF_FACTOR, + ) # dynamic pressure = 4 digits precision # drag coefficient = 5 digits precision - dynamics_keys = (Dynamic.Mission.DYNAMIC_PRESSURE, - Dynamic.Mission.MACH, 'CD0', 'CDI') + mission_keys = ( + Dynamic.Mission.DYNAMIC_PRESSURE, + Dynamic.Mission.MACH, + 'CD0', + 'CDI', + ) # drag = 4 digits precision outputs_keys = ('CD_prescaled', 'CD', Dynamic.Mission.DRAG) @@ -121,7 +138,7 @@ def test_case(self, case_name): prob = om.Problem() model = prob.model - q, _ = dynamics_data.get_item(Dynamic.Mission.DYNAMIC_PRESSURE) + q, _ = mission_data.get_item(Dynamic.Mission.DYNAMIC_PRESSURE) nn = len(q) model.add_subsystem('total_drag', TotalDrag(num_nodes=nn), promotes=['*']) @@ -131,15 +148,15 @@ def test_case(self, case_name): val, units = flops_inputs.get_item(key) prob.set_val(key, val, units) - for key in dynamics_keys: - val, units = dynamics_data.get_item(key) + for key in mission_keys: + val, units = mission_data.get_item(key) prob.set_val(key, val, units) prob.run_model() for key in outputs_keys: try: - desired, units = dynamics_data.get_item(key) + desired, units = mission_data.get_item(key) actual = prob.get_val(key, units) assert_near_equal(actual, desired, tol) @@ -152,10 +169,10 @@ def test_case(self, case_name): data = prob.check_partials(out_stream=None, method="cs") assert_check_partials(data, atol=2.5e-10, rtol=1e-12) + assert_near_equal(prob.get_val("CD"), mission_total_CD[case_name], 1e-6) assert_near_equal( - prob.get_val("CD"), dynamics_total_CD[case_name], 1e-6) - assert_near_equal( - prob.get_val(Dynamic.Mission.DRAG), dynamics_total_drag[case_name], 1e-6) + prob.get_val(Dynamic.Mission.DRAG), mission_total_drag[case_name], 1e-6 + ) class ComputedDragTest(unittest.TestCase): @@ -192,20 +209,18 @@ def test_derivs(self): prob.set_val(Aircraft.Design.SUBSONIC_DRAG_COEFF_FACTOR, 1.4) prob.set_val(Aircraft.Design.SUPERSONIC_DRAG_COEFF_FACTOR, 1.1) prob.set_val(Aircraft.Wing.AREA, 1370, units="ft**2") - prob.set_val(Dynamic.Mission.DYNAMIC_PRESSURE, [206., 205.6], 'lbf/ft**2') + prob.set_val(Dynamic.Mission.DYNAMIC_PRESSURE, [206.0, 205.6], 'lbf/ft**2') prob.run_model() derivs = prob.check_partials(out_stream=None, method="cs") assert_check_partials(derivs, atol=1e-12, rtol=1e-12) - assert_near_equal( - prob.get_val("CD"), [0.0249732, 0.0297451], 1e-6) - assert_near_equal( - prob.get_val(Dynamic.Mission.DRAG), [31350.8, 37268.8], 1e-6) + assert_near_equal(prob.get_val("CD"), [0.0249732, 0.0297451], 1e-6) + assert_near_equal(prob.get_val(Dynamic.Mission.DRAG), [31350.8, 37268.8], 1e-6) -# region - dynamic test data taken from the baseline FLOPS output for each case +# region - mission test data taken from the baseline FLOPS output for each case # - first "# DETAILED FLIGHT SEGMENT SUMMARY" # - second "SEGMENT..." # - first three points @@ -213,99 +228,107 @@ def test_derivs(self): # coefficient - no intermediate calculations are reported in the "# DETAILED FLIGHT # SEGMENT SUMMARY", so we have to back calculate what we can def _add_drag_coefficients( - case_name, dynamics_data: AviaryValues, M: np.ndarray, CD_scaled: np.ndarray, - CD0_scaled: np.ndarray, CDI_scaled: np.ndarray + case_name, + mission_data: AviaryValues, + M: np.ndarray, + CD_scaled: np.ndarray, + CD0_scaled: np.ndarray, + CDI_scaled: np.ndarray, ): ''' - Insert drag coefficients into the dynamics data, undoing FLOPS scaling. + Insert drag coefficients into the mission data, undoing FLOPS scaling. ''' flops_inputs = get_flops_inputs(case_name) FCDSUB = flops_inputs.get_val(Aircraft.Design.SUBSONIC_DRAG_COEFF_FACTOR) FCDSUP = flops_inputs.get_val(Aircraft.Design.SUPERSONIC_DRAG_COEFF_FACTOR) idx_sup = np.where(M >= 1.0) - dynamics_data.set_val('CD', CD_scaled) + mission_data.set_val('CD', CD_scaled) CD = CD_scaled / FCDSUB CD[idx_sup] = CD_scaled[idx_sup] / FCDSUP - dynamics_data.set_val('CD_prescaled', CD) - dynamics_data.set_val('CD', CD_scaled) + mission_data.set_val('CD_prescaled', CD) + mission_data.set_val('CD', CD_scaled) FCD0 = flops_inputs.get_val(Aircraft.Design.ZERO_LIFT_DRAG_COEFF_FACTOR) CD0 = CD0_scaled / FCD0 - dynamics_data.set_val('CD0', CD0) + mission_data.set_val('CD0', CD0) FCDI = flops_inputs.get_val(Aircraft.Design.LIFT_DEPENDENT_DRAG_COEFF_FACTOR) CDI = CDI_scaled / FCDI - dynamics_data.set_val('CDI', CDI) + mission_data.set_val('CDI', CDI) -dynamics_test_data = {} -dynamics_simple_CD = {} -dynamics_simple_drag = {} -dynamics_total_CD = {} -dynamics_total_drag = {} +mission_test_data = {} +mission_simple_CD = {} +mission_simple_drag = {} +mission_total_CD = {} +mission_total_drag = {} key = 'LargeSingleAisle1FLOPS' -dynamics_test_data[key] = _dynamics_data = AviaryValues() -_dynamics_data.set_val(Dynamic.Mission.DYNAMIC_PRESSURE, np.array( - [206.0, 205.6, 205.4]), 'lbf/ft**2') -_dynamics_data.set_val(Dynamic.Mission.MASS, np.array( - [176751., 176400., 176185.]), 'lbm') -_dynamics_data.set_val(Dynamic.Mission.DRAG, np.array([9350., 9333., 9323.]), 'lbf') +mission_test_data[key] = _mission_data = AviaryValues() +_mission_data.set_val( + Dynamic.Mission.DYNAMIC_PRESSURE, np.array([206.0, 205.6, 205.4]), 'lbf/ft**2' +) +_mission_data.set_val( + Dynamic.Mission.MASS, np.array([176751.0, 176400.0, 176185.0]), 'lbm' +) +_mission_data.set_val(Dynamic.Mission.DRAG, np.array([9350.0, 9333.0, 9323.0]), 'lbf') M = np.array([0.7750, 0.7750, 0.7750]) CD_scaled = np.array([0.03313, 0.03313, 0.03313]) CD0_scaled = np.array([0.02012, 0.02013, 0.02013]) CDI_scaled = np.array([0.01301, 0.01301, 0.01300]) -_dynamics_data.set_val(Dynamic.Mission.MACH, M) -_add_drag_coefficients(key, _dynamics_data, M, CD_scaled, CD0_scaled, CDI_scaled) +_mission_data.set_val(Dynamic.Mission.MACH, M) +_add_drag_coefficients(key, _mission_data, M, CD_scaled, CD0_scaled, CDI_scaled) -dynamics_simple_CD[key] = np.array([0.03313, 0.03313, 0.03313]) -dynamics_simple_drag[key] = np.array([41590.643508, 41509.884977, 41469.505712]) -dynamics_total_CD[key] = np.array([0.03313, 0.03314, 0.03313]) -dynamics_total_drag[key] = np.array([41590.64350841, 41522.41437213, 41469.50571178]) +mission_simple_CD[key] = np.array([0.03313, 0.03313, 0.03313]) +mission_simple_drag[key] = np.array([41590.643508, 41509.884977, 41469.505712]) +mission_total_CD[key] = np.array([0.03313, 0.03314, 0.03313]) +mission_total_drag[key] = np.array([41590.64350841, 41522.41437213, 41469.50571178]) key = 'LargeSingleAisle2FLOPS' -dynamics_test_data[key] = _dynamics_data = AviaryValues() -_dynamics_data.set_val(Dynamic.Mission.DYNAMIC_PRESSURE, [ - 215.4, 215.4, 215.4], 'lbf/ft**2') -_dynamics_data.set_val(Dynamic.Mission.MASS, [169730., 169200., 167400.], 'lbm') -_dynamics_data.set_val(Dynamic.Mission.DRAG, [9542., 9512., 9411.], 'lbf') +mission_test_data[key] = _mission_data = AviaryValues() +_mission_data.set_val( + Dynamic.Mission.DYNAMIC_PRESSURE, [215.4, 215.4, 215.4], 'lbf/ft**2' +) +_mission_data.set_val(Dynamic.Mission.MASS, [169730.0, 169200.0, 167400.0], 'lbm') +_mission_data.set_val(Dynamic.Mission.DRAG, [9542.0, 9512.0, 9411.0], 'lbf') M = np.array([0.7850, 0.7850, 0.7850]) CD_scaled = np.array([0.03304, 0.03293, 0.03258]) CD0_scaled = np.array([0.02016, 0.02016, 0.02016]) CDI_scaled = np.array([0.01288, 0.01277, 0.01242]) -_dynamics_data.set_val(Dynamic.Mission.MACH, M) -_add_drag_coefficients(key, _dynamics_data, M, CD_scaled, CD0_scaled, CDI_scaled) +_mission_data.set_val(Dynamic.Mission.MACH, M) +_add_drag_coefficients(key, _mission_data, M, CD_scaled, CD0_scaled, CDI_scaled) -dynamics_simple_CD[key] = np.array([0.03304, 0.03293, 0.03258]) -dynamics_simple_drag[key] = np.array([42452.271402, 42310.935148, 41861.228883]) -dynamics_total_CD[key] = np.array([0.03304, 0.03293, 0.03258]) -dynamics_total_drag[key] = np.array([42452.27140246, 42310.93514779, 41861.22888293]) +mission_simple_CD[key] = np.array([0.03304, 0.03293, 0.03258]) +mission_simple_drag[key] = np.array([42452.271402, 42310.935148, 41861.228883]) +mission_total_CD[key] = np.array([0.03304, 0.03293, 0.03258]) +mission_total_drag[key] = np.array([42452.27140246, 42310.93514779, 41861.22888293]) key = 'N3CC' -dynamics_test_data[key] = _dynamics_data = AviaryValues() -_dynamics_data.set_val(Dynamic.Mission.DYNAMIC_PRESSURE, [ - 208.4, 288.5, 364.0], 'lbf/ft**2') -_dynamics_data.set_val(Dynamic.Mission.MASS, [128777., 128721., 128667.], 'lbm') -_dynamics_data.set_val(Dynamic.Mission.DRAG, [5837., 6551., 7566.], 'lbf') +mission_test_data[key] = _mission_data = AviaryValues() +_mission_data.set_val( + Dynamic.Mission.DYNAMIC_PRESSURE, [208.4, 288.5, 364.0], 'lbf/ft**2' +) +_mission_data.set_val(Dynamic.Mission.MASS, [128777.0, 128721.0, 128667.0], 'lbm') +_mission_data.set_val(Dynamic.Mission.DRAG, [5837.0, 6551.0, 7566.0], 'lbf') M = np.array([0.4522, 0.5321, 0.5985]) CD_scaled = np.array([0.02296, 0.01861, 0.01704]) CD0_scaled = np.array([0.01611, 0.01569, 0.01556]) CDI_scaled = np.array([0.00806, 0.00390, 0.00237]) -_dynamics_data.set_val(Dynamic.Mission.MACH, M) -_add_drag_coefficients(key, _dynamics_data, M, CD_scaled, CD0_scaled, CDI_scaled) -# endregion - dynamic test data taken from the baseline FLOPS output for each case +_mission_data.set_val(Dynamic.Mission.MACH, M) +_add_drag_coefficients(key, _mission_data, M, CD_scaled, CD0_scaled, CDI_scaled) +# endregion - mission test data taken from the baseline FLOPS output for each case -dynamics_simple_CD[key] = np.array([0.02296, 0.01861, 0.01704]) -dynamics_simple_drag[key] = np.array([25966.645302, 29136.570888, 33660.241019]) -dynamics_total_CD[key] = np.array([0.0229615, 0.0186105, 0.0170335]) -dynamics_total_drag[key] = np.array([25968.341729, 29137.353709, 33647.401139]) +mission_simple_CD[key] = np.array([0.02296, 0.01861, 0.01704]) +mission_simple_drag[key] = np.array([25966.645302, 29136.570888, 33660.241019]) +mission_total_CD[key] = np.array([0.0229615, 0.0186105, 0.0170335]) +mission_total_drag[key] = np.array([25968.341729, 29137.353709, 33647.401139]) if __name__ == "__main__": diff --git a/aviary/subsystems/aerodynamics/flops_based/test/test_lift.py b/aviary/subsystems/aerodynamics/flops_based/test/test_lift.py index d686f8ac8..65137fec3 100644 --- a/aviary/subsystems/aerodynamics/flops_based/test/test_lift.py +++ b/aviary/subsystems/aerodynamics/flops_based/test/test_lift.py @@ -4,16 +4,18 @@ from openmdao.utils.assert_utils import assert_check_partials, assert_near_equal from parameterized import parameterized -from aviary.subsystems.aerodynamics.flops_based.lift import (LiftEqualsWeight, - SimpleLift) +from aviary.subsystems.aerodynamics.flops_based.lift import LiftEqualsWeight, SimpleLift from aviary.utils.aviary_values import AviaryValues -from aviary.validation_cases.validation_tests import (get_flops_case_names, - get_flops_inputs, - print_case) +from aviary.validation_cases.validation_tests import ( + get_flops_case_names, + get_flops_inputs, + print_case, +) from aviary.variable_info.variables import Aircraft, Dynamic data_sets = get_flops_case_names( - only=['LargeSingleAisle1FLOPS', 'LargeSingleAisle2FLOPS', 'N3CC']) + only=['LargeSingleAisle1FLOPS', 'LargeSingleAisle2FLOPS', 'N3CC'] +) class SimpleLiftTest(unittest.TestCase): @@ -22,14 +24,14 @@ class SimpleLiftTest(unittest.TestCase): def test_case(self, case_name): flops_inputs = get_flops_inputs(case_name) - dynamics_data: AviaryValues = dynamics_test_data[case_name] + mission_data: AviaryValues = mission_test_data[case_name] # area = 4 digits precision inputs_keys = (Aircraft.Wing.AREA,) # dynamic pressure = 4 digits precision # lift coefficient = 5 digits precision - dynamics_keys = (Dynamic.Mission.DYNAMIC_PRESSURE, 'cl') + mission_keys = (Dynamic.Mission.DYNAMIC_PRESSURE, 'cl') # lift = 6 digits precision outputs_keys = (Dynamic.Mission.LIFT,) @@ -40,7 +42,7 @@ def test_case(self, case_name): prob = om.Problem() model = prob.model - q, _ = dynamics_data.get_item(Dynamic.Mission.DYNAMIC_PRESSURE) + q, _ = mission_data.get_item(Dynamic.Mission.DYNAMIC_PRESSURE) nn = len(q) model.add_subsystem('simple_lift', SimpleLift(num_nodes=nn), promotes=['*']) @@ -50,15 +52,15 @@ def test_case(self, case_name): val, units = flops_inputs.get_item(key) prob.set_val(key, val, units) - for key in dynamics_keys: - val, units = dynamics_data.get_item(key) + for key in mission_keys: + val, units = mission_data.get_item(key) prob.set_val(key, val, units) prob.run_model() for key in outputs_keys: try: - desired, units = dynamics_data.get_item(key) + desired, units = mission_data.get_item(key) actual = prob.get_val(key, units) assert_near_equal(actual, desired, tol) @@ -72,7 +74,8 @@ def test_case(self, case_name): assert_check_partials(data, atol=2.5e-10, rtol=1e-12) assert_near_equal( - prob.get_val(Dynamic.Mission.LIFT), dynamics_simple_data[case_name], 1e-6) + prob.get_val(Dynamic.Mission.LIFT), mission_simple_data[case_name], 1e-6 + ) class LiftEqualsWeightTest(unittest.TestCase): @@ -81,14 +84,14 @@ class LiftEqualsWeightTest(unittest.TestCase): def test_case(self, case_name): flops_inputs = get_flops_inputs(case_name) - dynamics_data: AviaryValues = dynamics_test_data[case_name] + mission_data: AviaryValues = mission_test_data[case_name] # area = 4 digits precision inputs_keys = (Aircraft.Wing.AREA,) # dynamic pressure = 4 digits precision # mass = 6 digits precision - dynamics_keys = (Dynamic.Mission.DYNAMIC_PRESSURE, Dynamic.Mission.MASS) + mission_keys = (Dynamic.Mission.DYNAMIC_PRESSURE, Dynamic.Mission.MASS) # lift coefficient = 5 digits precision # lift = 6 digits precision @@ -100,11 +103,12 @@ def test_case(self, case_name): prob = om.Problem() model = prob.model - q, _ = dynamics_data.get_item(Dynamic.Mission.DYNAMIC_PRESSURE) + q, _ = mission_data.get_item(Dynamic.Mission.DYNAMIC_PRESSURE) nn = len(q) model.add_subsystem( - 'lift_equals_weight', LiftEqualsWeight(num_nodes=nn), promotes=['*']) + 'lift_equals_weight', LiftEqualsWeight(num_nodes=nn), promotes=['*'] + ) prob.setup(force_alloc_complex=True) @@ -112,15 +116,15 @@ def test_case(self, case_name): val, units = flops_inputs.get_item(key) prob.set_val(key, val, units) - for key in dynamics_keys: - val, units = dynamics_data.get_item(key) + for key in mission_keys: + val, units = mission_data.get_item(key) prob.set_val(key, val, units) prob.run_model() for key in outputs_keys: try: - desired, units = dynamics_data.get_item(key) + desired, units = mission_data.get_item(key) actual = prob.get_val(key, units) assert_near_equal(actual, desired, tol) @@ -134,43 +138,47 @@ def test_case(self, case_name): assert_check_partials(data, atol=2.5e-10, rtol=1e-12) assert_near_equal( - prob.get_val(Dynamic.Mission.LIFT), dynamics_equal_data[case_name], 1e-6) + prob.get_val(Dynamic.Mission.LIFT), mission_equal_data[case_name], 1e-6 + ) # dynamic test data taken from the baseline FLOPS output for each case # - first: # DETAILED FLIGHT SEGMENT SUMMARY # - first: SEGMENT... CRUISE # - first three points -dynamics_test_data = {} -dynamics_simple_data = {} -dynamics_equal_data = {} - -dynamics_test_data['LargeSingleAisle1FLOPS'] = _dynamics_data = AviaryValues() -_dynamics_data.set_val(Dynamic.Mission.DYNAMIC_PRESSURE, [ - 206.0, 205.6, 205.4], 'lbf/ft**2') -_dynamics_data.set_val('cl', [0.62630, 0.62623, 0.62619]) -_dynamics_data.set_val(Dynamic.Mission.LIFT, [176751., 176400., 176185.], 'lbf') -_dynamics_data.set_val(Dynamic.Mission.MASS, [176751., 176400., 176185.], 'lbm') -dynamics_simple_data['LargeSingleAisle1FLOPS'] = [786242.68, 784628.29, 783814.96] -dynamics_equal_data['LargeSingleAisle1FLOPS'] = [786227.62, 784666.29, 783709.93] - -dynamics_test_data['LargeSingleAisle2FLOPS'] = _dynamics_data = AviaryValues() -_dynamics_data.set_val(Dynamic.Mission.DYNAMIC_PRESSURE, [ - 215.4, 215.4, 215.4], 'lbf/ft**2') -_dynamics_data.set_val('cl', [0.58761, 0.58578, 0.57954]) -_dynamics_data.set_val(Dynamic.Mission.LIFT, [169730., 169200., 167400.], 'lbf') -_dynamics_data.set_val(Dynamic.Mission.MASS, [169730., 169200., 167400.], 'lbm') -dynamics_simple_data['LargeSingleAisle2FLOPS'] = [755005.42, 752654.10, 744636.48] -dynamics_equal_data['LargeSingleAisle2FLOPS'] = [754996.65, 752639.10, 744632.30] - -dynamics_test_data['N3CC'] = _dynamics_data = AviaryValues() -_dynamics_data.set_val(Dynamic.Mission.DYNAMIC_PRESSURE, [ - 208.4, 288.5, 364.0], 'lbf/ft**2') -_dynamics_data.set_val('cl', [0.50651, 0.36573, 0.28970]) -_dynamics_data.set_val(Dynamic.Mission.LIFT, [128777., 128721., 128667.], 'lbf') -_dynamics_data.set_val(Dynamic.Mission.MASS, [128777., 128721., 128667.], 'lbm') -dynamics_simple_data['N3CC'] = [572838.22, 572601.72, 572263.60] -dynamics_equal_data['N3CC'] = [572828.63, 572579.53, 572339.33] +mission_test_data = {} +mission_simple_data = {} +mission_equal_data = {} + +mission_test_data['LargeSingleAisle1FLOPS'] = _mission_data = AviaryValues() +_mission_data.set_val( + Dynamic.Mission.DYNAMIC_PRESSURE, [206.0, 205.6, 205.4], 'lbf/ft**2' +) +_mission_data.set_val('cl', [0.62630, 0.62623, 0.62619]) +_mission_data.set_val(Dynamic.Mission.LIFT, [176751.0, 176400.0, 176185.0], 'lbf') +_mission_data.set_val(Dynamic.Mission.MASS, [176751.0, 176400.0, 176185.0], 'lbm') +mission_simple_data['LargeSingleAisle1FLOPS'] = [786242.68, 784628.29, 783814.96] +mission_equal_data['LargeSingleAisle1FLOPS'] = [786227.62, 784666.29, 783709.93] + +mission_test_data['LargeSingleAisle2FLOPS'] = _mission_data = AviaryValues() +_mission_data.set_val( + Dynamic.Mission.DYNAMIC_PRESSURE, [215.4, 215.4, 215.4], 'lbf/ft**2' +) +_mission_data.set_val('cl', [0.58761, 0.58578, 0.57954]) +_mission_data.set_val(Dynamic.Mission.LIFT, [169730.0, 169200.0, 167400.0], 'lbf') +_mission_data.set_val(Dynamic.Mission.MASS, [169730.0, 169200.0, 167400.0], 'lbm') +mission_simple_data['LargeSingleAisle2FLOPS'] = [755005.42, 752654.10, 744636.48] +mission_equal_data['LargeSingleAisle2FLOPS'] = [754996.65, 752639.10, 744632.30] + +mission_test_data['N3CC'] = _mission_data = AviaryValues() +_mission_data.set_val( + Dynamic.Mission.DYNAMIC_PRESSURE, [208.4, 288.5, 364.0], 'lbf/ft**2' +) +_mission_data.set_val('cl', [0.50651, 0.36573, 0.28970]) +_mission_data.set_val(Dynamic.Mission.LIFT, [128777.0, 128721.0, 128667.0], 'lbf') +_mission_data.set_val(Dynamic.Mission.MASS, [128777.0, 128721.0, 128667.0], 'lbm') +mission_simple_data['N3CC'] = [572838.22, 572601.72, 572263.60] +mission_equal_data['N3CC'] = [572828.63, 572579.53, 572339.33] if __name__ == "__main__": diff --git a/aviary/subsystems/aerodynamics/flops_based/test/test_solved_aero_group.py b/aviary/subsystems/aerodynamics/flops_based/test/test_solved_aero_group.py index b272eef81..1618deab5 100644 --- a/aviary/subsystems/aerodynamics/flops_based/test/test_solved_aero_group.py +++ b/aviary/subsystems/aerodynamics/flops_based/test/test_solved_aero_group.py @@ -72,7 +72,7 @@ def test_solved_aero_pass_polar(self): CL_base = prob.get_val("traj.cruise.rhs_all.core_aerodynamics.tabular_aero.CL") CD_base = prob.get_val("traj.cruise.rhs_all.core_aerodynamics.tabular_aero.CD") - # Lift and Drag polars passed from external component in static. + # Lift and Drag polars passed from external component in pre-mission. ph_in = deepcopy(phase_info) @@ -148,7 +148,7 @@ def test_solved_aero_pass_polar_unique_abscissa(self): CL_base = prob.get_val("traj.cruise.rhs_all.core_aerodynamics.tabular_aero.CL") CD_base = prob.get_val("traj.cruise.rhs_all.core_aerodynamics.tabular_aero.CD") - # Lift and Drag polars passed from external component in static. + # Lift and Drag polars passed from external component in pre-mission. ph_in = deepcopy(phase_info) @@ -272,7 +272,7 @@ def build_pre_mission(self, aviary_inputs): ------- pre_mission_sys : openmdao.core.Group An OpenMDAO group containing all computations that need to happen in - the pre-mission (formerly statics) part of the Aviary problem. This + the pre-mission part of the Aviary problem. This includes sizing, design, and other non-mission parameters. """ diff --git a/aviary/subsystems/aerodynamics/flops_based/test/test_tabular_aero_group.py b/aviary/subsystems/aerodynamics/flops_based/test/test_tabular_aero_group.py index 3bee7c3ee..9ddf681bf 100644 --- a/aviary/subsystems/aerodynamics/flops_based/test/test_tabular_aero_group.py +++ b/aviary/subsystems/aerodynamics/flops_based/test/test_tabular_aero_group.py @@ -1,10 +1,11 @@ +from parameterized import parameterized import unittest import numpy as np + import openmdao.api as om from aviary.subsystems.atmosphere.atmosphere import Atmosphere from openmdao.utils.assert_utils import assert_check_partials, assert_near_equal -from parameterized import parameterized from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder from aviary.subsystems.premission import CorePreMission @@ -17,7 +18,6 @@ get_flops_outputs, print_case) from aviary.variable_info.variables import Aircraft, Dynamic, Mission, Settings -from aviary.variable_info.variables_in import VariablesIn from aviary.variable_info.enums import LegacyCode FLOPS = LegacyCode.FLOPS @@ -252,13 +252,7 @@ def test_case(self, case_name): except: pass # unused variable - for (key, (val, units)) in get_items(flops_inputs): - try: - prob.set_val(key, val, units) - - except: - # Should be an option or an overridden output. - continue + set_aviary_initial_values(prob, flops_inputs) prob.run_model() @@ -548,20 +542,8 @@ def _run_computed_aero_harness(flops_inputs, dynamic_inputs, num_nodes): prob.setup() - for (key, (val, units)) in get_items(dynamic_inputs): - try: - prob.set_val(key, val, units) - - except: - pass # unused variable - - for (key, (val, units)) in get_items(flops_inputs): - try: - prob.set_val(key, val, units) - - except: - # Should be an option or an overridden output. - continue + set_aviary_initial_values(prob, dynamic_inputs) + set_aviary_initial_values(prob, flops_inputs) prob.run_model() @@ -597,7 +579,7 @@ def setup(self): default_premission_subsystems = get_default_premission_subsystems('FLOPS', engine)[ :-1] - # Upstream static analysis for aero + # Upstream pre-mission analysis for aero pre_mission: om.Group = self.add_subsystem( 'pre_mission', CorePreMission(aviary_options=aviary_options, @@ -621,15 +603,6 @@ def setup(self): val, units = aviary_options.get_item(key) pre_mission.set_input_defaults(key, val, units) - self.add_subsystem( - 'input_sink', - VariablesIn(aviary_options=aviary_options), - promotes_inputs=['*'], - promotes_outputs=['*'] - ) - - set_aviary_initial_values(self, aviary_options) - _design_altitudes = AviaryValues({ 'LargeSingleAisle1FLOPS': (41000, 'ft'), 'LargeSingleAisle2FLOPS': (41000, 'ft'), 'N3CC': (43000, 'ft')}) diff --git a/aviary/subsystems/aerodynamics/gasp_based/gaspaero.py b/aviary/subsystems/aerodynamics/gasp_based/gaspaero.py index bfcf16833..2553c8174 100644 --- a/aviary/subsystems/aerodynamics/gasp_based/gaspaero.py +++ b/aviary/subsystems/aerodynamics/gasp_based/gaspaero.py @@ -157,7 +157,7 @@ def sigmoid(x, x0, alpha=0.1): class WingTailRatios(om.ExplicitComponent): - """Static calculation of ratios between tail and wing parameters""" + """Pre-mission calculation of ratios between tail and wing parameters""" def setup(self): @@ -257,7 +257,7 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] - # dynamic inputs + # mission inputs self.add_input(Dynamic.Mission.MACH, val=0.0, units="unitless", shape=nn, desc="Mach number") @@ -864,7 +864,7 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] - # dynamic inputs + # mission inputs self.add_input(Dynamic.Mission.ALTITUDE, val=0.0, units="ft", shape=nn, desc="Altitude") self.add_input( @@ -1006,7 +1006,7 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] - # dynamic inputs + # mission inputs self.add_input(Dynamic.Mission.MACH, val=0.0, units="unitless", shape=nn, desc="Mach number") self.add_input( @@ -1071,7 +1071,7 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] - # dynamic inputs + # mission inputs self.add_input("alpha", val=0.0, units="deg", shape=nn, desc="Angle of attack") self.add_input(Dynamic.Mission.ALTITUDE, val=0.0, units="ft", shape=nn, desc="Altitude") diff --git a/aviary/subsystems/aerodynamics/gasp_based/premission_aero.py b/aviary/subsystems/aerodynamics/gasp_based/premission_aero.py index 617bc7c52..619b5dd50 100644 --- a/aviary/subsystems/aerodynamics/gasp_based/premission_aero.py +++ b/aviary/subsystems/aerodynamics/gasp_based/premission_aero.py @@ -1,6 +1,6 @@ -"""Group containing all static aero calculations. +"""Group containing all pre-mission aero calculations. -Flaps modeling occurs as a static calculation to provide CL max and CL/CD increments to +Flaps modeling occurs as a pre-mission calculation to provide CL max and CL/CD increments to the dynamic aero. """ diff --git a/aviary/subsystems/aerodynamics/gasp_based/test/test_gaspaero.py b/aviary/subsystems/aerodynamics/gasp_based/test/test_gaspaero.py index f81b65857..9cfb0d9a5 100644 --- a/aviary/subsystems/aerodynamics/gasp_based/test/test_gaspaero.py +++ b/aviary/subsystems/aerodynamics/gasp_based/test/test_gaspaero.py @@ -19,7 +19,7 @@ class GASPAeroTest(unittest.TestCase): - """Test overall static and dynamic aero systems in cruise and near-ground flight""" + """Test overall pre-mission and mission aero systems in cruise and near-ground flight""" cruise_tol = 1.5e-3 ground_tol = 0.5e-3 @@ -170,14 +170,14 @@ def _init_geom(prob): prob.set_val(Aircraft.HorizontalTail.AREA, setup_data["sht"]) prob.set_val(Aircraft.HorizontalTail.AVERAGE_CHORD, setup_data["cbarht"]) prob.set_val(Aircraft.Fuselage.AVG_DIAMETER, setup_data["swf"]) - # ground & cruise, dynamic: mach + # ground & cruise, mission: mach prob.set_val(Aircraft.Design.STATIC_MARGIN, setup_data["stmarg"]) prob.set_val(Aircraft.Design.CG_DELTA, setup_data["delcg"]) prob.set_val(Aircraft.Wing.ASPECT_RATIO, setup_data["ar"]) prob.set_val(Aircraft.Wing.SWEEP, setup_data["dlmc4"]) prob.set_val(Aircraft.HorizontalTail.SWEEP, setup_data["dwpqch"]) prob.set_val(Aircraft.HorizontalTail.MOMENT_RATIO, setup_data["coelth"]) - # ground & cruise, dynamic: alt + # ground & cruise, mission: alt prob.set_val(Aircraft.Wing.FORM_FACTOR, setup_data["ckw"]) prob.set_val(Aircraft.Fuselage.FORM_FACTOR, setup_data["ckf"]) prob.set_val(Aircraft.Nacelle.FORM_FACTOR, setup_data["ckn"]) @@ -202,7 +202,7 @@ def _init_geom(prob): prob.set_val(Aircraft.VerticalTail.AREA, setup_data["svt"]) prob.set_val(Aircraft.Wing.THICKNESS_TO_CHORD_UNWEIGHTED, setup_data["tc"]) prob.set_val(Aircraft.Strut.CHORD, 0) # not defined in standalone aero - # ground & cruise, dynamic: alpha + # ground & cruise, mission: alpha prob.set_val(Aircraft.Wing.ZERO_LIFT_ANGLE, setup_data["alphl0"]) # ground & cruise, config-specific: CL_max_flaps # ground: wing_height @@ -218,7 +218,7 @@ def _init_geom(prob): # ground: dCD_flaps_model # ground: t_init_gear # ground: dt_gear - # ground & cruise, dynamic: q + # ground & cruise, mission: q if __name__ == "__main__": diff --git a/aviary/subsystems/geometry/flops_based/test/test_prep_geom.py b/aviary/subsystems/geometry/flops_based/test/test_prep_geom.py index c20ae0433..ae81cc3b1 100644 --- a/aviary/subsystems/geometry/flops_based/test/test_prep_geom.py +++ b/aviary/subsystems/geometry/flops_based/test/test_prep_geom.py @@ -45,7 +45,8 @@ def setUp(self): @parameterized.expand(get_flops_case_names(), name_func=print_case) def test_case(self, case_name): - class Statics(om.Group): + + class PreMission(om.Group): def initialize(self): self.options.declare( @@ -76,7 +77,8 @@ def configure(self): prob = self.prob prob.model.add_subsystem( - 'statics', Statics(aviary_options=options), promotes=['*']) + 'premission', PreMission(aviary_options=options), promotes=['*'] + ) prob.setup(check=False, force_alloc_complex=True) diff --git a/aviary/subsystems/mass/flops_based/test/test_fuel_capacity.py b/aviary/subsystems/mass/flops_based/test/test_fuel_capacity.py index ee3c2cd49..1e2b94ad3 100644 --- a/aviary/subsystems/mass/flops_based/test/test_fuel_capacity.py +++ b/aviary/subsystems/mass/flops_based/test/test_fuel_capacity.py @@ -23,7 +23,7 @@ class FuelCapacityGroupTest(unittest.TestCase): name_func=print_case) def test_case(self, case_name): - class StaticsHarness(om.Group): + class PreMission(om.Group): def initialize(self): self.options.declare( @@ -34,8 +34,11 @@ def setup(self): aviary_options = self.options['aviary_options'] self.add_subsystem( - 'fuel_capy_group', FuelCapacityGroup(aviary_options=aviary_options), - promotes_inputs=['*'], promotes_outputs=['*']) + 'fuel_capacity_group', + FuelCapacityGroup(aviary_options=aviary_options), + promotes_inputs=['*'], + promotes_outputs=['*'], + ) def configure(self): aviary_options = self.options['aviary_options'] @@ -45,10 +48,10 @@ def configure(self): prob = om.Problem() prob.model.add_subsystem( - "statics", - StaticsHarness(aviary_options=get_flops_inputs(case_name)), + "premission", + PreMission(aviary_options=get_flops_inputs(case_name)), promotes_outputs=['*'], - promotes_inputs=['*'] + promotes_inputs=['*'], ) prob.setup(check=False, force_alloc_complex=True) diff --git a/aviary/subsystems/premission.py b/aviary/subsystems/premission.py index 993b986a7..1d348ec96 100644 --- a/aviary/subsystems/premission.py +++ b/aviary/subsystems/premission.py @@ -6,7 +6,6 @@ from aviary.variable_info.variables import Aircraft, Mission from aviary.utils.aviary_values import AviaryValues -from aviary.variable_info.variables_in import VariablesIn from aviary.variable_info.variable_meta_data import _MetaData from aviary.variable_info.functions import override_aviary_vars diff --git a/aviary/subsystems/propulsion/engine_deck.py b/aviary/subsystems/propulsion/engine_deck.py index 9e3dee3e6..9f126d855 100644 --- a/aviary/subsystems/propulsion/engine_deck.py +++ b/aviary/subsystems/propulsion/engine_deck.py @@ -43,6 +43,7 @@ from aviary.utils.aviary_values import AviaryValues, NamedValues, get_keys, get_items from aviary.variable_info.variable_meta_data import _MetaData from aviary.variable_info.variables import Aircraft, Dynamic, Mission, Settings +from aviary.variable_info.enums import Verbosity from aviary.utils.csv_data_file import read_data_file from aviary.interface.utils.markdown_utils import round_it @@ -144,7 +145,8 @@ class EngineDeck(EngineModel): """ def __init__(self, name='engine_deck', options: AviaryValues = None, - data: NamedValues = None, required_variables=default_required_variables): + data: NamedValues = None, + required_variables: set = default_required_variables): if data is not None: self.read_from_file = False else: @@ -215,7 +217,7 @@ def _preprocess_inputs(self): val = _MetaData[key]['default_value'] units = _MetaData[key]['units'] - if self.get_val(Settings.VERBOSITY).value >= 1: + if self.get_val(Settings.VERBOSITY) >= Verbosity.BRIEF: warnings.warn( f'<{key}> is a required option for EngineDecks, but has not been ' f'specified for EngineDeck <{self.name}>. The default value ' @@ -241,8 +243,9 @@ def _preprocess_inputs(self): if idle_min > idle_max: if self.get_val(Settings.VERBOSITY).value >= 1: warnings.warn( - f'EngineDeck <{self.name}>: Minimum flight idle fraction exceeds maximum ' - f'flight idle fraction. Values for min and max fraction will be flipped.' + f'EngineDeck <{self.name}>: Minimum flight idle fraction ' + 'exceeds maximum flight idle fraction. Values for min and max ' + 'fraction will be flipped.' ) self.set_val(Aircraft.Engine.FLIGHT_IDLE_MIN_FRACTION, val=idle_max) @@ -395,16 +398,18 @@ def _read_data(self, raw_data: NamedValues): val = np.array([convert_units(i, units, default_units[key]) for i in val]) except TypeError: - raise TypeError(f"{message}: units of '{units}' provided for " - f'<{key.name}> are not compatible with expected units ' - f'of {default_units[key]}') + raise TypeError( + f"{message}: units of '{units}' provided for " + f'<{key.name}> are not compatible with expected ' + f'units of {default_units[key]}' + ) # Engine_variables currently only used to store "valid" engine variables # as defined in EngineModelVariables Enum self.engine_variables[key] = default_units[key] else: - if self.get_val(Settings.VERBOSITY).value >= 1: + if self.get_val(Settings.VERBOSITY) >= Verbosity.BRIEF: warnings.warn( f'{message}: header <{key}> was not recognized, and will be skipped') @@ -514,7 +519,11 @@ def _check_data(self): # them for consistency, as that requires information not avaliable during setup # (freestream air temp and pressure). Instead, we must trust the source and # assume either data set is valid and can be used. - if SHAFT_POWER in engine_variables and SHAFT_POWER_CORRECTED in engine_variables and self.get_val(Settings.VERBOSITY).value >= 1: + if ( + SHAFT_POWER in engine_variables + and SHAFT_POWER_CORRECTED in engine_variables + and self.get_val(Settings.VERBOSITY) >= Verbosity.BRIEF + ): warnings.warn( 'Both corrected and uncorrected shaft horsepower are ' f'present in {message}. The two cannot be validated for ' @@ -1145,11 +1154,13 @@ def _set_reference_thrust(self): sls_idx = np.intersect1d(sea_level_idx, static_idx) if sls_idx.size == 0: - raise UserWarning('Could not find sea-level static max thrust point for ' - f'EngineDeck <{self.name}>. Please review the data file ' - f'<{self.get_val(Aircraft.Engine.DATA_FILE)}> or ' - 'manually specify Aircraft.Engine.REFERENCE_SLS_THRUST ' - 'in EngineDeck options') + raise UserWarning( + 'Could not find sea-level static max thrust point for ' + f'EngineDeck <{self.name}>. Please review the data ' + f'file <{self.get_val(Aircraft.Engine.DATA_FILE)}> or ' + 'manually specify Aircraft.Engine.REFERENCE_SLS_THRUST ' + 'in EngineDeck options' + ) reference_sls_thrust = max(self.data[THRUST][sls_idx]) @@ -1179,8 +1190,8 @@ def _set_reference_thrust(self): # user wants scaling but provided conflicting inputs, # cannot be resolved raise AttributeError( - f'EngineModel <{self.name}>: Conflicting values provided for ' - 'aircraft:engine:scale_factor and ' + f'EngineModel <{self.name}>: Conflicting values provided ' + 'for aircraft:engine:scale_factor and ' 'aircraft:engine:scaled_sls_thrust' ) # get thrust target & scale factor matching exactly. Scale factor is @@ -1455,10 +1466,12 @@ def _count_data(self): # new Mach number # if there are less than two altitudes for this Mach number, quit if alt_count < 2 and mach_count > 0: - raise UserWarning('Only one altitude provided for Mach number ' - f'{mach_numbers[mach_count]:6.3f} in engine data file ' - f'<{self.get_val(Aircraft.Engine.DATA_FILE).name}>' - ) + raise UserWarning( + 'Only one altitude provided for Mach number ' + f'{mach_numbers[mach_count]:6.3f} in engine data ' + 'file ' + f'<{self.get_val(Aircraft.Engine.DATA_FILE).name}>' + ) # record and count mach numbers curr_mach = mach_num diff --git a/aviary/subsystems/propulsion/engine_sizing.py b/aviary/subsystems/propulsion/engine_sizing.py index 32ccff84e..379c5272b 100644 --- a/aviary/subsystems/propulsion/engine_sizing.py +++ b/aviary/subsystems/propulsion/engine_sizing.py @@ -8,8 +8,8 @@ class SizeEngine(om.ExplicitComponent): ''' - Calculates thrust scaling factors for dynamic mission parameters. Designed for use - with EngineDecks. + Calculates thrust scaling factors for mission performance parameters. Designed for + use with EngineDecks. Can be vectorized for all unique engines present on aircraft. Each index represents a single instance of an engine model. diff --git a/aviary/subsystems/propulsion/propeller/hamilton_standard.py b/aviary/subsystems/propulsion/propeller/hamilton_standard.py index d364c4074..67e7e0ae1 100644 --- a/aviary/subsystems/propulsion/propeller/hamilton_standard.py +++ b/aviary/subsystems/propulsion/propeller/hamilton_standard.py @@ -723,7 +723,7 @@ def compute(self, inputs, outputs): CP_CLi_table[CL_tab_idx][:cli_len], XPCLI[CL_tab_idx], CPE1X) if (run_flag == 1): ichck = ichck + 1 - if (verbosity is Verbosity.DEBUG or ichck <= 1): + if verbosity == Verbosity.DEBUG or ichck <= Verbosity.BRIEF: if (run_flag == 1): warnings.warn( f"Mach,VTMACH,J,power_coefficient,CP_Eff =: {inputs[Dynamic.Mission.MACH][i_node]},{inputs['tip_mach'][i_node]},{inputs['advance_ratio'][i_node]},{power_coefficient},{CP_Eff}") @@ -747,10 +747,12 @@ def compute(self, inputs, outputs): CP_Eff = CP_Eff*PCLI # the effective CP at baseline point for kdx ang_len = ang_arr_len[kdx] BLL[kdx], run_flag = _unint( - CP_Angle_table[idx_blade][kdx][:ang_len], Blade_angle_table[kdx], CP_Eff) # blade angle at baseline point for kdx + # blade angle at baseline point for kdx + CP_Angle_table[idx_blade][kdx][:ang_len], Blade_angle_table[kdx], CP_Eff) try: CTT[kdx], run_flag = _unint( - Blade_angle_table[kdx], CT_Angle_table[idx_blade][kdx][:ang_len], BLL[kdx]) # thrust coeff at baseline point for kdx + # thrust coeff at baseline point for kdx + Blade_angle_table[kdx], CT_Angle_table[idx_blade][kdx][:ang_len], BLL[kdx]) except IndexError: raise om.AnalysisError( "interp failed for CTT (thrust coefficient) in hamilton_standard.py") diff --git a/aviary/subsystems/propulsion/propulsion_premission.py b/aviary/subsystems/propulsion/propulsion_premission.py index dd9030850..7480211bf 100644 --- a/aviary/subsystems/propulsion/propulsion_premission.py +++ b/aviary/subsystems/propulsion/propulsion_premission.py @@ -6,6 +6,7 @@ from aviary.utils.aviary_values import AviaryValues from aviary.variable_info.functions import add_aviary_input, add_aviary_output from aviary.variable_info.variables import Aircraft, Settings +from aviary.variable_info.enums import Verbosity class PropulsionPreMission(om.Group): @@ -74,8 +75,9 @@ def configure(self): # determine if openMDAO messages and warnings should be suppressed verbosity = self.options['aviary_options'].get_val(Settings.VERBOSITY) out_stream = None + # DEBUG - if verbosity.value > 2: + if verbosity > Verbosity.VERBOSE: out_stream = sys.stdout comp_list = [ diff --git a/aviary/subsystems/propulsion/test/test_custom_engine_model.py b/aviary/subsystems/propulsion/test/test_custom_engine_model.py index d95e623d0..9577ce5fe 100644 --- a/aviary/subsystems/propulsion/test/test_custom_engine_model.py +++ b/aviary/subsystems/propulsion/test/test_custom_engine_model.py @@ -6,17 +6,17 @@ from aviary.subsystems.propulsion.engine_model import EngineModel from aviary.utils.aviary_values import AviaryValues -from aviary.variable_info.enums import Verbosity +from aviary.variable_info.variables import Aircraft, Dynamic from openmdao.utils.assert_utils import assert_near_equal from openmdao.utils.testing_utils import use_tempdirs from aviary.subsystems.propulsion.turboprop_model import TurbopropModel from aviary.variable_info.options import get_option_defaults from aviary.utils.functions import get_path from aviary.interface.methods_for_level2 import AviaryProblem -from aviary.variable_info.variables import Aircraft, Dynamic from dymos.transcriptions.transcription_base import TranscriptionBase + if hasattr(TranscriptionBase, 'setup_polynomial_controls'): use_new_dymos_syntax = False else: @@ -25,8 +25,11 @@ class PreMissionEngine(om.Group): def setup(self): - self.add_subsystem('dummy_comp', om.ExecComp( - 'y=x**2', x={'units': 'm', 'val': 2.}, y={'units': 'm**2'}), promotes=['*']) + self.add_subsystem( + 'dummy_comp', + om.ExecComp('y=x**2', x={'units': 'm', 'val': 2.0}, y={'units': 'm**2'}), + promotes=['*'], + ) class SimpleEngine(om.ExplicitComponent): @@ -36,62 +39,81 @@ def initialize(self): def setup(self): nn = self.options['num_nodes'] # add inputs and outputs to interpolator - self.add_input(Dynamic.Mission.MACH, - shape=nn, - units='unitless', - desc='Current flight Mach number') - self.add_input(Dynamic.Mission.ALTITUDE, - shape=nn, - units='ft', - desc='Current flight altitude') - self.add_input(Dynamic.Mission.THROTTLE, - shape=nn, - units='unitless', - desc='Current engine throttle') - self.add_input('different_throttle', - shape=nn, - units='unitless', - desc='Little bonus throttle for testing') - self.add_input('y', - units='m**2', - desc='Dummy variable for bus testing') - - self.add_output(Dynamic.Mission.THRUST, - shape=nn, - units='lbf', - desc='Current net thrust produced (scaled)') - self.add_output(Dynamic.Mission.THRUST_MAX, - shape=nn, - units='lbf', - desc='Current net thrust produced (scaled)') - self.add_output(Dynamic.Mission.FUEL_FLOW_RATE_NEGATIVE, - shape=nn, - units='lbm/s', - desc='Current fuel flow rate (scaled)') - self.add_output(Dynamic.Mission.ELECTRIC_POWER_IN, - shape=nn, - units='W', - desc='Current electric energy rate (scaled)') - self.add_output(Dynamic.Mission.NOX_RATE, - shape=nn, - units='lbm/s', - desc='Current NOx emission rate (scaled)') - self.add_output(Dynamic.Mission.TEMPERATURE_T4, - shape=nn, - units='degR', - desc='Current turbine exit temperature') + self.add_input( + Dynamic.Mission.MACH, + shape=nn, + units='unitless', + desc='Current flight Mach number', + ) + self.add_input( + Dynamic.Mission.ALTITUDE, + shape=nn, + units='ft', + desc='Current flight altitude', + ) + self.add_input( + Dynamic.Mission.THROTTLE, + shape=nn, + units='unitless', + desc='Current engine throttle', + ) + self.add_input( + 'different_throttle', + shape=nn, + units='unitless', + desc='Little bonus throttle for testing', + ) + self.add_input('y', units='m**2', desc='Dummy variable for bus testing') + + self.add_output( + Dynamic.Mission.THRUST, + shape=nn, + units='lbf', + desc='Current net thrust produced (scaled)', + ) + self.add_output( + Dynamic.Mission.THRUST_MAX, + shape=nn, + units='lbf', + desc='Current net thrust produced (scaled)', + ) + self.add_output( + Dynamic.Mission.FUEL_FLOW_RATE_NEGATIVE, + shape=nn, + units='lbm/s', + desc='Current fuel flow rate (scaled)', + ) + self.add_output( + Dynamic.Mission.ELECTRIC_POWER_IN, + shape=nn, + units='W', + desc='Current electric energy rate (scaled)', + ) + self.add_output( + Dynamic.Mission.NOX_RATE, + shape=nn, + units='lbm/s', + desc='Current NOx emission rate (scaled)', + ) + self.add_output( + Dynamic.Mission.TEMPERATURE_T4, + shape=nn, + units='degR', + desc='Current turbine exit temperature', + ) self.declare_partials('*', '*', method='fd') def compute(self, inputs, outputs): - combined_throttle = inputs[Dynamic.Mission.THROTTLE] + \ - inputs['different_throttle'] + combined_throttle = ( + inputs[Dynamic.Mission.THROTTLE] + inputs['different_throttle'] + ) # calculate outputs - outputs[Dynamic.Mission.THRUST] = 10000. * combined_throttle - outputs[Dynamic.Mission.THRUST_MAX] = 10000. - outputs[Dynamic.Mission.FUEL_FLOW_RATE_NEGATIVE] = -10. * combined_throttle - outputs[Dynamic.Mission.TEMPERATURE_T4] = 2800. + outputs[Dynamic.Mission.THRUST] = 10000.0 * combined_throttle + outputs[Dynamic.Mission.THRUST_MAX] = 10000.0 + outputs[Dynamic.Mission.FUEL_FLOW_RATE_NEGATIVE] = -10.0 * combined_throttle + outputs[Dynamic.Mission.TEMPERATURE_T4] = 2800.0 class SimpleTestEngine(EngineModel): @@ -107,7 +129,7 @@ def build_mission(self, num_nodes, aviary_inputs): def get_controls(self, **kwargs): controls_dict = { - "different_throttle": {'units': 'unitless', 'lower': 0., 'upper': 0.1}, + "different_throttle": {'units': 'unitless', 'lower': 0.0, 'upper': 0.1}, } if use_new_dymos_syntax: controls_dict['different_throttle']['control_type'] = 'polynomial' @@ -164,22 +186,25 @@ def test_custom_engine(self): "constrain_final": False, "fix_duration": False, "initial_bounds": ((0.0, 0.0), "min"), - "duration_bounds": ((10., 30.), "min"), + "duration_bounds": ((10.0, 30.0), "min"), }, "initial_guesses": {"time": ([0, 30], "min")}, }, 'post_mission': { 'include_landing': False, 'external_subsystems': [], - } + }, } prob = AviaryProblem(reports=False) # Load aircraft and options data from user # Allow for user overrides here - prob.load_inputs("models/test_aircraft/aircraft_for_bench_GwFm.csv", - phase_info, engine_builders=[SimpleTestEngine()]) + prob.load_inputs( + "models/test_aircraft/aircraft_for_bench_GwFm.csv", + phase_info, + engine_builders=[SimpleTestEngine()], + ) # Preprocess inputs prob.check_and_preprocess_inputs() @@ -193,7 +218,7 @@ def test_custom_engine(self): # Link phases and variables prob.link_phases() - prob.add_driver("SLSQP", verbosity=Verbosity.QUIET) + prob.add_driver("SLSQP", verbosity=0) prob.add_design_variables() @@ -207,19 +232,24 @@ def test_custom_engine(self): # check that the different throttle initial guess has been set correctly initial_guesses = prob.get_val( - 'traj.phases.cruise.controls:different_throttle')[0] + 'traj.phases.cruise.controls:different_throttle' + )[0] assert_near_equal(float(initial_guesses), 0.05) - # and run mission, and dynamics + # and run mission dm.run_problem(prob, run_driver=True, simulate=False, make_plots=False) - tol = 1.e-4 + tol = 1.0e-4 - assert_near_equal(float(prob.get_val('traj.cruise.rhs_all.y')), 4., tol) + assert_near_equal(float(prob.get_val('traj.cruise.rhs_all.y')), 4.0, tol) @use_tempdirs class TurbopropTest(unittest.TestCase): + """ + Test integrating turboprop component with full AviaryProblem + """ + def test_turboprop(self): phase_info = { 'pre_mission': { @@ -247,17 +277,17 @@ def test_turboprop(self): "constrain_final": False, "fix_duration": False, "initial_bounds": ((0.0, 0.0), "min"), - "duration_bounds": ((30., 60.), "min"), + "duration_bounds": ((30.0, 60.0), "min"), }, "initial_guesses": {"time": ([0, 30], "min")}, }, 'post_mission': { 'include_landing': False, 'external_subsystems': [], - } + }, } - engine_filepath = get_path('models/engines/turboprop_4465hp.deck') + engine_filepath = get_path('models/engines/turboshaft_4465hp.deck') options = get_option_defaults() options.set_val(Aircraft.Engine.DATA_FILE, engine_filepath) options.set_val(Aircraft.Engine.NUM_ENGINES, 2) @@ -268,8 +298,7 @@ def test_turboprop(self): val=True, units='unitless', ) - options.set_val(Aircraft.Engine.NUM_PROPELLER_BLADES, - val=4, units='unitless') + options.set_val(Aircraft.Engine.NUM_PROPELLER_BLADES, val=4, units='unitless') engine = TurbopropModel(options=options) @@ -277,8 +306,11 @@ def test_turboprop(self): # Load aircraft and options data from user # Allow for user overrides here - prob.load_inputs("models/test_aircraft/aircraft_for_bench_FwFm.csv", - phase_info, engine_builders=[engine]) + prob.load_inputs( + "models/test_aircraft/aircraft_for_bench_FwFm.csv", + phase_info, + engine_builders=[engine], + ) # Preprocess inputs prob.check_and_preprocess_inputs() @@ -316,16 +348,21 @@ def test_turboprop(self): units='unitless', ) prob.set_val( - f'traj.cruise.rhs_all.{Aircraft.Engine.PROPELLER_INTEGRATED_LIFT_COEFFICIENT}', + ( + 'traj.cruise.rhs_all.' + f'{Aircraft.Engine.PROPELLER_INTEGRATED_LIFT_COEFFICIENT}' + ), 0.5, units='unitless', ) prob.set_solver_print(level=0) - # and run mission, and dynamics + # and run mission dm.run_problem(prob, run_driver=True, simulate=False, make_plots=True) if __name__ == '__main__': unittest.main() + # test = TurbopropTest() + # test.test_turboprop() diff --git a/aviary/subsystems/propulsion/test/test_engine_scaling.py b/aviary/subsystems/propulsion/test/test_engine_scaling.py index e6a61477f..75daf047b 100644 --- a/aviary/subsystems/propulsion/test/test_engine_scaling.py +++ b/aviary/subsystems/propulsion/test/test_engine_scaling.py @@ -10,7 +10,6 @@ from aviary.utils.preprocessors import preprocess_propulsion from aviary.utils.functions import get_path from aviary.variable_info.variables import Aircraft, Dynamic, Mission -from aviary.variable_info.enums import Verbosity from aviary.subsystems.propulsion.utils import EngineModelVariables @@ -48,7 +47,7 @@ def test_case(self): preprocess_propulsion(options, [engine1]) - options.set_val(Mission.Summary.FUEL_FLOW_SCALER, 10.) + options.set_val(Mission.Summary.FUEL_FLOW_SCALER, 10.0) engine_variables = { EngineModelVariables.THRUST: 'lbf', EngineModelVariables.FUEL_FLOW: 'lbm/h', @@ -63,15 +62,19 @@ def test_case(self): promotes=['*'], ) self.prob.setup(force_alloc_complex=True) - self.prob.set_val('thrust_net_unscaled', np.ones( - [nn, count]) * 1000, units='lbf') - self.prob.set_val('fuel_flow_rate_unscaled', np.ones( - [nn, count]) * 100, units='lbm/h') + self.prob.set_val( + 'thrust_net_unscaled', np.ones([nn, count]) * 1000, units='lbf' + ) + self.prob.set_val( + 'fuel_flow_rate_unscaled', np.ones([nn, count]) * 100, units='lbm/h' + ) self.prob.set_val('nox_rate_unscaled', np.ones([nn, count]) * 10, units='lbm/h') - self.prob.set_val(Dynamic.Mission.MACH, np.linspace( - 0, 0.75, nn), units='unitless') - self.prob.set_val(Aircraft.Engine.SCALE_FACTOR, - options.get_val(Aircraft.Engine.SCALE_FACTOR)) + self.prob.set_val( + Dynamic.Mission.MACH, np.linspace(0, 0.75, nn), units='unitless' + ) + self.prob.set_val( + Aircraft.Engine.SCALE_FACTOR, options.get_val(Aircraft.Engine.SCALE_FACTOR) + ) self.prob.run_model() @@ -80,11 +83,11 @@ def test_case(self): nox_rate = self.prob.get_val(Dynamic.Mission.NOX_RATE) # exit_area = self.prob.get_val(Dynamic.Mission.EXIT_AREA) - thrust_expected = np.array([900., 900., 900., 900]) + thrust_expected = np.array([900.0, 900.0, 900.0, 900]) fuel_flow_expected = np.array([-1836.55, -1836.55, -1836.55, -1836.55]) - nox_rate_expected = np.array([9., 9., 9., 9]) + nox_rate_expected = np.array([9.0, 9.0, 9.0, 9]) assert_near_equal(thrust, thrust_expected, tolerance=1e-10) assert_near_equal(fuel_flow, fuel_flow_expected, tolerance=1e-10) diff --git a/aviary/subsystems/propulsion/test/test_turboprop_model.py b/aviary/subsystems/propulsion/test/test_turboprop_model.py index 0aa82ab70..eacd6595e 100644 --- a/aviary/subsystems/propulsion/test/test_turboprop_model.py +++ b/aviary/subsystems/propulsion/test/test_turboprop_model.py @@ -7,7 +7,9 @@ from pathlib import Path from aviary.subsystems.propulsion.turboprop_model import TurbopropModel -from aviary.subsystems.propulsion.propeller.propeller_performance import PropellerPerformance +from aviary.subsystems.propulsion.propeller.propeller_performance import ( + PropellerPerformance, +) from aviary.utils.preprocessors import preprocess_propulsion from aviary.utils.functions import get_path from aviary.variable_info.variables import Aircraft, Dynamic, Mission @@ -21,7 +23,13 @@ class TurbopropTest(unittest.TestCase): def setUp(self): self.prob = om.Problem() - def prepare_model(self, test_points=[(0, 0, 0), (0, 0, 1)], shp_model=None, prop_model=None, **kwargs): + def prepare_model( + self, + test_points=[(0, 0, 0), (0, 0, 1)], + shp_model=None, + prop_model=None, + **kwargs + ): options = get_option_defaults() if isinstance(shp_model, Path): options.set_val(Aircraft.Engine.DATA_FILE, shp_model) @@ -43,27 +51,24 @@ def prepare_model(self, test_points=[(0, 0, 0), (0, 0, 1)], shp_model=None, prop options.set_val(Aircraft.Engine.GEOPOTENTIAL_ALT, False) options.set_val(Aircraft.Engine.INTERPOLATION_METHOD, 'slinear') - options.set_val(Aircraft.Engine.COMPUTE_PROPELLER_INSTALLATION_LOSS, - val=True, units='unitless') - options.set_val(Aircraft.Engine.NUM_PROPELLER_BLADES, - val=4, units='unitless') + options.set_val( + Aircraft.Engine.COMPUTE_PROPELLER_INSTALLATION_LOSS, + val=True, + units='unitless', + ) + options.set_val(Aircraft.Engine.NUM_PROPELLER_BLADES, val=4, units='unitless') num_nodes = len(test_points) engine = TurbopropModel( - options=options, shaft_power_model=shp_model, propeller_model=prop_model) + options=options, shaft_power_model=shp_model, propeller_model=prop_model + ) preprocess_propulsion(options, [engine]) machs, alts, throttles = zip(*test_points) - IVC = om.IndepVarComp(Dynamic.Mission.MACH, - np.array(machs), - units='unitless') - IVC.add_output(Dynamic.Mission.ALTITUDE, - np.array(alts), - units='ft') - IVC.add_output(Dynamic.Mission.THROTTLE, - np.array(throttles), - units='unitless') + IVC = om.IndepVarComp(Dynamic.Mission.MACH, np.array(machs), units='unitless') + IVC.add_output(Dynamic.Mission.ALTITUDE, np.array(alts), units='ft') + IVC.add_output(Dynamic.Mission.THROTTLE, np.array(throttles), units='unitless') self.prob.model.add_subsystem('IVC', IVC, promotes=['*']) # calculate atmospheric properties @@ -76,9 +81,11 @@ def prepare_model(self, test_points=[(0, 0, 0), (0, 0, 1)], shp_model=None, prop self.prob.model.add_subsystem( engine.name, subsys=engine.build_mission( - num_nodes=num_nodes, aviary_inputs=options, **kwargs), + num_nodes=num_nodes, aviary_inputs=options, **kwargs + ), promotes_inputs=['*'], - promotes_outputs=['*']) + promotes_outputs=['*'], + ) self.prob.setup(force_alloc_complex=False) self.prob.set_val(Aircraft.Engine.SCALE_FACTOR, 1, units='unitless') @@ -86,41 +93,69 @@ def prepare_model(self, test_points=[(0, 0, 0), (0, 0, 1)], shp_model=None, prop def get_results(self, point_names=None, display_results=False): shp = self.prob.get_val(Dynamic.Mission.SHAFT_POWER, units='hp') total_thrust = self.prob.get_val(Dynamic.Mission.THRUST, units='lbf') - prop_thrust = self.prob.get_val( - 'turboprop_model.propeller_thrust', units='lbf') + prop_thrust = self.prob.get_val('turboprop_model.propeller_thrust', units='lbf') tailpipe_thrust = self.prob.get_val( - 'turboprop_model.turboshaft_thrust', units='lbf') + 'turboprop_model.turboshaft_thrust', units='lbf' + ) max_thrust = self.prob.get_val(Dynamic.Mission.THRUST_MAX, units='lbf') fuel_flow = self.prob.get_val( - Dynamic.Mission.FUEL_FLOW_RATE_NEGATIVE, units='lbm/h') + Dynamic.Mission.FUEL_FLOW_RATE_NEGATIVE, units='lbm/h' + ) results = [] for n, _ in enumerate(shp): results.append( - (shp[n], - tailpipe_thrust[n], - prop_thrust[n], - total_thrust[n], - max_thrust[n], - fuel_flow[n])) + ( + shp[n], + tailpipe_thrust[n], + prop_thrust[n], + total_thrust[n], + max_thrust[n], + fuel_flow[n], + ) + ) return results def test_case_1(self): # test case using GASP-derived engine deck and "user specified" prop model - filename = get_path('models/engines/turboprop_1120hp.deck') + filename = get_path('models/engines/turboshaft_1120hp.deck') # Mach, alt, throttle @ idle, SLS, TOC - test_points = [(0, 0, 0), (0, 0, 1), (.6, 25000, 1)] + test_points = [(0, 0, 0), (0, 0, 1), (0.6, 25000, 1)] # shp, tailpipe thrust, prop_thrust, total_thrust, max_thrust, fuel flow - truth_vals = [(223.99923788786057, 37.699999999999996, 1195.4410168571105, 1233.1410168571106, 4983.816421227165, -195.79999999999995), - (2239.9923788786077, 136.29999999999967, 4847.516421227166, - 4983.816421227165, 4983.816421227165, -643.9999999999998), - (2466.55094358958, 21.30000000000001, 1833.4755577366554, 1854.7755577366554, 1854.7755577366554, -839.7000000000685)] + truth_vals = [ + ( + 223.99923788786057, + 37.699999999999996, + 1195.4410168571105, + 1233.1410168571106, + 4983.816421227165, + -195.79999999999995, + ), + ( + 2239.9923788786077, + 136.29999999999967, + 4847.516421227166, + 4983.816421227165, + 4983.816421227165, + -643.9999999999998, + ), + ( + 2466.55094358958, + 21.30000000000001, + 1833.4755577366554, + 1854.7755577366554, + 1854.7755577366554, + -839.7000000000685, + ), + ] options = get_option_defaults() - options.set_val(Aircraft.Engine.COMPUTE_PROPELLER_INSTALLATION_LOSS, - val=True, units='unitless') - options.set_val(Aircraft.Engine.NUM_PROPELLER_BLADES, - val=4, units='unitless') + options.set_val( + Aircraft.Engine.COMPUTE_PROPELLER_INSTALLATION_LOSS, + val=True, + units='unitless', + ) + options.set_val(Aircraft.Engine.NUM_PROPELLER_BLADES, val=4, units='unitless') options.set_val('speed_type', SpeedType.MACH) prop_group = ExamplePropModel('custom_prop_model') @@ -128,12 +163,14 @@ def test_case_1(self): self.prepare_model(test_points, filename, prop_group) self.prob.set_val(Aircraft.Engine.PROPELLER_DIAMETER, 10.5, units="ft") - self.prob.set_val(Aircraft.Engine.PROPELLER_ACTIVITY_FACTOR, - 114.0, units="unitless") + self.prob.set_val( + Aircraft.Engine.PROPELLER_ACTIVITY_FACTOR, 114.0, units="unitless" + ) # self.prob.set_val(Dynamic.Mission.PERCENT_ROTOR_RPM_CORRECTED, # np.array([1, 1, 0.7]), units="unitless") self.prob.set_val( - Aircraft.Engine.PROPELLER_INTEGRATED_LIFT_COEFFICIENT, 0.5, units="unitless") + Aircraft.Engine.PROPELLER_INTEGRATED_LIFT_COEFFICIENT, 0.5, units="unitless" + ) self.prob.set_val(Aircraft.Engine.PROPELLER_TIP_SPEED_MAX, 800, units="ft/s") @@ -147,22 +184,46 @@ def test_case_1(self): def test_case_2(self): # test case using GASP-derived engine deck and default HS prop model. - filename = get_path('models/engines/turboprop_1120hp.deck') - test_points = [(0.001, 0, 0), (0, 0, 1), (.6, 25000, 1)] - truth_vals = [(223.99007751511726, 37.507374999999996, 1186.6952790705282, 1224.202654070528, 4984.168836459296, -195.78762499999996), - (2239.9923788786077, 136.29999999999967, 4847.516421227166, - 4983.816421227165, 4983.816421227165, -643.9999999999998), - (2466.55094358958, 21.30000000000001, 1833.4755577366554, 1854.7755577366554, 1854.7755577366554, -839.7000000000685)] + filename = get_path('models/engines/turboshaft_1120hp.deck') + test_points = [(0.001, 0, 0), (0, 0, 1), (0.6, 25000, 1)] + truth_vals = [ + ( + 223.99007751511726, + 37.507374999999996, + 1186.6952790705282, + 1224.202654070528, + 4984.168836459296, + -195.78762499999996, + ), + ( + 2239.9923788786077, + 136.29999999999967, + 4847.516421227166, + 4983.816421227165, + 4983.816421227165, + -643.9999999999998, + ), + ( + 2466.55094358958, + 21.30000000000001, + 1833.4755577366554, + 1854.7755577366554, + 1854.7755577366554, + -839.7000000000685, + ), + ] self.prepare_model(test_points, filename) self.prob.set_val(Aircraft.Engine.PROPELLER_DIAMETER, 10.5, units="ft") - self.prob.set_val(Aircraft.Engine.PROPELLER_ACTIVITY_FACTOR, - 114.0, units="unitless") + self.prob.set_val( + Aircraft.Engine.PROPELLER_ACTIVITY_FACTOR, 114.0, units="unitless" + ) # self.prob.set_val(Dynamic.Mission.PERCENT_ROTOR_RPM_CORRECTED, # np.array([1,1,0.7]), units="unitless") self.prob.set_val( - Aircraft.Engine.PROPELLER_INTEGRATED_LIFT_COEFFICIENT, 0.5, units="unitless") + Aircraft.Engine.PROPELLER_INTEGRATED_LIFT_COEFFICIENT, 0.5, units="unitless" + ) self.prob.set_val(Aircraft.Engine.PROPELLER_TIP_SPEED_MAX, 800, units="ft/s") @@ -176,20 +237,44 @@ def test_case_2(self): def test_case_3(self): # test case using GASP-derived engine deck w/o tailpipe thrust and default HS prop model. - filename = get_path('models/engines/turboprop_1120hp_no_tailpipe.deck') - test_points = [(0, 0, 0), (0, 0, 1), (.6, 25000, 1)] - truth_vals = [(223.99923788786057, 0.0, 1195.4410168571105, 1195.4410168571105, 4847.516421227166, -195.79999999999995), - (2239.9923788786077, 0.0, 4847.516421227166, - 4847.516421227166, 4847.516421227166, -643.9999999999998), - (2466.55094358958, 0.0, 1833.4755577366554, 1833.4755577366554, 1833.4755577366554, -839.7000000000685)] + filename = get_path('models/engines/turboshaft_1120hp_no_tailpipe.deck') + test_points = [(0, 0, 0), (0, 0, 1), (0.6, 25000, 1)] + truth_vals = [ + ( + 223.99923788786057, + 0.0, + 1195.4410168571105, + 1195.4410168571105, + 4847.516421227166, + -195.79999999999995, + ), + ( + 2239.9923788786077, + 0.0, + 4847.516421227166, + 4847.516421227166, + 4847.516421227166, + -643.9999999999998, + ), + ( + 2466.55094358958, + 0.0, + 1833.4755577366554, + 1833.4755577366554, + 1833.4755577366554, + -839.7000000000685, + ), + ] self.prepare_model(test_points, filename) self.prob.set_val(Aircraft.Engine.PROPELLER_DIAMETER, 10.5, units="ft") - self.prob.set_val(Aircraft.Engine.PROPELLER_ACTIVITY_FACTOR, - 114.0, units="unitless") self.prob.set_val( - Aircraft.Engine.PROPELLER_INTEGRATED_LIFT_COEFFICIENT, 0.5, units="unitless") + Aircraft.Engine.PROPELLER_ACTIVITY_FACTOR, 114.0, units="unitless" + ) + self.prob.set_val( + Aircraft.Engine.PROPELLER_INTEGRATED_LIFT_COEFFICIENT, 0.5, units="unitless" + ) self.prob.set_val(Aircraft.Engine.PROPELLER_TIP_SPEED_MAX, 800, units="ft/s") self.prob.run_model() @@ -202,19 +287,21 @@ def test_case_3(self): def test_electroprop(self): # test case using electric motor and default HS prop model. - test_points = [(0, 0, 0), (0, 0, 1), (.6, 25000, 1)] + test_points = [(0, 0, 0), (0, 0, 1), (0.6, 25000, 1)] num_nodes = len(test_points) motor_model = MotorBuilder() self.prepare_model(test_points, motor_model, input_rpm=True) - self.prob.set_val(Dynamic.Mission.RPM, np.ones(num_nodes)*2000., units='rpm') + self.prob.set_val(Dynamic.Mission.RPM, np.ones(num_nodes) * 2000.0, units='rpm') self.prob.set_val(Aircraft.Engine.PROPELLER_DIAMETER, 10.5, units="ft") - self.prob.set_val(Aircraft.Engine.PROPELLER_ACTIVITY_FACTOR, - 114.0, units="unitless") self.prob.set_val( - Aircraft.Engine.PROPELLER_INTEGRATED_LIFT_COEFFICIENT, 0.5, units="unitless") + Aircraft.Engine.PROPELLER_ACTIVITY_FACTOR, 114.0, units="unitless" + ) + self.prob.set_val( + Aircraft.Engine.PROPELLER_INTEGRATED_LIFT_COEFFICIENT, 0.5, units="unitless" + ) self.prob.set_val(Aircraft.Engine.PROPELLER_TIP_SPEED_MAX, 800, units="ft/s") @@ -231,7 +318,9 @@ def test_electroprop(self): shp = self.prob.get_val(Dynamic.Mission.SHAFT_POWER, units='hp') total_thrust = self.prob.get_val(Dynamic.Mission.THRUST, units='lbf') prop_thrust = self.prob.get_val('turboprop_model.propeller_thrust', units='lbf') - electric_power = self.prob.get_val(Dynamic.Mission.ELECTRIC_POWER_IN, units='kW') + electric_power = self.prob.get_val( + Dynamic.Mission.ELECTRIC_POWER_IN, units='kW' + ) assert_near_equal(shp, shp_expected, tolerance=1e-8) assert_near_equal(total_thrust, total_thrust_expected, tolerance=1e-8) @@ -239,7 +328,8 @@ def test_electroprop(self): assert_near_equal(electric_power, electric_power_expected, tolerance=1e-8) partial_data = self.prob.check_partials( - out_stream=None, method="fd", form="central") + out_stream=None, method="fd", form="central" + ) assert_check_partials(partial_data, atol=0.17, rtol=0.15) @@ -265,8 +355,11 @@ def build_mission(self, num_nodes, aviary_inputs, **kwargs): ) pp.set_input_defaults(Aircraft.Engine.PROPELLER_DIAMETER, 10, units="ft") - pp.set_input_defaults(Dynamic.Mission.PROPELLER_TIP_SPEED, - 800.*np.ones(num_nodes), units="ft/s") + pp.set_input_defaults( + Dynamic.Mission.PROPELLER_TIP_SPEED, + 800.0 * np.ones(num_nodes), + units="ft/s", + ) pp.set_input_defaults( Dynamic.Mission.VELOCITY, 100.0 * np.ones(num_nodes), units="knot" ) diff --git a/aviary/subsystems/subsystem_builder_base.py b/aviary/subsystems/subsystem_builder_base.py index d00bd139e..783e14737 100644 --- a/aviary/subsystems/subsystem_builder_base.py +++ b/aviary/subsystems/subsystem_builder_base.py @@ -30,7 +30,7 @@ def build_pre_mission(self, aviary_inputs, **kwargs): ------- pre_mission_sys : openmdao.core.System An OpenMDAO system containing all computations that need to happen in - the pre-mission (formerly statics) part of the Aviary problem. This + the pre-mission part of the Aviary problem. This includes sizing, design, and other non-mission parameters. """ return None diff --git a/aviary/subsystems/test/test_flops_based_premission.py b/aviary/subsystems/test/test_flops_based_premission.py index d2544dac4..7d8934b44 100644 --- a/aviary/subsystems/test/test_flops_based_premission.py +++ b/aviary/subsystems/test/test_flops_based_premission.py @@ -6,13 +6,11 @@ from aviary.subsystems.premission import CorePreMission from aviary.utils.aviary_values import AviaryValues -from aviary.utils.aviary_values import get_items +from aviary.utils.functions import set_aviary_initial_values from aviary.validation_cases.validation_tests import ( flops_validation_test, get_flops_inputs, get_flops_outputs, get_flops_case_names, print_case ) from aviary.variable_info.variables import Aircraft, Mission, Settings -from aviary.variable_info.variables_in import VariablesIn -from aviary.utils.functions import set_aviary_initial_values from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.test_utils.default_subsystems import get_default_premission_subsystems from aviary.utils.preprocessors import preprocess_options @@ -30,7 +28,7 @@ def test_case(self, case_name): flops_outputs = get_flops_outputs(case_name) flops_inputs.set_val(Aircraft.Propulsion.TOTAL_NUM_WING_ENGINES, flops_outputs.get_val(Aircraft.Propulsion.TOTAL_NUM_WING_ENGINES)) - flops_inputs.set_val(Settings.VERBOSITY, 0.0) + flops_inputs.set_val(Settings.VERBOSITY, 0) engine = build_engine_deck(flops_inputs) preprocess_options(flops_inputs, engine_models=engine) @@ -47,14 +45,6 @@ def test_case(self, case_name): promotes_outputs=['*'], ) - prob.model.add_subsystem( - 'input_sink', - VariablesIn(aviary_options=flops_inputs), - promotes_inputs=['*'], - promotes_outputs=['*'] - ) - - set_aviary_initial_values(prob.model, flops_inputs) prob.setup(check=False, force_alloc_complex=True) prob.set_solver_print(2) @@ -62,14 +52,7 @@ def test_case(self, case_name): # We set it to an unconverged value to test convergence. prob.set_val(Mission.Design.GROSS_MASS, val=1000.0) - # Set inital values for all variables. - for (key, (val, units)) in get_items(flops_inputs): - try: - prob.set_val(key, val, units) - - except KeyError: - # This is an option, not a variable - continue + set_aviary_initial_values(prob, flops_inputs) if case_name in ['LargeSingleAisle1FLOPS', 'LargeSingleAisle2FLOPSdw']: # We set these so that their derivatives are defined. @@ -106,7 +89,7 @@ def test_diff_configuration_mass(self): flops_inputs: AviaryValues = LargeSingleAisle2FLOPS['inputs'] flops_outputs: AviaryValues = LargeSingleAisle2FLOPS['outputs'] - flops_inputs.set_val(Settings.VERBOSITY, 0.0) + flops_inputs.set_val(Settings.VERBOSITY, 0) engine = build_engine_deck(flops_inputs) preprocess_options(flops_inputs, engine_models=engine) @@ -121,24 +104,9 @@ def test_diff_configuration_mass(self): promotes_outputs=['*'], ) - prob.model.add_subsystem( - 'input_sink', - VariablesIn(aviary_options=flops_inputs), - promotes_inputs=['*'], - promotes_outputs=['*'] - ) - - set_aviary_initial_values(prob.model, flops_inputs) prob.setup(check=False) - # Set inital values for all variables. - for (key, (val, units)) in get_items(flops_inputs): - try: - prob.set_val(key, val, units) - - except KeyError: - # This is an option, not a variable - continue + set_aviary_initial_values(prob, flops_inputs) flops_validation_test( prob, diff --git a/aviary/subsystems/test/test_premission.py b/aviary/subsystems/test/test_premission.py index 12e4b21b9..51724e96a 100644 --- a/aviary/subsystems/test/test_premission.py +++ b/aviary/subsystems/test/test_premission.py @@ -16,7 +16,6 @@ from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData from aviary.models.large_single_aisle_1.V3_bug_fixed_IO import V3_bug_fixed_options, V3_bug_fixed_non_metadata from aviary.utils.functions import set_aviary_initial_values -from aviary.variable_info.variables_in import VariablesIn from aviary.variable_info.enums import LegacyCode from aviary.subsystems.propulsion.utils import build_engine_deck @@ -103,15 +102,10 @@ def setUp(self): for (key, (val, units)) in get_items(V3_bug_fixed_non_metadata): self.prob.model.set_input_defaults(key, val=val, units=units) - self.prob.model.add_subsystem( - 'input_sink', - VariablesIn(aviary_options=input_options), - promotes_inputs=['*'], - promotes_outputs=['*'] - ) + self.prob.model.set_input_defaults(Aircraft.Wing.SPAN, val=1.0, units='ft') - set_aviary_initial_values(self.prob.model, input_options) self.prob.setup(check=False, force_alloc_complex=True) + self.prob.set_solver_print(2) # Initial guess for gross mass. @@ -119,11 +113,7 @@ def setUp(self): self.prob.set_val(Mission.Design.GROSS_MASS, val=1000.0) # Set inital values for all variables. - for (key, (val, units)) in get_items(input_options): - try: - self.prob.set_val(key, val, units) - except KeyError: - continue + set_aviary_initial_values(self.prob, input_options) def test_GASP_mass_FLOPS_everything_else(self): self.prob.run_model() @@ -311,15 +301,6 @@ def test_manual_override(self): for (key, (val, units)) in get_items(V3_bug_fixed_non_metadata): prob.model.set_input_defaults(key, val=val, units=units) - prob.model.add_subsystem( - 'input_sink', - VariablesIn(aviary_options=aviary_inputs), - promotes_inputs=['*'], - promotes_outputs=['*'] - ) - - set_aviary_initial_values(prob.model, aviary_inputs) - prob.setup() # Problem in setup is FLOPS prioritized, so shared inputs for FLOPS will be manually overriden. diff --git a/aviary/utils/engine_deck_conversion.py b/aviary/utils/engine_deck_conversion.py index 1eb8af7f7..16025095f 100644 --- a/aviary/utils/engine_deck_conversion.py +++ b/aviary/utils/engine_deck_conversion.py @@ -22,7 +22,7 @@ class EngineDeckType(Enum): FLOPS = 'FLOPS' GASP = 'GASP' - GASP_TP = 'GASP_TP' + GASP_TS = 'GASP_TS' def __str__(self): return self.value @@ -100,7 +100,7 @@ def EngineDeckConverter(input_file, output_file, data_format: EngineDeckType): comments.append(f'# created {timestamp} by {user}') legacy_code = data_format.value engine_type = 'engine' - if legacy_code == 'GASP_TP': + if legacy_code == 'GASP_TS': engine_type = 'turboshaft engine' legacy_code = 'GASP' @@ -132,8 +132,8 @@ def EngineDeckConverter(input_file, output_file, data_format: EngineDeckType): data[NOX_RATE] = np.append(data[NOX_RATE], line[6]) # data[EXIT_AREA].append(line[7]) - elif data_format in (EngineDeckType.GASP, EngineDeckType.GASP_TP): - is_turbo_prop = True if data_format == EngineDeckType.GASP_TP else False + elif data_format in (EngineDeckType.GASP, EngineDeckType.GASP_TS): + is_turbo_prop = True if data_format == EngineDeckType.GASP_TS else False temperature = gasp_keys.pop() fuelflow = gasp_keys.pop() if is_turbo_prop: @@ -368,8 +368,8 @@ def _read_gasp_engine(fp, is_turbo_prop=False): Each table consists of both the independent variables and the dependent variable for the corresponding field. The table is a "tidy format" 2D array where the first three columns are the independent varaiables (altitude, T4/T2, and Mach number) and the - final column is the dependent variable (one of thrust, fuelflow, or airflow for TurboFans or - shaft_power_corrected, fuelflow, or tailpipe_thrust for TurboProps). + final column is the dependent variable (one of thrust, fuelflow, or airflow for + turbofans or shaft_power_corrected, fuelflow, or tailpipe_thrust for turboshafts). """ with open(fp, "r") as f: if is_turbo_prop: @@ -435,8 +435,8 @@ def _read_table(f, is_turbo_prop=False): """Read an entire table from a GASP engine deck file. The table data is returned as a "tidy format" array with three columns for the independent variables (altitude, T4/T2, and Mach number) and the final column for - the table field (one of thrust, fuelflow, or airflow for TurboFans or - shaft_power_corrected, fuelflow, or tailpipe_thrust for TurboProps). + the table field (one of thrust, fuelflow, or airflow for turbofans or + shaft_power_corrected, fuelflow, or tailpipe_thrust for turboshafts). """ tab_data = None @@ -501,6 +501,13 @@ def _make_structured_grid(data, method="lagrange3", fields=["thrust", "fuelflow" # would explicitly use lagrange3 here to mimic GASP, but some engine # decks may not have enough points per dimension + # For GASP engine deck, try to provide at least 4 Mach numbers. + # For GASP_TP engine deck, try to provide at least 4 Mach numbers + # avoid devide-by-zero RuntimeWarning + if len(mach) == 3 and method == "lagrange3": + method = "lagrange2" + elif len(mach) == 2: + method = "slinear" interp = InterpND( method="2D-" + method, points=(t4t2, mach), values=f, extrapolate=True ) diff --git a/aviary/utils/fortran_to_aviary.py b/aviary/utils/fortran_to_aviary.py index 8d87b7dcc..426e98ca2 100644 --- a/aviary/utils/fortran_to_aviary.py +++ b/aviary/utils/fortran_to_aviary.py @@ -46,6 +46,9 @@ def create_aviary_deck(fortran_deck: str, legacy_code=None, defaults_deck=None, If an invalid filepath is given, pre-packaged resources will be checked for input decks with a matching name. ''' + # compatibility with being passed int for verbosity + verbosity = Verbosity(verbosity) + # TODO generate both an Aviary input file and a phase_info file vehicle_data = {'input_values': NamedValues(), 'unused_values': NamedValues(), @@ -59,7 +62,9 @@ def create_aviary_deck(fortran_deck: str, legacy_code=None, defaults_deck=None, comments.append(f'# created {timestamp} by {user}') comments.append( - f'# {legacy_code.value}-derived aircraft input deck converted from {fortran_deck.name}') + f'# {legacy_code.value}-derived aircraft input deck converted from ' + f'{fortran_deck.name}' + ) if out_file: out_file = Path(out_file) @@ -101,13 +106,13 @@ def create_aviary_deck(fortran_deck: str, legacy_code=None, defaults_deck=None, if not force: raise RuntimeError(f'{out_file} already exists. Choose a new name or enable ' '--force') - elif verbosity.value >= 1: + elif verbosity >= Verbosity.BRIEF: print(f'Overwriting existing file: {out_file.name}') else: # create any directories defined by the new filename if they don't already exist out_file.parent.mkdir(parents=True, exist_ok=True) - if verbosity.value >= 2: + if verbosity >= Verbosity.VERBOSE: print('Writing to:', out_file) # open the file in write mode @@ -275,7 +280,7 @@ def process_and_store_data(data, var_name, legacy_code, current_namelist, altern vehicle_data['unused_values'] = set_value(name, var_values, vehicle_data['unused_values'], var_ind=var_ind, units=data_units) - if vehicle_data['verbosity'].value >= 2: + if vehicle_data['verbosity'].value >= Verbosity.VERBOSE: print('Unused:', name, var_values, comment) return vehicle_data @@ -363,14 +368,16 @@ def update_name(alternate_names, var_name, verbosity=Verbosity.BRIEF): if altname.endswith(var_name.lower()): all_equivalent_names.append(key) continue - elif var_ind is not None and altname.endswith(f'{var_name.lower()}({var_ind})'): + elif var_ind is not None and altname.endswith( + f'{var_name.lower()}({var_ind})' + ): all_equivalent_names.append(key) var_ind = None continue # if there are no equivalent variable names, return the original name if len(all_equivalent_names) == 0: - if verbosity.value >= 2: + if verbosity >= Verbosity.VERBOSE: print('passing: ', var_name) all_equivalent_names = [var_name] @@ -658,7 +665,7 @@ def _exec_F2A(args, user_args): args.input_deck = args.input_deck[0] filepath = args.input_deck - # convert verbosity from number to enum + # convert verbosity from int to enum verbosity = Verbosity(args.verbosity) create_aviary_deck(filepath, args.legacy_code, args.defaults_deck, diff --git a/aviary/utils/functions.py b/aviary/utils/functions.py index 199ebc19e..39c97ab06 100644 --- a/aviary/utils/functions.py +++ b/aviary/utils/functions.py @@ -9,7 +9,7 @@ import numpy as np from openmdao.utils.units import convert_units -from aviary.utils.aviary_values import AviaryValues, get_keys +from aviary.utils.aviary_values import AviaryValues, get_keys, get_items from aviary.variable_info.enums import ProblemType, EquationsOfMotion, LegacyCode from aviary.variable_info.functions import add_aviary_output, add_aviary_input from aviary.variable_info.variable_meta_data import _MetaData @@ -46,43 +46,58 @@ def get_aviary_resource_path(resource_name: str) -> str: return path -def set_aviary_initial_values(model, inputs, meta_data=_MetaData): - ''' - This function sorts through all the input - variables to an Aviary model, and for those - which are not options it sets the input - value to be the value in the inputs, or - to be the default if the value is not in the - inputs. - - In the case when the value is not input nor - present in the default, nothing is set. - ''' - for key in meta_data: - if ':' not in key or key.startswith('dynamic:'): +def set_aviary_initial_values(prob, aviary_inputs: AviaryValues): + """ + Sets initial values for all inputs in the aviary inputs. + + This method is mostly used in tests and level 3 scripts. + + Parameters + ---------- + prob : Problem + OpenMDAO problem after setup. + aviary_inputs : AviaryValues + Instance of AviaryValues containing all initial values. + """ + for (key, (val, units)) in get_items(aviary_inputs): + try: + prob.set_val(key, val, units) + + except: + # Should be an option or an overridden output. continue - if not meta_data[key]['option']: - if key in inputs: - val, units = inputs.get_item(key) - else: - val = meta_data[key]['default_value'] - units = meta_data[key]['units'] - if val is None: - # optional, but no default value - continue - model.set_input_defaults(key, val=val, units=units) +def set_aviary_input_defaults(model, inputs, aviary_inputs: AviaryValues, + meta_data=_MetaData): + """ + This function sets the default values and units for any inputs prior to + setup. This is needed to resolve ambiguities when inputs are promoted + with the same name, but different units or values. + This method is mostly used in tests and level 3 scripts. -def apply_all_values(aircraft_values: AviaryValues, prob): - for var_name in get_keys(aircraft_values): - var_data, var_units = aircraft_values.get_item(var_name) - try: - prob.set_val(var_name, val=var_data, units=var_units) - except KeyError: - pass - return prob + Parameters + ---------- + model : System + Top level aviary model. + inputs : list + List of varibles that are causing promotion problems. This needs to + be crafted based on the openmdao exception messages. + aviary_inputs : AviaryValues + Instance of AviaryValues containing all initial values. + meta_data : dict + (Optional) Dictionary of aircraft metadata. Uses Aviary's built-in + metadata by default. + """ + for key in inputs: + if key in aviary_inputs: + val, units = aviary_inputs.get_item(key) + else: + val = meta_data[key]['default_value'] + units = meta_data[key]['units'] + + model.set_input_defaults(key, val=val, units=units) def convert_strings_to_data(string_list): diff --git a/aviary/utils/process_input_decks.py b/aviary/utils/process_input_decks.py index 2d9edcc68..080112200 100644 --- a/aviary/utils/process_input_decks.py +++ b/aviary/utils/process_input_decks.py @@ -182,11 +182,13 @@ def parse_inputs(vehicle_deck, aircraft_values: AviaryValues = None, initial_gue elif ":" in var_name: warnings.warn( - f"Variable '{var_name}' is not in meta_data nor in 'guess_names'. It will be ignored.", - UserWarning) + f"Variable '{var_name}' is not in meta_data nor in 'guess_names'. " + "It will be ignored.", + UserWarning, + ) continue - if aircraft_values.get_val(Settings.VERBOSITY).value >= 2: + if aircraft_values.get_val(Settings.VERBOSITY) >= Verbosity.VERBOSE: print('Unused:', var_name, var_values, comment) return aircraft_values, initial_guesses @@ -230,7 +232,7 @@ def update_GASP_options(aircraft_values: AviaryValues): aircraft_values.set_val( Aircraft.Wing.FOLD_DIMENSIONAL_LOCATION_SPECIFIED, val=False) - if aircraft_values.get_val(Settings.VERBOSITY).value >= 2: + if aircraft_values.get_val(Settings.VERBOSITY) >= Verbosity.VERBOSE: print('\nOptions') for key in get_keys(aircraft_values): val, units = aircraft_values.get_item(key) @@ -402,7 +404,7 @@ def initial_guessing(aircraft_values: AviaryValues, initial_guesses, engine_buil initial_guesses['climb_range'] = initial_guesses['time_to_climb'] / \ (60 * 60) * (avg_speed_guess * np.cos(gamma_guess)) - if aircraft_values.get_val(Settings.VERBOSITY).value >= 2: + if aircraft_values.get_val(Settings.VERBOSITY) >= Verbosity.VERBOSE: print('\nInitial Guesses') for key, value in initial_guesses.items(): print(key, value) diff --git a/aviary/utils/propeller_map_conversion.py b/aviary/utils/propeller_map_conversion.py index 3fb418d5f..51183fabd 100644 --- a/aviary/utils/propeller_map_conversion.py +++ b/aviary/utils/propeller_map_conversion.py @@ -26,11 +26,13 @@ def __str__(self): def PropDataConverter(input_file, output_file, data_format: PropMapType): - """This is a utility class to convert a propeller map file to Aviary format. + """ + This is a utility class to convert a propeller map file to Aviary format. Currently, there is only one option: from GASP format to Aviary format. As an Aviary command, the usage is: - aviary convert_prop_table -F GASP input_file output_file + aviary convert_prop_table -f GASP input_file output_file """ + timestamp = datetime.now().strftime('%m/%d/%y at %H:%M') comments = [] header = {} diff --git a/aviary/utils/test/test_drag_polar_data.csv b/aviary/utils/test/test_drag_polar_data.csv new file mode 100644 index 000000000..558d2b8ff --- /dev/null +++ b/aviary/utils/test/test_drag_polar_data.csv @@ -0,0 +1,145 @@ +altitude ,mach_number ,angle_of_attack,lift_coefficient, total_drag_coefficient +0,0.2,-2,0.094876064,0.009822906 +0,0.2,0,0.312245244,0.012348892 +0,0.2,2,0.526732695,0.018104994 +0,0.2,4,0.738624783,0.026931199 +0,0.2,6,0.948256669,0.038891873 +0,0.2,10,1.37381249,0.072045321 +0,0.4,-2,0.10109343,0.00897394 +0,0.4,0,0.331242642,0.011794696 +0,0.4,2,0.558215806,0.018197124 +0,0.4,4,0.78234899,0.028002964 +0,0.4,6,1.00403071,0.041269684 +0,0.4,10,1.4548071,0.078202412 +0,0.5,-2,0.106943991,0.008799865 +0,0.5,0,0.348482284,0.011907858 +0,0.5,2,0.586591715,0.018925735 +0,0.5,4,0.821646945,0.029659873 +0,0.5,6,1.0540806,0.044158552 +0,0.5,10,1.52725612,0.084663215 +0,0.6,-2,0.116201724,0.008755005 +0,0.6,0,0.374581734,0.012332239 +0,0.6,2,0.628753876,0.020345718 +0,0.6,4,0.880405735,0.032546498 +0,0.6,6,1.12886908,0.048978839 +0,0.6,10,1.63519161,0.095162308 +0,0.7,-2,0.132280494,0.008902517 +0,0.7,0,0.416816813,0.013336078 +0,0.7,2,0.696795397,0.023089868 +0,0.7,4,0.973468481,0.037876912 +0,0.7,6,1.24662498,0.057702287 +0,0.7,10,1.80500261,0.113703109 +0,0.8,-2,0.167428501,0.00957739 +0,0.8,0,0.497855602,0.016016403 +0,0.8,2,0.823365761,0.029601436 +0,0.8,4,1.14426867,0.049986007 +0,0.8,6,1.46094114,0.07706223 +0,0.8,10,2.11188335,0.153895725 +20000,0.2,-2,0.094876064,0.010748936 +20000,0.2,0,0.312245244,0.013278551 +20000,0.2,2,0.526732695,0.019035924 +20000,0.2,4,0.738624783,0.027861195 +20000,0.2,6,0.948256669,0.039818804 +20000,0.2,10,1.37381249,0.072958942 +20000,0.4,-2,0.10109343,0.009771491 +20000,0.4,0,0.331242642,0.01259557 +20000,0.4,2,0.558215806,0.018999276 +20000,0.4,4,0.78234899,0.028804499 +20000,0.4,6,1.00403071,0.042068785 +20000,0.4,10,1.4548071,0.078990525 +20000,0.5,-2,0.106943991,0.009561 +20000,0.5,0,0.348482284,0.012672322 +20000,0.5,2,0.586591715,0.019691561 +20000,0.5,4,0.821646945,0.030425263 +20000,0.5,6,1.0540806,0.044921782 +20000,0.5,10,1.52725612,0.085416334 +20000,0.6,-2,0.116201724,0.009487973 +20000,0.6,0,0.374581734,0.013068627 +20000,0.6,2,0.628753876,0.021083571 +20000,0.6,4,0.880405735,0.03328418 +20000,0.6,6,1.12886908,0.04971467 +20000,0.6,10,1.63519161,0.095888927 +20000,0.7,-2,0.132280494,0.009612703 +20000,0.7,0,0.416816813,0.014049894 +20000,0.7,2,0.696795397,0.023805393 +20000,0.7,4,0.973468481,0.038592556 +20000,0.7,6,1.24662498,0.058416441 +20000,0.7,10,1.80500261,0.11440913 +20000,0.8,-2,0.167428501,0.010268506 +20000,0.8,0,0.497855602,0.016711584 +20000,0.8,2,0.823365761,0.030298771 +20000,0.8,4,1.14426867,0.050683899 +20000,0.8,6,1.46094114,0.077759149 +20000,0.8,10,2.11188335,0.154586048 +30000,0.2,-2,0.094876064,0.01132055 +30000,0.2,0,0.312245244,0.013852395 +30000,0.2,2,0.526732695,0.019610547 +30000,0.2,4,0.738624783,0.028435235 +30000,0.2,6,0.948256669,0.040390954 +30000,0.2,10,1.37381249,0.073522871 +30000,0.4,-2,0.10109343,0.010261747 +30000,0.4,0,0.331242642,0.013087861 +30000,0.4,2,0.558215806,0.019492345 +30000,0.4,4,0.78234899,0.029297188 +30000,0.4,6,1.00403071,0.042559974 +30000,0.4,10,1.4548071,0.079474954 +30000,0.5,-2,0.106943991,0.010028278 +30000,0.5,0,0.348482284,0.013141634 +30000,0.5,2,0.586591715,0.020161704 +30000,0.5,4,0.821646945,0.030895132 +30000,0.5,6,1.0540806,0.045390324 +30000,0.5,10,1.52725612,0.085878672 +30000,0.6,-2,0.116201724,0.009937502 +30000,0.6,0,0.374581734,0.013520245 +30000,0.6,2,0.628753876,0.02153608 +30000,0.6,4,0.880405735,0.033736581 +30000,0.6,6,1.12886908,0.050165931 +30000,0.6,10,1.63519161,0.09633454 +30000,0.7,-2,0.132280494,0.010047891 +30000,0.7,0,0.416816813,0.0144873 +30000,0.7,2,0.696795397,0.024243841 +30000,0.7,4,0.973468481,0.039031073 +30000,0.7,6,1.24662498,0.058854041 +30000,0.7,10,1.80500261,0.114841741 +30000,0.8,-2,0.167428501,0.010691707 +30000,0.8,0,0.497855602,0.017137263 +30000,0.8,2,0.823365761,0.030725762 +30000,0.8,4,1.14426867,0.051111226 +30000,0.8,6,1.46094114,0.078185878 +30000,0.8,10,2.11188335,0.155008735 +40000,0.2,-2,0.094876064,0.012082662 +40000,0.2,0,0.312245244,0.01461747 +40000,0.2,2,0.526732695,0.020376651 +40000,0.2,4,0.738624783,0.029200558 +40000,0.2,6,0.948256669,0.041153752 +40000,0.2,10,1.37381249,0.074274716 +40000,0.4,-2,0.10109343,0.010913067 +40000,0.4,0,0.331242642,0.013741874 +40000,0.4,2,0.558215806,0.020147387 +40000,0.4,4,0.78234899,0.029951717 +40000,0.4,6,1.00403071,0.04321251 +40000,0.4,10,1.4548071,0.080118518 +40000,0.5,-2,0.106943991,0.0106484 +40000,0.5,0,0.348482284,0.013764447 +40000,0.5,2,0.586591715,0.020785612 +40000,0.5,4,0.821646945,0.031518677 +40000,0.5,6,1.0540806,0.046012099 +40000,0.5,10,1.52725612,0.086492212 +40000,0.6,-2,0.116201724,0.010533556 +40000,0.6,0,0.374581734,0.014119058 +40000,0.6,2,0.628753876,0.02213607 +40000,0.6,4,0.880405735,0.034336421 +40000,0.6,6,1.12886908,0.05076426 +40000,0.6,10,1.63519161,0.096925373 +40000,0.7,-2,0.132280494,0.010624519 +40000,0.7,0,0.416816813,0.015066856 +40000,0.7,2,0.696795397,0.024824771 +40000,0.7,4,0.973468481,0.039612086 +40000,0.7,6,1.24662498,0.059433833 +40000,0.7,10,1.80500261,0.115414928 +40000,0.8,-2,0.167428501,0.011252107 +40000,0.8,0,0.497855602,0.017700938 +40000,0.8,2,0.823365761,0.031291167 +40000,0.8,4,1.14426867,0.051677067 +40000,0.8,6,1.46094114,0.078750926 +40000,0.8,10,2.11188335,0.155568425 diff --git a/aviary/utils/test/test_engine_deck_conversion.py b/aviary/utils/test/test_engine_deck_conversion.py index c9e281e36..e3ec50a25 100644 --- a/aviary/utils/test/test_engine_deck_conversion.py +++ b/aviary/utils/test/test_engine_deck_conversion.py @@ -62,7 +62,11 @@ def compare_files(self, filepath, skip_list=[]): self.assertEqual(line_no_whitespace.count(expected_line), 1) except Exception as error: - exc_string = f'Error: {filename}\nFound: {line_no_whitespace}\nExpected: {expected_line}' + exc_string = ( + f'Error: {filename}\n' + f'Found: {line_no_whitespace}\n' + f'Expected: {expected_line}' + ) raise Exception(exc_string) # TODO currently untested!! @@ -70,9 +74,9 @@ def compare_files(self, filepath, skip_list=[]): # return def test_TP_conversion(self): - filename = 'turboprop_4465hp.eng' + filename = 'turboshaft_4465hp.eng' - args = self.prepare_and_run(filename, data_format=EngineDeckType.GASP_TP) + args = self.prepare_and_run(filename, data_format=EngineDeckType.GASP_TS) self.compare_files(filename, skip_list=['# created']) diff --git a/aviary/utils/test/test_fortran_to_aviary.py b/aviary/utils/test/test_fortran_to_aviary.py index ecbb016cc..5f99a9962 100644 --- a/aviary/utils/test/test_fortran_to_aviary.py +++ b/aviary/utils/test/test_fortran_to_aviary.py @@ -14,7 +14,7 @@ def __init__(self): self.legacy_code = None self.defaults_deck = False self.force = False - self.verbosity = 1 + self.verbosity = 0 @use_tempdirs @@ -62,7 +62,11 @@ def compare_files(self, filepath, skip_list=[]): self.assertEqual(line_no_whitespace.count(expected_line), 1) except Exception as error: - exc_string = f'Error: {filename}\nFound: {line_no_whitespace}\nExpected: {expected_line}' + exc_string = ( + f'Error: {filename}\n' + f'Found: {line_no_whitespace}\n' + f'Expected: {expected_line}' + ) raise Exception(exc_string) def test_large_single_aisle(self): diff --git a/aviary/utils/test_utils/variable_test.py b/aviary/utils/test_utils/variable_test.py index 05d90ddd2..ea498de77 100644 --- a/aviary/utils/test_utils/variable_test.py +++ b/aviary/utils/test_utils/variable_test.py @@ -145,7 +145,22 @@ def assert_metadata_alphabetization(metadata_variables_list): ) -def assert_match_varnames(system, MetaData=None): +def assert_match_varnames(system, MetaData=None, exclude_inputs=None, exclude_outputs=None): + """ + Assert that the inputs and outputs of the system match with those in MetaData + + Parameters: + ----------- + exclude_inputs: set + The inputs that are excluded from comparison with MetaData. + exclude_outputs: set + The outputs that are excluded from comparison with MetaData. + + Returns + ------- + list + List of all names in the hierarchy, including duplicates. + """ prob = om.Problem() prob.model = system @@ -165,15 +180,21 @@ def assert_match_varnames(system, MetaData=None): if input_overlap != sys_inputs: diff = sys_inputs - input_overlap - raise ValueError( - f"The inputs {diff} in the provided subsystem are not found in the provided variable structure." - ) + if not exclude_inputs is None: + diff = diff - exclude_inputs + if len(diff) > 0: + raise ValueError( + f"The inputs {diff} in the provided subsystem are not found in the provided variable structure." + ) if output_overlap != sys_outputs: diff = sys_outputs - output_overlap - raise ValueError( - f"The outputs {diff} in the provided subsystem are not found in the provided variable structure." - ) + if not exclude_outputs is None: + diff = diff - exclude_outputs + if len(diff) > 0: + raise ValueError( + f"The outputs {diff} in the provided subsystem are not found in the provided variable structure." + ) def get_names_from_hierarchy(hierarchy): diff --git a/aviary/validation_cases/benchmark_tests/test_0_iters.py b/aviary/validation_cases/benchmark_tests/test_0_iters.py index 18cbcadc6..7acf75e60 100644 --- a/aviary/validation_cases/benchmark_tests/test_0_iters.py +++ b/aviary/validation_cases/benchmark_tests/test_0_iters.py @@ -4,7 +4,6 @@ from openmdao.utils.testing_utils import require_pyoptsparse, use_tempdirs from aviary.interface.methods_for_level2 import AviaryProblem -from aviary.variable_info.enums import Verbosity from aviary.interface.default_phase_info.two_dof import phase_info as two_dof_phase_info from aviary.interface.default_phase_info.height_energy import phase_info as height_energy_phase_info from aviary.models.N3CC.N3CC_data import inputs @@ -23,7 +22,7 @@ def build_and_run_problem(self, input_filename, phase_info, objective_type=None) prob.add_phases() prob.add_post_mission_systems() prob.link_phases() - prob.add_driver("SLSQP", max_iter=0, verbosity=Verbosity.QUIET) + prob.add_driver("SLSQP", max_iter=0, verbosity=0) prob.add_design_variables() prob.add_objective(objective_type if objective_type else None) prob.setup() diff --git a/aviary/validation_cases/benchmark_tests/test_FLOPS_balanced_field_length.py b/aviary/validation_cases/benchmark_tests/test_FLOPS_balanced_field_length.py index ed62ffd76..31eb017cc 100644 --- a/aviary/validation_cases/benchmark_tests/test_FLOPS_balanced_field_length.py +++ b/aviary/validation_cases/benchmark_tests/test_FLOPS_balanced_field_length.py @@ -1,3 +1,6 @@ +""" +Balanced field length optimization implemented with the Level 3 API. +""" import copy import unittest import warnings @@ -8,9 +11,8 @@ from openmdao.utils.assert_utils import assert_near_equal from openmdao.utils.testing_utils import require_pyoptsparse, use_tempdirs -from aviary.interface.methods_for_level2 import AviaryProblem - -from aviary.utils.functions import set_aviary_initial_values +from aviary.utils.functions import \ + set_aviary_initial_values, set_aviary_input_defaults from aviary.utils.preprocessors import preprocess_options from aviary.models.N3CC.N3CC_data import \ balanced_liftoff_user_options as _takeoff_liftoff_user_options @@ -18,8 +20,7 @@ balanced_trajectory_builder as _takeoff_trajectory_builder from aviary.models.N3CC.N3CC_data import \ inputs as _inputs -from aviary.variable_info.variables import Dynamic -from aviary.variable_info.variables_in import VariablesIn +from aviary.variable_info.variables import Dynamic, Aircraft from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems from aviary.subsystems.premission import CorePreMission @@ -100,22 +101,18 @@ def _do_run(self, driver: Driver, optimizer, *args): liftoff.add_objective( Dynamic.Mission.DISTANCE, loc='final', ref=distance_max, units=units) - takeoff.model.add_subsystem( - 'input_sink', - VariablesIn(aviary_options=aviary_options), - promotes_inputs=['*'], - promotes_outputs=['*'] - ) + varnames = [Aircraft.Wing.ASPECT_RATIO] + set_aviary_input_defaults(takeoff.model, varnames, aviary_options) # suppress warnings: # "input variable '...' promoted using '*' was already promoted using 'aircraft:*' with warnings.catch_warnings(): - # Set initial default values for all aircraft variables. - set_aviary_initial_values(takeoff.model, aviary_options) warnings.simplefilter("ignore", om.PromotionWarning) takeoff.setup(check=True) + set_aviary_initial_values(takeoff, aviary_options) + # Turn off solver printing so that the SNOPT output is readable. takeoff.set_solver_print(level=0) diff --git a/aviary/validation_cases/benchmark_tests/test_FLOPS_based_sizing_N3CC.py b/aviary/validation_cases/benchmark_tests/test_FLOPS_based_sizing_N3CC.py index 50e5ff9fc..b00e59201 100644 --- a/aviary/validation_cases/benchmark_tests/test_FLOPS_based_sizing_N3CC.py +++ b/aviary/validation_cases/benchmark_tests/test_FLOPS_based_sizing_N3CC.py @@ -1,28 +1,29 @@ -''' -NOTES: +""" +Sizing the N3CC using the level 3 API. + Includes: -Takeoff, Climb, Cruise, Descent, Landing -Computed Aero -N3CC data -''' + Takeoff, Climb, Cruise, Descent, Landing + Computed Aero + N3CC data +""" import unittest -import dymos as dm import numpy as np -import openmdao.api as om import scipy.constants as _units + +import dymos as dm +import openmdao.api as om from openmdao.utils.assert_utils import assert_near_equal from openmdao.utils.testing_utils import use_tempdirs from openmdao.utils.testing_utils import require_pyoptsparse from aviary.mission.flops_based.phases.build_landing import Landing from aviary.mission.flops_based.phases.build_takeoff import Takeoff -from aviary.utils.functions import set_aviary_initial_values +from aviary.utils.functions import set_aviary_input_defaults from aviary.utils.test_utils.assert_utils import warn_timeseries_near_equal from aviary.validation_cases.validation_tests import get_flops_inputs from aviary.variable_info.functions import setup_trajectory_params from aviary.utils.aviary_values import AviaryValues -from aviary.variable_info.variables_in import VariablesIn from aviary.mission.energy_phase import EnergyPhase from aviary.variable_info.variables import Aircraft, Dynamic, Mission @@ -35,8 +36,9 @@ from aviary.subsystems.mass.mass_builder import CoreMassBuilder from aviary.subsystems.aerodynamics.aerodynamics_builder import CoreAerodynamicsBuilder from aviary.subsystems.propulsion.utils import build_engine_deck -from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems +from aviary.utils.functions import set_aviary_initial_values from aviary.utils.preprocessors import preprocess_crewpayload +from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems try: import pyoptsparse @@ -422,18 +424,20 @@ def run_trajectory(sim=True): prob.model.add_objective('reg_objective', ref=1) - # Set initial default values for all LEAPS aircraft variables. - set_aviary_initial_values(prob.model, aviary_inputs) - - prob.model.add_subsystem( - 'input_sink', - VariablesIn(aviary_options=aviary_inputs), - promotes_inputs=['*'], - promotes_outputs=['*'] - ) + varnames = [ + Aircraft.Wing.MAX_CAMBER_AT_70_SEMISPAN, + Aircraft.Wing.SWEEP, + Aircraft.Wing.TAPER_RATIO, + Aircraft.Wing.THICKNESS_TO_CHORD, + Mission.Design.GROSS_MASS, + Mission.Summary.GROSS_MASS, + ] + set_aviary_input_defaults(prob.model, varnames, aviary_inputs) prob.setup(force_alloc_complex=True) + set_aviary_initial_values(prob, aviary_inputs) + ############################################ # Initial Settings for States and Controls # ############################################ diff --git a/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_landing.py b/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_landing.py index 4e8db9bbe..2ade54270 100644 --- a/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_landing.py +++ b/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_landing.py @@ -10,18 +10,18 @@ from aviary.subsystems.premission import CorePreMission -from aviary.utils.functions import set_aviary_initial_values +from aviary.utils.functions import \ + set_aviary_initial_values, set_aviary_input_defaults from aviary.models.N3CC.N3CC_data import ( inputs as _inputs, outputs as _outputs, landing_trajectory_builder as _landing_trajectory_builder, landing_fullstop_user_options as _landing_fullstop_user_options) -from aviary.variable_info.variables import Dynamic +from aviary.variable_info.variables import Aircraft, Dynamic from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems from aviary.utils.preprocessors import preprocess_options -from aviary.variable_info.variables_in import VariablesIn @use_tempdirs @@ -94,22 +94,18 @@ def _do_run(self, driver: Driver, optimizer, *args): fullstop.add_objective(Dynamic.Mission.DISTANCE, loc='final', ref=distance_max, units=units) - landing.model.add_subsystem( - 'input_sink', - VariablesIn(aviary_options=aviary_options), - promotes_inputs=['*'], - promotes_outputs=['*'] - ) + varnames = [Aircraft.Wing.ASPECT_RATIO] + set_aviary_input_defaults(landing.model, varnames, aviary_options) # suppress warnings: # "input variable '...' promoted using '*' was already promoted using 'aircraft:*' with warnings.catch_warnings(): - # Set initial default values for all aircraft variables. - set_aviary_initial_values(landing.model, aviary_options) warnings.simplefilter("ignore", om.PromotionWarning) landing.setup(check=True) + set_aviary_initial_values(landing, aviary_options) + # Turn off solver printing so that the SNOPT output is readable. landing.set_solver_print(level=0) diff --git a/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_takeoff.py b/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_takeoff.py index 2b75a5d49..2ed7b6e71 100644 --- a/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_takeoff.py +++ b/aviary/validation_cases/benchmark_tests/test_FLOPS_detailed_takeoff.py @@ -10,7 +10,8 @@ from aviary.subsystems.premission import CorePreMission -from aviary.utils.functions import set_aviary_initial_values +from aviary.utils.functions import \ + set_aviary_initial_values, set_aviary_input_defaults from aviary.models.N3CC.N3CC_data import ( inputs as _inputs, outputs as _outputs, @@ -21,7 +22,6 @@ from aviary.subsystems.propulsion.utils import build_engine_deck from aviary.utils.test_utils.default_subsystems import get_default_mission_subsystems from aviary.utils.preprocessors import preprocess_options -from aviary.variable_info.variables_in import VariablesIn @use_tempdirs @@ -109,22 +109,18 @@ def _do_run(self, driver: Driver, optimizer, *args): 'traj.takeoff_decision_speed.states:velocity', equals=155.36, units='kn', ref=159.0, indices=[-1]) - takeoff.model.add_subsystem( - 'input_sink', - VariablesIn(aviary_options=aviary_options), - promotes_inputs=['*'], - promotes_outputs=['*'] - ) + varnames = [Aircraft.Wing.ASPECT_RATIO] + set_aviary_input_defaults(takeoff.model, varnames, aviary_options) # suppress warnings: # "input variable '...' promoted using '*' was already promoted using 'aircraft:*' with warnings.catch_warnings(): - # Set initial default values for all aircraft variables. - set_aviary_initial_values(takeoff.model, aviary_options) warnings.simplefilter("ignore", om.PromotionWarning) takeoff.setup(check=True) + set_aviary_initial_values(takeoff, aviary_options) + # Turn off solver printing so that the SNOPT output is readable. takeoff.set_solver_print(level=0) diff --git a/aviary/validation_cases/benchmark_tests/test_bench_FwFm.py b/aviary/validation_cases/benchmark_tests/test_bench_FwFm.py index 7daa6027b..cf3947496 100644 --- a/aviary/validation_cases/benchmark_tests/test_bench_FwFm.py +++ b/aviary/validation_cases/benchmark_tests/test_bench_FwFm.py @@ -9,7 +9,6 @@ from aviary.api import Mission from aviary.interface.methods_for_level1 import run_aviary -from aviary.variable_info.enums import Verbosity from aviary.validation_cases.benchmark_utils import \ compare_against_expected_values @@ -366,17 +365,25 @@ class TestBenchFwFmSerial(ProblemPhaseTestCase): @require_pyoptsparse(optimizer="IPOPT") def test_bench_FwFm_IPOPT(self): - prob = run_aviary('models/test_aircraft/aircraft_for_bench_FwFm.csv', - self.phase_info, verbosity=Verbosity.QUIET, - max_iter=50, optimizer='IPOPT') + prob = run_aviary( + 'models/test_aircraft/aircraft_for_bench_FwFm.csv', + self.phase_info, + verbosity=0, + max_iter=50, + optimizer='IPOPT', + ) compare_against_expected_values(prob, self.expected_dict) @require_pyoptsparse(optimizer="SNOPT") def test_bench_FwFm_SNOPT(self): - prob = run_aviary('models/test_aircraft/aircraft_for_bench_FwFm.csv', - self.phase_info, verbosity=Verbosity.QUIET, - max_iter=50, optimizer='SNOPT') + prob = run_aviary( + 'models/test_aircraft/aircraft_for_bench_FwFm.csv', + self.phase_info, + verbosity=0, + max_iter=50, + optimizer='SNOPT', + ) compare_against_expected_values(prob, self.expected_dict) @@ -395,9 +402,13 @@ class TestBenchFwFmParallel(ProblemPhaseTestCase): @require_pyoptsparse(optimizer="SNOPT") def test_bench_FwFm_SNOPT_MPI(self): - prob = run_aviary('models/test_aircraft/aircraft_for_bench_FwFm.csv', - self.phase_info, verbosity=Verbosity.QUIET, - max_iter=50, optimizer='SNOPT') + prob = run_aviary( + 'models/test_aircraft/aircraft_for_bench_FwFm.csv', + self.phase_info, + verbosity=0, + max_iter=50, + optimizer='SNOPT', + ) compare_against_expected_values(prob, self.expected_dict) diff --git a/aviary/validation_cases/benchmark_tests/test_bench_FwGm.py b/aviary/validation_cases/benchmark_tests/test_bench_FwGm.py index e062eb760..b1f1975d9 100644 --- a/aviary/validation_cases/benchmark_tests/test_bench_FwGm.py +++ b/aviary/validation_cases/benchmark_tests/test_bench_FwGm.py @@ -8,7 +8,6 @@ from aviary.interface.default_phase_info.two_dof import phase_info from aviary.interface.methods_for_level1 import run_aviary from aviary.variable_info.variables import Aircraft, Mission -from aviary.variable_info.enums import Verbosity @use_tempdirs @@ -20,23 +19,28 @@ def setUp(self): @require_pyoptsparse(optimizer="IPOPT") def bench_test_swap_3_FwGm_IPOPT(self): local_phase_info = deepcopy(phase_info) - prob = run_aviary('models/test_aircraft/aircraft_for_bench_FwGm.csv', local_phase_info, - max_iter=100, verbosity=Verbosity.QUIET, optimizer='IPOPT') + prob = run_aviary( + 'models/test_aircraft/aircraft_for_bench_FwGm.csv', + local_phase_info, + max_iter=100, + verbosity=0, + optimizer='IPOPT', + ) rtol = 1e-2 # There are no truth values for these. assert_near_equal(prob.get_val(Mission.Design.GROSS_MASS), - 184533., tolerance=rtol) + 179391., tolerance=rtol) assert_near_equal(prob.get_val(Aircraft.Design.OPERATING_MASS), - 104530., tolerance=rtol) + 101556., tolerance=rtol) assert_near_equal(prob.get_val(Mission.Summary.TOTAL_FUEL_MASS), - 42444., tolerance=rtol) + 39979., tolerance=rtol) assert_near_equal(prob.get_val('landing.' + Mission.Landing.GROUND_DISTANCE), - 2528., tolerance=rtol) + 2595., tolerance=rtol) assert_near_equal(prob.get_val("traj.desc2.timeseries.distance")[-1], 3675.0, tolerance=rtol) @@ -44,23 +48,27 @@ def bench_test_swap_3_FwGm_IPOPT(self): @require_pyoptsparse(optimizer="SNOPT") def bench_test_swap_3_FwGm_SNOPT(self): local_phase_info = deepcopy(phase_info) - prob = run_aviary('models/test_aircraft/aircraft_for_bench_FwGm.csv', - local_phase_info, verbosity=Verbosity.QUIET, optimizer='SNOPT') + prob = run_aviary( + 'models/test_aircraft/aircraft_for_bench_FwGm.csv', + local_phase_info, + verbosity=0, + optimizer='SNOPT', + ) rtol = 1e-2 # There are no truth values for these. assert_near_equal(prob.get_val(Mission.Design.GROSS_MASS), - 186418., tolerance=rtol) + 179390., tolerance=rtol) assert_near_equal(prob.get_val(Aircraft.Design.OPERATING_MASS), - 104530., tolerance=rtol) + 101556., tolerance=rtol) assert_near_equal(prob.get_val(Mission.Summary.TOTAL_FUEL_MASS), - 42942., tolerance=rtol) + 39979., tolerance=rtol) assert_near_equal(prob.get_val('landing.' + Mission.Landing.GROUND_DISTANCE), - 2528., tolerance=rtol) + 2595., tolerance=rtol) assert_near_equal(prob.get_val("traj.desc2.timeseries.distance")[-1], 3675.0, tolerance=rtol) @@ -69,5 +77,5 @@ def bench_test_swap_3_FwGm_SNOPT(self): if __name__ == "__main__": test = ProblemPhaseTestCase() test.setUp() - test.bench_test_swap_3_FwGm_IPOPT() test.bench_test_swap_3_FwGm_SNOPT() + # test.bench_test_swap_3_FwGm_IPOPT() diff --git a/aviary/validation_cases/benchmark_tests/test_bench_GwFm.py b/aviary/validation_cases/benchmark_tests/test_bench_GwFm.py index b22332b07..13e359840 100644 --- a/aviary/validation_cases/benchmark_tests/test_bench_GwFm.py +++ b/aviary/validation_cases/benchmark_tests/test_bench_GwFm.py @@ -13,7 +13,6 @@ from openmdao.core.problem import _clear_problem_names from aviary.interface.methods_for_level1 import run_aviary -from aviary.variable_info.enums import Verbosity from aviary.validation_cases.benchmark_utils import \ compare_against_expected_values @@ -361,15 +360,25 @@ def setUp(self): @require_pyoptsparse(optimizer="IPOPT") def bench_test_swap_1_GwFm_IPOPT(self): - prob = run_aviary('models/test_aircraft/aircraft_for_bench_GwFm.csv', self.phase_info, - max_iter=100, optimizer='IPOPT', verbosity=Verbosity.QUIET) + prob = run_aviary( + 'models/test_aircraft/aircraft_for_bench_GwFm.csv', + self.phase_info, + max_iter=100, + optimizer='IPOPT', + verbosity=0, + ) compare_against_expected_values(prob, self.expected_dict) @require_pyoptsparse(optimizer="SNOPT") def bench_test_swap_1_GwFm_SNOPT(self): - prob = run_aviary('models/test_aircraft/aircraft_for_bench_GwFm.csv', self.phase_info, - max_iter=50, optimizer='SNOPT', verbosity=Verbosity.QUIET) + prob = run_aviary( + 'models/test_aircraft/aircraft_for_bench_GwFm.csv', + self.phase_info, + max_iter=50, + optimizer='SNOPT', + verbosity=0, + ) compare_against_expected_values(prob, self.expected_dict) diff --git a/aviary/validation_cases/benchmark_tests/test_bench_GwGm.py b/aviary/validation_cases/benchmark_tests/test_bench_GwGm.py index 1a6d23d5c..ca18abdf1 100644 --- a/aviary/validation_cases/benchmark_tests/test_bench_GwGm.py +++ b/aviary/validation_cases/benchmark_tests/test_bench_GwGm.py @@ -1,14 +1,14 @@ from copy import deepcopy import unittest +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 openmdao.core.problem import _clear_problem_names from aviary.interface.default_phase_info.two_dof import phase_info from aviary.interface.methods_for_level1 import run_aviary from aviary.variable_info.variables import Aircraft, Mission, Dynamic -from aviary.variable_info.enums import AnalysisScheme, Verbosity +from aviary.variable_info.enums import AnalysisScheme @use_tempdirs @@ -20,56 +20,96 @@ def setUp(self): @require_pyoptsparse(optimizer="IPOPT") def test_bench_GwGm(self): local_phase_info = deepcopy(phase_info) - prob = run_aviary('models/test_aircraft/aircraft_for_bench_GwGm.csv', - local_phase_info, optimizer='IPOPT', verbosity=Verbosity.QUIET) + prob = run_aviary( + 'models/test_aircraft/aircraft_for_bench_GwGm.csv', + local_phase_info, + optimizer='IPOPT', + verbosity=0, + ) rtol = 0.01 # There are no truth values for these. - assert_near_equal(prob.get_val(Mission.Design.GROSS_MASS, units='lbm'), - 174039., tolerance=rtol) + assert_near_equal( + prob.get_val(Mission.Design.GROSS_MASS, units='lbm'), + 174039.0, + tolerance=rtol, + ) - assert_near_equal(prob.get_val(Aircraft.Design.OPERATING_MASS, units='lbm'), - 95509, tolerance=rtol) + assert_near_equal( + prob.get_val(Aircraft.Design.OPERATING_MASS, units='lbm'), + 95509, + tolerance=rtol, + ) - assert_near_equal(prob.get_val(Mission.Summary.TOTAL_FUEL_MASS, units='lbm'), - 41856., tolerance=rtol) + assert_near_equal( + prob.get_val(Mission.Summary.TOTAL_FUEL_MASS, units='lbm'), + 41856.0, + tolerance=rtol, + ) - assert_near_equal(prob.get_val(Mission.Landing.GROUND_DISTANCE, units='ft'), - 2634.8, tolerance=rtol) + assert_near_equal( + prob.get_val(Mission.Landing.GROUND_DISTANCE, units='ft'), + 2634.8, + tolerance=rtol, + ) - assert_near_equal(prob.get_val(Mission.Summary.RANGE, units='NM'), - 3675.0, tolerance=rtol) + assert_near_equal( + prob.get_val(Mission.Summary.RANGE, units='NM'), 3675.0, tolerance=rtol + ) - assert_near_equal(prob.get_val(Mission.Landing.TOUCHDOWN_MASS, units='lbm'), - 136823.47, tolerance=rtol) + assert_near_equal( + prob.get_val(Mission.Landing.TOUCHDOWN_MASS, units='lbm'), + 136823.47, + tolerance=rtol, + ) @require_pyoptsparse(optimizer="SNOPT") def test_bench_GwGm_SNOPT(self): local_phase_info = deepcopy(phase_info) - prob = run_aviary('models/test_aircraft/aircraft_for_bench_GwGm.csv', - local_phase_info, optimizer='SNOPT', verbosity=Verbosity.QUIET) + prob = run_aviary( + 'models/test_aircraft/aircraft_for_bench_GwGm.csv', + local_phase_info, + optimizer='SNOPT', + verbosity=0, + ) rtol = 0.01 # There are no truth values for these. - assert_near_equal(prob.get_val(Mission.Design.GROSS_MASS, units='lbm'), - 174039., tolerance=rtol) + assert_near_equal( + prob.get_val(Mission.Design.GROSS_MASS, units='lbm'), + 174039.0, + tolerance=rtol, + ) - assert_near_equal(prob.get_val(Aircraft.Design.OPERATING_MASS, units='lbm'), - 95509, tolerance=rtol) + assert_near_equal( + prob.get_val(Aircraft.Design.OPERATING_MASS, units='lbm'), + 95509, + tolerance=rtol, + ) - assert_near_equal(prob.get_val(Mission.Summary.TOTAL_FUEL_MASS, units='lbm'), - 42529., tolerance=rtol) + assert_near_equal( + prob.get_val(Mission.Summary.TOTAL_FUEL_MASS, units='lbm'), + 42529.0, + tolerance=rtol, + ) - assert_near_equal(prob.get_val(Mission.Landing.GROUND_DISTANCE, units='ft'), - 2634.8, tolerance=rtol) + assert_near_equal( + prob.get_val(Mission.Landing.GROUND_DISTANCE, units='ft'), + 2634.8, + tolerance=rtol, + ) - assert_near_equal(prob.get_val(Mission.Summary.RANGE, units='NM'), - 3675.0, tolerance=rtol) + assert_near_equal( + prob.get_val(Mission.Summary.RANGE, units='NM'), 3675.0, tolerance=rtol + ) - assert_near_equal(prob.get_val(Mission.Landing.TOUCHDOWN_MASS, units='lbm'), - 136823.47, tolerance=rtol) + assert_near_equal( + prob.get_val(Mission.Landing.TOUCHDOWN_MASS, units='lbm'), + 136823.47, + tolerance=rtol, + ) @require_pyoptsparse(optimizer="SNOPT") def test_bench_GwGm_SNOPT_lbm_s(self): @@ -78,65 +118,111 @@ def test_bench_GwGm_SNOPT_lbm_s(self): 'models/test_aircraft/aircraft_for_bench_GwGm_lbm_s.csv', local_phase_info, optimizer='SNOPT', - verbosity=Verbosity.QUIET, + verbosity=0, ) rtol = 0.01 # There are no truth values for these. - assert_near_equal(prob.get_val(Mission.Design.GROSS_MASS, units='lbm'), - 174039., tolerance=rtol) + assert_near_equal( + prob.get_val(Mission.Design.GROSS_MASS, units='lbm'), + 174039.0, + tolerance=rtol, + ) - assert_near_equal(prob.get_val(Aircraft.Design.OPERATING_MASS, units='lbm'), - 95509, tolerance=rtol) + assert_near_equal( + prob.get_val(Aircraft.Design.OPERATING_MASS, units='lbm'), + 95509, + tolerance=rtol, + ) - assert_near_equal(prob.get_val(Mission.Summary.TOTAL_FUEL_MASS, units='lbm'), - 42529., tolerance=rtol) + assert_near_equal( + prob.get_val(Mission.Summary.TOTAL_FUEL_MASS, units='lbm'), + 42529.0, + tolerance=rtol, + ) - assert_near_equal(prob.get_val(Mission.Landing.GROUND_DISTANCE, units='ft'), - 2634.8, tolerance=rtol) + assert_near_equal( + prob.get_val(Mission.Landing.GROUND_DISTANCE, units='ft'), + 2634.8, + tolerance=rtol, + ) - assert_near_equal(prob.get_val(Mission.Summary.RANGE, units='NM'), - 3675.0, tolerance=rtol) + assert_near_equal( + prob.get_val(Mission.Summary.RANGE, units='NM'), 3675.0, tolerance=rtol + ) - assert_near_equal(prob.get_val(Mission.Landing.TOUCHDOWN_MASS, units='lbm'), - 136823.47, tolerance=rtol) + assert_near_equal( + prob.get_val(Mission.Landing.TOUCHDOWN_MASS, units='lbm'), + 136823.47, + tolerance=rtol, + ) @require_pyoptsparse(optimizer="IPOPT") def test_bench_GwGm_shooting(self): - from aviary.interface.default_phase_info.two_dof_fiti import phase_info, \ - phase_info_parameterization + from aviary.interface.default_phase_info.two_dof_fiti import ( + phase_info, + phase_info_parameterization, + ) + local_phase_info = deepcopy(phase_info) - prob = run_aviary('models/test_aircraft/aircraft_for_bench_GwGm.csv', - local_phase_info, optimizer='IPOPT', run_driver=False, - analysis_scheme=AnalysisScheme.SHOOTING, verbosity=Verbosity.QUIET, - phase_info_parameterization=phase_info_parameterization) + prob = run_aviary( + 'models/test_aircraft/aircraft_for_bench_GwGm.csv', + local_phase_info, + optimizer='IPOPT', + run_driver=False, + analysis_scheme=AnalysisScheme.SHOOTING, + verbosity=0, + phase_info_parameterization=phase_info_parameterization, + ) rtol = 0.01 - assert_near_equal(prob.get_val(Mission.Design.RESERVE_FUEL, units='lbm'), - 4998, tolerance=rtol) + assert_near_equal( + prob.get_val(Mission.Design.RESERVE_FUEL, units='lbm'), 4998, tolerance=rtol + ) - assert_near_equal(prob.get_val(Mission.Design.GROSS_MASS, units='lbm'), - 174039., tolerance=rtol) + assert_near_equal( + prob.get_val(Mission.Design.GROSS_MASS, units='lbm'), + 174039.0, + tolerance=rtol, + ) - assert_near_equal(prob.get_val(Aircraft.Design.OPERATING_MASS, units='lbm'), - 95509, tolerance=rtol) + assert_near_equal( + prob.get_val(Aircraft.Design.OPERATING_MASS, units='lbm'), + 95509, + tolerance=rtol, + ) - assert_near_equal(prob.get_val(Mission.Summary.TOTAL_FUEL_MASS, units='lbm'), - 43574., tolerance=rtol) + assert_near_equal( + prob.get_val(Mission.Summary.TOTAL_FUEL_MASS, units='lbm'), + 43574.0, + tolerance=rtol, + ) - assert_near_equal(prob.get_val(Mission.Landing.GROUND_DISTANCE, units='ft'), - 2623.4, tolerance=rtol) + assert_near_equal( + prob.get_val(Mission.Landing.GROUND_DISTANCE, units='ft'), + 2623.4, + tolerance=rtol, + ) - assert_near_equal(prob.get_val(Mission.Summary.RANGE, units='NM'), - 3774.3, tolerance=rtol) + assert_near_equal( + prob.get_val(Mission.Summary.RANGE, units='NM'), 3774.3, tolerance=rtol + ) - assert_near_equal(prob.get_val(Mission.Landing.TOUCHDOWN_MASS, units='lbm'), - 136823.47, tolerance=rtol) + assert_near_equal( + prob.get_val(Mission.Landing.TOUCHDOWN_MASS, units='lbm'), + 136823.47, + tolerance=rtol, + ) - assert_near_equal(prob.get_val('traj.cruise_' + Dynamic.Mission.DISTANCE + '_final', - units='nmi'), 3668.3, tolerance=rtol) + assert_near_equal( + prob.get_val( + 'traj.cruise_' + Dynamic.Mission.DISTANCE + '_final', units='nmi' + ), + 3668.3, + tolerance=rtol, + ) if __name__ == '__main__': diff --git a/aviary/validation_cases/benchmark_tests/test_subsystems_within_a_mission.py b/aviary/validation_cases/benchmark_tests/test_subsystems_within_a_mission.py index 57e3acdc1..a37df8a49 100644 --- a/aviary/validation_cases/benchmark_tests/test_subsystems_within_a_mission.py +++ b/aviary/validation_cases/benchmark_tests/test_subsystems_within_a_mission.py @@ -5,7 +5,6 @@ from openmdao.utils.testing_utils import use_tempdirs from aviary.interface.methods_for_level2 import AviaryProblem -from aviary.variable_info.enums import Verbosity from aviary.subsystems.test.test_dummy_subsystem import ( PostOnlyBuilder, ArrayGuessSubsystemBuilder, AdditionalArrayGuessSubsystemBuilder, @@ -71,7 +70,7 @@ def test_subsystems_in_a_mission(self): # Link phases and variables prob.link_phases() - prob.add_driver("SLSQP", max_iter=0, verbosity=Verbosity.QUIET) + prob.add_driver("SLSQP", max_iter=0, verbosity=0) prob.add_design_variables() diff --git a/aviary/variable_info/core_promotes.py b/aviary/variable_info/core_promotes.py deleted file mode 100644 index 89dd35279..000000000 --- a/aviary/variable_info/core_promotes.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -Curated list of aviary inputs that are promoted as parameters in the mission. -""" -from aviary.variable_info.variables import Aircraft, Mission - - -core_mission_inputs = [ - 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.HorizontalTail.CHARACTERISTIC_LENGTH, - Aircraft.HorizontalTail.FINENESS, - Aircraft.HorizontalTail.LAMINAR_FLOW_LOWER, - Aircraft.HorizontalTail.LAMINAR_FLOW_UPPER, - Aircraft.HorizontalTail.WETTED_AREA, - Aircraft.VerticalTail.CHARACTERISTIC_LENGTH, - Aircraft.VerticalTail.FINENESS, - Aircraft.VerticalTail.LAMINAR_FLOW_LOWER, - Aircraft.VerticalTail.LAMINAR_FLOW_UPPER, - Aircraft.VerticalTail.WETTED_AREA, - Aircraft.Wing.AREA, - Aircraft.Wing.ASPECT_RATIO, - Aircraft.Wing.CHARACTERISTIC_LENGTH, - Aircraft.Wing.FINENESS, - Aircraft.Wing.INCIDENCE, - 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, - Mission.Takeoff.FINAL_MASS, -] diff --git a/aviary/variable_info/functions.py b/aviary/variable_info/functions.py index 154b9c20e..efb175718 100644 --- a/aviary/variable_info/functions.py +++ b/aviary/variable_info/functions.py @@ -4,7 +4,6 @@ from openmdao.core.component import Component from aviary.utils.aviary_values import AviaryValues -from aviary.variable_info.core_promotes import core_mission_inputs from aviary.variable_info.variable_meta_data import _MetaData # --------------------------- @@ -166,9 +165,9 @@ def setup_trajectory_params( are being used in the trajectory, and for the variables which are not options it adds them as a parameter of the trajectory. """ - # TODO: variables_to_add might be an unused option. + # TODO: variables_to_add is required, so should be an arg, not a kwarg. if variables_to_add is None: - variables_to_add = sorted(core_mission_inputs) + variables_to_add = [] # Step 1: Initialize a dictionary to hold parameters and their associated phases parameters_with_phases = {} diff --git a/aviary/variable_info/variable_meta_data.py b/aviary/variable_info/variable_meta_data.py index 2a86612e0..45cbd12d5 100644 --- a/aviary/variable_info/variable_meta_data.py +++ b/aviary/variable_info/variable_meta_data.py @@ -7981,7 +7981,7 @@ '0. QUIET: All output except errors are suppressed' '1. BRIEF: Only important information is output, in human-readable format' '2. VERBOSE: All user-relevant information is output, in human-readable format' - '3. DEBUG: All information is output, including warnings, intermediate calculations, etc., no formatting requirement', + '3. DEBUG: Any information can be outtputed, including warnings, intermediate calculations, etc., with no formatting requirement', option=True, types=Verbosity, default_value=Verbosity.BRIEF diff --git a/aviary/variable_info/variables.py b/aviary/variable_info/variables.py index 16eced995..b0498199f 100644 --- a/aviary/variable_info/variables.py +++ b/aviary/variable_info/variables.py @@ -732,7 +732,7 @@ class Objectives: RANGE = 'mission:objectives:range' class Summary: - # These values are inputs and outputs to/from dynamic analysis + # These values are inputs and outputs to/from mission analysis # for the given mission (whether it is design or off-design). # In on-design these may be constrained to design values, but # in off-design they independently represent the final analysis diff --git a/aviary/variable_info/variables_in.py b/aviary/variable_info/variables_in.py deleted file mode 100644 index 205040710..000000000 --- a/aviary/variable_info/variables_in.py +++ /dev/null @@ -1,63 +0,0 @@ -''' -Dummy explicit component which serves as an input port for all variables in -the aircraft and mission hierarchy. -''' -import numpy as np - -import openmdao.api as om - -from aviary.utils.aviary_values import AviaryValues -from aviary.variable_info.functions import add_aviary_input -from aviary.variable_info.variable_meta_data import _MetaData -from aviary.variable_info.functions import add_aviary_input -from aviary.variable_info.core_promotes import core_mission_inputs - - -class VariablesIn(om.ExplicitComponent): - ''' - Provides a central place to connect input variable information to a component - but doesn't actually do anything on its own. - ''' - - def initialize(self): - self.options.declare( - 'aviary_options', types=AviaryValues, - desc='collection of Aircraft/Mission specific options') - self.options.declare( - 'meta_data', types=dict, default=_MetaData, - desc='variable metadata associated with the variables to be passed through this port' - ) - self.options.declare( - 'context', default='full', values=['full', 'mission'], - desc='Limit to a subset of the aircraft and mission variables.' - ) - - def setup(self): - aviary_options: AviaryValues = self.options['aviary_options'] - meta_data = self.options['meta_data'] - context = self.options['context'] - - if context == 'mission': - inputs = core_mission_inputs - else: - inputs = meta_data - - for key in inputs: - # TODO temp line to ignore dynamic mission variables, will not work - # if names change to 'dynamic:mission:*' - if ':' not in key: - continue - info = meta_data[key] - - if not info['option'] and ('aircraft:' in key or 'mission:' in key): - # Since all the variable initial values are stored in aviary_options, - # we can use the initial values to get the correct shape. - val = info['default_value'] - if val is None: - val = 0.0 - item = val, info['units'] - val, units = aviary_options.get_item(key, item) - if units == 'unitless' and info['units'] != 'unitless': - units = info['units'] - - add_aviary_input(self, key, val=val, units=units, meta_data=meta_data,) diff --git a/aviary/visualization/dashboard.py b/aviary/visualization/dashboard.py index 5738f1a4e..ee652845e 100644 --- a/aviary/visualization/dashboard.py +++ b/aviary/visualization/dashboard.py @@ -1,22 +1,22 @@ import argparse +from collections import defaultdict +from dataclasses import dataclass +import importlib.util import json import os -from pathlib import Path import pathlib +import re import shutil -import importlib.util -from string import Template -from dataclasses import dataclass -from typing import ( - List, - Iterator, - Tuple, -) import warnings -import zipfile # Use typing.List and typing.Tuple for compatibility +import zipfile import numpy as np -from bokeh.palettes import Category10 + +import bokeh.palettes as bp +from bokeh.models import Legend, CheckboxGroup, CustomJS +from bokeh.plotting import figure +from bokeh.models import ColumnDataSource + import hvplot.pandas # noqa # need this ! Otherwise hvplot using DataFrames does not work import pandas as pd import panel as pn @@ -24,6 +24,7 @@ import openmdao.api as om from openmdao.utils.general_utils import env_truthy +from openmdao.utils.units import conversion_to_base_units try: from openmdao.utils.gui_testing_utils import get_free_port except: @@ -32,6 +33,8 @@ def get_free_port(): return 5000 from openmdao.utils.om_warnings import issue_warning +from dymos.visualization.timeseries.bokeh_timeseries_report import _meta_tree_subsys_iter + from aviary.visualization.aircraft_3d_model import Aircraft3DModel # support getting this function from OpenMDAO post movement of the function to utils @@ -45,10 +48,11 @@ def get_free_port(): import aviary.api as av +# Enable Panel extensions pn.extension(sizing_mode="stretch_width") +# Initialize any custom extensions pn.extension('tabulator') - # Constants aviary_variables_json_file_name = "aviary_vars.json" documentation_text_align = 'left' @@ -114,6 +118,13 @@ def _dashboard_setup_parser(parser): default=0, help="dashboard server port ID (default is 0, which indicates get any free port)", ) + parser.add_argument( + "-b", + "--background", + action="store_true", + dest="run_in_background", + help="Run the server in the background (don't automatically open the browser)", + ) # For future use parser.add_argument( @@ -178,6 +189,7 @@ def _dashboard_cmd(options, user_args): options.problem_recorder, options.driver_recorder, options.port, + options.run_in_background, ) return @@ -196,6 +208,7 @@ def _dashboard_cmd(options, user_args): options.problem_recorder, options.driver_recorder, options.port, + options.run_in_background, ) @@ -235,10 +248,10 @@ def create_csv_frame(csv_filepath, documentation): Returns ------- pane : Panel.Pane or None - A Panel Pane object showing the tabular display of the CSV file contents. + A Panel Pane object showing the tabular display of the CSV file contents. Or None if the CSV file does not exist. """ - if os.path.exists(csv_filepath): + if os.path.isfile(csv_filepath): df = pd.read_csv(csv_filepath) df_pane = pn.widgets.Tabulator( df, @@ -254,7 +267,12 @@ def create_csv_frame(csv_filepath, documentation): df_pane ) else: - report_pane = None + report_pane = pn.Column( + pn.pane.HTML(f"

{documentation}

", + styles={'text-align': documentation_text_align}), + pn.pane.Markdown( + f"# Report not shown because data source CSV file, '{csv_filepath}', not found.") + ) return report_pane @@ -296,7 +314,7 @@ def create_report_frame(format, text_filepath, documentation): pn.pane.HTML(f"

{documentation}

", styles={'text-align': 'left'}), pn.pane.HTML(f"

{text_filepath}

", styles={'text-align': 'left'}) ) - elif os.path.exists(text_filepath): + elif os.path.isfile(text_filepath): if format == "html": iframe_css = 'width=1200px height=800px overflow-x="scroll" overflow="scroll" margin=0px padding=0px border=20px frameBorder=20px scrolling="yes"' report_pane = pn.Column( @@ -319,7 +337,11 @@ def create_report_frame(format, text_filepath, documentation): else: raise RuntimeError(f"Report format of {format} is not supported.") else: - report_pane = None + report_pane = pn.Column( + pn.pane.HTML(f"

{documentation}

", styles={'text-align': 'left'}), + pn.pane.Markdown( + f"# Report not shown because report file, '{text_filepath}', not found.") + ) return report_pane @@ -537,8 +559,17 @@ def create_aircraft_3d_file(recorder_file, reports_dir, outfilepath): aircraft_3d_model.write_file(aircraft_3d_template_filepath, outfilepath) +def _get_interactive_plot_sources(data_by_varname_and_phase, x_varname, y_varname, phase): + x = data_by_varname_and_phase[x_varname][phase] + y = data_by_varname_and_phase[y_varname][phase] + if len(x) > 0 and len(x) == len(y): + return x, y + else: + return [], [] + + # The main script that generates all the tabs in the dashboard -def dashboard(script_name, problem_recorder, driver_recorder, port): +def dashboard(script_name, problem_recorder, driver_recorder, port, run_in_background=False): """ Generate the dashboard app display. @@ -554,17 +585,17 @@ def dashboard(script_name, problem_recorder, driver_recorder, port): HTTP port used for the dashboard webapp. If 0, use any free port """ if "reports/" not in script_name: - reports_dir = f"reports/{script_name}/" + reports_dir = f"reports/{script_name}" else: reports_dir = script_name - if not Path(reports_dir).is_dir(): + if not pathlib.Path(reports_dir).is_dir(): raise ValueError( f"The script name, '{script_name}', does not have a reports folder associated with it. " f"The directory '{reports_dir}' does not exist." ) - if not os.path.exists(problem_recorder): + if not os.path.isfile(problem_recorder): issue_warning( f"Given Problem case recorder file {problem_recorder} does not exist.") @@ -576,52 +607,47 @@ def dashboard(script_name, problem_recorder, driver_recorder, port): input_list_pane = create_report_frame("text", "input_list.txt", ''' A plain text display of the model inputs. Recommended for beginners. Only created if Settings.VERBOSITY is set to at least 2 in the input deck. The variables are listed in a tree structure. There are three columns. The left column is a list of variable names, - the middle column is the value, and the right column is the - promoted variable name. The hierarchy is phase, subgroups, components, and variables. An input variable can appear under - different phases and within different components. Its values can be different because its value has - been updated during the computation. On the top-left corner is the total number of inputs. + the middle column is the value, and the right column is the + promoted variable name. The hierarchy is phase, subgroups, components, and variables. An input variable can appear under + different phases and within different components. Its values can be different because its value has + been updated during the computation. On the top-left corner is the total number of inputs. That number counts the duplicates because one variable can appear in different phases.''') - if input_list_pane: - model_tabs_list.append(("Debug Input List", input_list_pane)) + model_tabs_list.append(("Debug Input List", input_list_pane)) # Debug Output List output_list_pane = create_report_frame("text", "output_list.txt", ''' A plain text display of the model outputs. Recommended for beginners. Only created if Settings.VERBOSITY is set to at least 2 in the input deck. The variables are listed in a tree structure. There are three columns. The left column is a list of variable names, - the middle column is the value, and the right column is the - promoted variable name. The hierarchy is phase, subgroups, components, and variables. An output variable can appear under - different phases and within different components. Its values can be different because its value has - been updated during the computation. On the top-left corner is the total number of outputs. + the middle column is the value, and the right column is the + promoted variable name. The hierarchy is phase, subgroups, components, and variables. An output variable can appear under + different phases and within different components. Its values can be different because its value has + been updated during the computation. On the top-left corner is the total number of outputs. That number counts the duplicates because one variable can appear in different phases.''') - if output_list_pane: - model_tabs_list.append(("Debug Output List", output_list_pane)) + model_tabs_list.append(("Debug Output List", output_list_pane)) # Inputs inputs_pane = create_report_frame( "html", f"{reports_dir}/inputs.html", "Detailed report on the model inputs.") - if inputs_pane: - model_tabs_list.append(("Inputs", inputs_pane)) + model_tabs_list.append(("Inputs", inputs_pane)) # N2 n2_pane = create_report_frame("html", f"{reports_dir}/n2.html", ''' - The N2 diagram, sometimes referred to as an eXtended Design Structure Matrix (XDSM), is a - powerful tool for understanding your model in OpenMDAO. It is an N-squared diagram in the - shape of a matrix representing functional or physical interfaces between system elements. - It can be used to systematically identify, define, tabulate, design, and analyze functional + The N2 diagram, sometimes referred to as an eXtended Design Structure Matrix (XDSM), is a + powerful tool for understanding your model in OpenMDAO. It is an N-squared diagram in the + shape of a matrix representing functional or physical interfaces between system elements. + It can be used to systematically identify, define, tabulate, design, and analyze functional and physical interfaces.''') - if n2_pane: - model_tabs_list.append(("N2", n2_pane)) + model_tabs_list.append(("N2", n2_pane)) # Trajectory Linkage traj_linkage_report_pane = create_report_frame( "html", f"{reports_dir}/traj_linkage_report.html", ''' - This is a Dymos linkage report in a customized N2 diagram. It provides a report detailing how phases + This is a Dymos linkage report in a customized N2 diagram. It provides a report detailing how phases are linked together via constraint or connection. The diagram clearly shows how mission phases are linked. It can be used to identify errant linkages between fixed quantities. ''' ) - if traj_linkage_report_pane: - model_tabs_list.append(("Trajectory Linkage", traj_linkage_report_pane)) + model_tabs_list.append(("Trajectory Linkage", traj_linkage_report_pane)) ####### Optimization Tab ####### optimization_tabs_list = [] @@ -629,20 +655,17 @@ def dashboard(script_name, problem_recorder, driver_recorder, port): # Driver scaling driver_scaling_report_pane = create_report_frame( "html", f"{reports_dir}/driver_scaling_report.html", ''' - This report is a summary of driver scaling information. After all design variables, objectives, and constraints - are declared and the problem has been set up, this report presents all the design variables and constraints - in all phases as well as the objectives. It also shows Jacobian information showing responses with respect to + This report is a summary of driver scaling information. After all design variables, objectives, and constraints + are declared and the problem has been set up, this report presents all the design variables and constraints + in all phases as well as the objectives. It also shows Jacobian information showing responses with respect to design variables (DV). ''' ) - if driver_scaling_report_pane: - optimization_tabs_list.append( - ("Driver Scaling", driver_scaling_report_pane) - ) + model_tabs_list.append(("Driver Scaling", driver_scaling_report_pane)) # Desvars, cons, opt interactive plot if driver_recorder: - if os.path.exists(driver_recorder): + if os.path.isfile(driver_recorder): df = convert_case_recorder_file_to_df(f"{driver_recorder}") if df is not None: variables = pn.widgets.CheckBoxGroup( @@ -656,7 +679,7 @@ def dashboard(script_name, problem_recorder, driver_recorder, port): y=variables, responsive=True, min_height=400, - color=list(Category10[10]), + color=list(bp.Category10[10]), yformatter="%.0f", title="Model Optimization using OpenMDAO", ) @@ -677,7 +700,7 @@ def dashboard(script_name, problem_recorder, driver_recorder, port): ) else: optimization_plot_pane = pn.pane.Markdown( - f"# Recorder file '{driver_recorder}' not found.") + f"# Recorder file containing optimization history,'{driver_recorder}', not found.") optimization_plot_pane_with_doc = pn.Column( pn.pane.HTML(f"

Plot of design variables, constraints, and objectives.

", @@ -689,55 +712,53 @@ def dashboard(script_name, problem_recorder, driver_recorder, port): ) # IPOPT report - ipopt_pane = create_report_frame("text", f"{reports_dir}/IPOPT.out", ''' - This report is generated by the IPOPT optimizer. - ''') - if ipopt_pane: + if os.path.isfile(f"{reports_dir}/IPOPT.out"): + ipopt_pane = create_report_frame("text", f"{reports_dir}/IPOPT.out", ''' + This report is generated by the IPOPT optimizer. + ''') optimization_tabs_list.append(("IPOPT Output", ipopt_pane)) # Optimization report opt_report_pane = create_report_frame("html", f"{reports_dir}/opt_report.html", ''' - This report is an OpenMDAO optimization report. All values are in unscaled, physical units. - On the top is a summary of the optimization, followed by the objective, design variables, constraints, + This report is an OpenMDAO optimization report. All values are in unscaled, physical units. + On the top is a summary of the optimization, followed by the objective, design variables, constraints, and optimizer settings. This report is important when dissecting optimal results produced by Aviary.''') - if opt_report_pane: - optimization_tabs_list.append(("Summary", opt_report_pane)) + optimization_tabs_list.append(("Summary", opt_report_pane)) # PyOpt report - pyopt_solution_pane = create_report_frame( - "text", f"{reports_dir}/pyopt_solution.txt", ''' - This report is generated by the pyOptSparse optimizer. - ''' - ) - if pyopt_solution_pane: + if os.path.isfile(f"{reports_dir}/pyopt_solution.out"): + pyopt_solution_pane = create_report_frame( + "text", f"{reports_dir}/pyopt_solution.txt", ''' + This report is generated by the pyOptSparse optimizer. + ''' + ) optimization_tabs_list.append(("PyOpt Solution", pyopt_solution_pane)) # SNOPT report - snopt_pane = create_report_frame("text", f"{reports_dir}/SNOPT_print.out", ''' - This report is generated by the SNOPT optimizer. - ''') - if snopt_pane: + if os.path.isfile(f"{reports_dir}/SNOPT_print.out"): + snopt_pane = create_report_frame("text", f"{reports_dir}/SNOPT_print.out", ''' + This report is generated by the SNOPT optimizer. + ''') optimization_tabs_list.append(("SNOPT Output", snopt_pane)) # SNOPT summary - snopt_summary_pane = create_report_frame("text", f"{reports_dir}/SNOPT_summary.out", ''' - This is a report generated by the SNOPT optimizer that summarizes the optimization results.''') - if snopt_summary_pane: + if os.path.isfile(f"{reports_dir}/SNOPT_summary.out"): + snopt_summary_pane = create_report_frame("text", f"{reports_dir}/SNOPT_summary.out", ''' + This is a report generated by the SNOPT optimizer that summarizes the optimization results.''') optimization_tabs_list.append(("SNOPT Summary", snopt_summary_pane)) # Coloring report coloring_report_pane = create_report_frame( "html", f"{reports_dir}/total_coloring.html", "The report shows metadata associated with the creation of the coloring." ) - if coloring_report_pane: - optimization_tabs_list.append(("Total Coloring", coloring_report_pane)) + optimization_tabs_list.append(("Total Coloring", coloring_report_pane)) ####### Results Tab ####### results_tabs_list = [] # Aircraft 3d model display if problem_recorder: - if os.path.exists(problem_recorder): + if os.path.isfile(problem_recorder): try: create_aircraft_3d_file( problem_recorder, reports_dir, f"{reports_dir}/aircraft_3d.html" @@ -746,21 +767,15 @@ def dashboard(script_name, problem_recorder, driver_recorder, port): "html", f"{reports_dir}/aircraft_3d.html", "3D model view of designed aircraft." ) - if aircraft_3d_pane: - results_tabs_list.append(("Aircraft 3d model", aircraft_3d_pane)) except Exception as e: - issue_warning( - f'Unable to create aircraft 3D model display due to error {e}' - ) - error_pane = create_report_frame( + aircraft_3d_pane = create_report_frame( "simple_message", f"Unable to create aircraft 3D model display due to error: {e}", - "Error" + "3D model view of designed aircraft." ) - if error_pane: - results_tabs_list.append(("Aircraft 3d model", error_pane)) + results_tabs_list.append(("Aircraft 3d model", aircraft_3d_pane)) # Make the Aviary variables table pane - if os.path.exists(problem_recorder): + if os.path.isfile(problem_recorder): # Make dir reports/script_name/aviary_vars if needed aviary_vars_dir = pathlib.Path(f"reports/{script_name}/aviary_vars") @@ -798,16 +813,12 @@ def dashboard(script_name, problem_recorder, driver_recorder, port): # Mission Summary mission_summary_pane = create_report_frame( "markdown", f"{reports_dir}/mission_summary.md", "A report of mission results from an Aviary problem") - if mission_summary_pane: - results_tabs_list.append(("Mission Summary", mission_summary_pane)) + results_tabs_list.append(("Mission Summary", mission_summary_pane)) # Run status pane status_pane = create_table_pane_from_json(f"{reports_dir}/status.json") - if status_pane: - results_tabs_list.append(("Run status pane", status_pane)) - run_status_pane_tab_number = len(results_tabs_list) - 1 - else: - run_status_pane_tab_number = None + results_tabs_list.append(("Run status pane", status_pane)) + run_status_pane_tab_number = len(results_tabs_list) - 1 # Timeseries Mission Output Report mission_timeseries_pane = create_csv_frame( @@ -816,26 +827,175 @@ def dashboard(script_name, problem_recorder, driver_recorder, port): Any value that is included in the timeseries data is included in this report. This data is useful for post-processing, especially those used for acoustic analysis. ''') - if mission_timeseries_pane: - results_tabs_list.append( - ("Timeseries Mission Output", mission_timeseries_pane) - ) + results_tabs_list.append( + ("Timeseries Mission Output", mission_timeseries_pane) + ) # Trajectory results traj_results_report_pane = create_report_frame( "html", f"{reports_dir}/traj_results_report.html", ''' - This is one of the most important reports produced by Aviary. It will help you visualize and + This is one of the most important reports produced by Aviary. It will help you visualize and understand the optimal trajectory produced by Aviary. - Users should play with it and try to grasp all possible features. - This report contains timeseries and phase parameters in different tabs. - On the timeseries tab, users can select which phases to view. - Other features include hovering the mouse over the solution points to see solution value and + Users should play with it and try to grasp all possible features. + This report contains timeseries and phase parameters in different tabs. + On the timeseries tab, users can select which phases to view. + Other features include hovering the mouse over the solution points to see solution value and zooming into a particular region for details, etc. ''' ) - if traj_results_report_pane: + results_tabs_list.append( + ("Trajectory Results", traj_results_report_pane) + ) + + # Interactive XY plot of mission variables + if problem_recorder: + if os.path.exists(problem_recorder): + cr = om.CaseReader(problem_recorder) + + # determine what trajectories there are + traj_nodes = [n for n in _meta_tree_subsys_iter( + cr.problem_metadata['tree'], cls='dymos.trajectory.trajectory:Trajectory')] + + if len(traj_nodes) == 0: + raise ValueError("No trajectories available in case recorder file for use " + "in generating interactive XY plot of mission variables") + traj_name = traj_nodes[0]["name"] + if len(traj_nodes) > 1: + issue_warning("More than one trajectory found in problem case recorder file. Only using " + f'the first one, "{traj_name}", for the interactive XY plot of mission variables') + case = cr.get_case("final") + outputs = case.list_outputs(out_stream=None, units=True) + + # data_by_varname_and_phase = defaultdict(dict) + data_by_varname_and_phase = defaultdict(lambda: defaultdict(list)) + + # Find the "largest" unit used for any timeseries output across all phases + units_by_varname = {} + phases = set() + varnames = set() + # pattern used to parse out the phase names and variable names + pattern = fr"{traj_name}\.phases\.([a-zA-Z0-9_]+)\.timeseries\.timeseries_comp\.([a-zA-Z0-9_]+)" + for varname, meta in outputs: + match = re.match(pattern, varname) + if match: + phase, name = match.group(1), match.group(2) + phases.add(phase) + varnames.add(name) + if name not in units_by_varname: + units_by_varname[name] = meta['units'] + else: + _, new_conv_factor = conversion_to_base_units(meta['units']) + _, old_conv_factor = conversion_to_base_units( + units_by_varname[name]) + if new_conv_factor < old_conv_factor: + units_by_varname[name] = meta['units'] + + # Now get the values using those units + for varname, meta in outputs: + match = re.match(pattern, varname) + if match: + phase, name = match.group(1), match.group(2) + val = case.get_val(varname, units=units_by_varname[name]) + data_by_varname_and_phase[name][phase] = val + + # determine the initial variables used for X and Y + varname_options = list(sorted(varnames, key=str.casefold)) + if "distance" in varname_options: + x_varname_default = "distance" + elif "time" in varname_options: + x_varname_default = "time" + else: + x_varname_default = varname_options[0] + + if "altitude" in varname_options: + y_varname_default = "altitude" + else: + y_varname_default = varname_options[-1] + + # need to create ColumnDataSource for each phase + sources = {} + for phase in phases: + x, y = _get_interactive_plot_sources(data_by_varname_and_phase, + x_varname_default, y_varname_default, phase) + sources[phase] = ColumnDataSource(data=dict( + x=x, + y=y)) + + # Create the figure + p = figure( + width=800, height=400, + tools='pan,box_zoom,xwheel_zoom,hover,undo,reset,save', + tooltips=[ + ('x', '@x'), + ('y', '@y'), + ], + ) + + colors = bp.d3['Category20'][20][0::2] + bp.d3['Category20'][20][1::2] + legend_data = [] + phases = sorted(phases, key=str.casefold) + for i, phase in enumerate(phases): + color = colors[i % 20] + scatter_plot = p.scatter('x', 'y', source=sources[phase], + color=color, + size=5, + ) + line_plot = p.line('x', 'y', source=sources[phase], + color=color, + line_width=1, + ) + legend_data.append((phase, [scatter_plot, line_plot])) + + # Make the Legend + legend = Legend(items=legend_data, location='center', + label_text_font_size='8pt') + # so users can click on the dot in the legend to turn off/on that phase in the plot + legend.click_policy = "hide" + p.add_layout(legend, 'right') + + # Create dropdown menus for X and Y axis selection + x_select = pn.widgets.Select( + name="X-Axis", value=x_varname_default, options=varname_options) + y_select = pn.widgets.Select( + name="Y-Axis", value=y_varname_default, options=varname_options) + + # Callback function to update the plot + @pn.depends(x_select, y_select) + def update_plot(x_varname, y_varname): + for phase in phases: + x = data_by_varname_and_phase[x_varname][phase] + y = data_by_varname_and_phase[y_varname][phase] + x, y = _get_interactive_plot_sources(data_by_varname_and_phase, + x_varname, y_varname, phase) + sources[phase].data = dict(x=x, y=y) + + p.xaxis.axis_label = f'{x_varname} ({units_by_varname[x_varname]})' + p.yaxis.axis_label = f'{y_varname} ({units_by_varname[y_varname]})' + + p.hover.tooltips = [ + (x_varname, "@x"), + (y_varname, "@y") + ] + return p + + # Create the dashboard pane for this plot + interactive_mission_var_plot_pane = pn.Column( + pn.pane.Markdown( + f"# Interactive Mission Variable Plot for Trajectory, {traj_name}"), + pn.Row(x_select, y_select), + pn.Row(pn.HSpacer(), update_plot, pn.HSpacer()) + ) + else: + interactive_mission_var_plot_pane = pn.pane.Markdown( + f"# Recorder file '{problem_recorder}' not found.") + + interactive_mission_var_plot_pane_with_doc = pn.Column( + pn.pane.HTML(f"

Plot of mission variables allowing user to select X and Y plot values.

", + styles={'text-align': documentation_text_align}), + interactive_mission_var_plot_pane + ) results_tabs_list.append( - ("Trajectory Results", traj_results_report_pane) + ("Interactive Mission Variable Plot", interactive_mission_var_plot_pane_with_doc) ) ####### Subsystems Tab ####### @@ -844,7 +1004,7 @@ def dashboard(script_name, problem_recorder, driver_recorder, port): # Look through subsystems directory for markdown files # The subsystems report tab shows selected results for every major subsystem in the Aviary problem - for md_file in sorted(Path(f"{reports_dir}subsystems").glob("*.md"), key=str): + for md_file in sorted(pathlib.Path(f"{reports_dir}subsystems").glob("*.md"), key=str): subsystems_pane = create_report_frame("markdown", str( md_file), f''' @@ -919,6 +1079,10 @@ def save_dashboard(event): show = True threaded = False + # override `show` without changing `threaded` + if run_in_background: + show = False + assets_dir = pathlib.Path( importlib.util.find_spec("aviary").origin ).parent.joinpath("visualization/assets/")