From 60881efc74a3636f4b1c4c7746b8591e86c3f7df Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Fri, 29 Dec 2023 15:21:16 -0500 Subject: [PATCH 01/21] Small formatting improvements --- .../docs/user_guide/outputs_and_how_to_read_them.md | 2 +- aviary/interface/utils/markdown_utils.py | 11 +++++------ aviary/subsystems/geometry/geometry_builder.py | 6 +++--- aviary/subsystems/mass/mass_builder.py | 4 ++-- aviary/subsystems/propulsion/propulsion_builder.py | 4 ++-- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/aviary/docs/user_guide/outputs_and_how_to_read_them.md b/aviary/docs/user_guide/outputs_and_how_to_read_them.md index 87672db50..41c56cb0e 100644 --- a/aviary/docs/user_guide/outputs_and_how_to_read_them.md +++ b/aviary/docs/user_guide/outputs_and_how_to_read_them.md @@ -21,7 +21,7 @@ The dashboard assumes these locations for the various reports that are embedded | Optimization | SNOPT Output (similarly for other optimizers) | ./reports/*name_of_run_script*/SNOPT_print.out | | Optimization | Desvars, cons, opt plot | Derived from Case Recorder file specified by `driver_recorder` command option | | Results | Trajectory Results Report | ./reports/*name_of_run_script*/traj_results_report.html | -| Results | Aviary Variables | Derived from Case Recorder file specified by `problem_recorder` command option | +| Results | Subsystem Results | ./reports/subsystems/*name_of_subsystem.md (or .html)* | As an example of the workflow for the dashboard, assume that the user has run an Aviary script, `test_full_mission_solved_level3`, 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`.) diff --git a/aviary/interface/utils/markdown_utils.py b/aviary/interface/utils/markdown_utils.py index 4928d617a..8c44b9fad 100644 --- a/aviary/interface/utils/markdown_utils.py +++ b/aviary/interface/utils/markdown_utils.py @@ -1,9 +1,9 @@ import numpy as np from math import floor, log10 -# TODO openMDAO has generate_table() that can eventually replace this +# TODO openMDAO has generate_table() that might be able to replace this -# TODO this might have other use cases, move to utils if so +# TODO rounding might have other use cases, move to utils if so def round_it(x, sig=None): @@ -57,8 +57,7 @@ def write_markdown_variable_table(open_file, problem, outputs, metadata): round_it(val) if not units: units = 'unknown' - summary_line = f'| {var_name} | {val} |' - if units != 'unitless': - summary_line = summary_line + f' {units}' - summary_line = summary_line + ' |\n' + if units == 'unitless': + units = '-' + summary_line = f'| {var_name} | {val} | {units} |\n' open_file.write(summary_line) diff --git a/aviary/subsystems/geometry/geometry_builder.py b/aviary/subsystems/geometry/geometry_builder.py index 9a9520816..bf4f536ab 100644 --- a/aviary/subsystems/geometry/geometry_builder.py +++ b/aviary/subsystems/geometry/geometry_builder.py @@ -98,10 +98,10 @@ def report(self, prob, reports_folder, **kwargs): Aircraft.Fuselage.AVG_DIAMETER] with open(filepath, mode='w') as f: - method = self.code_origin + ' METHOD' + method = self.code_origin + ' method' if self.use_both_geometries: - method = ('FLOPS AND GASP METHODS') - f.write(f'# GEOMETRY: {method}\n') + method = ('FLOPS and GASP methods') + f.write(f'# Geometry: {method}\n') f.write('## Wing') write_markdown_variable_table(f, prob, wing_outputs, self.meta_data) f.write('\n## Empennage\n') diff --git a/aviary/subsystems/mass/mass_builder.py b/aviary/subsystems/mass/mass_builder.py index f491c7231..1686a71f5 100644 --- a/aviary/subsystems/mass/mass_builder.py +++ b/aviary/subsystems/mass/mass_builder.py @@ -114,6 +114,6 @@ def report(self, prob, reports_folder, **kwargs): ] with open(filepath, mode='w') as f: - method = self.code_origin + ' ESTIMATING RELATIONS' - f.write(f'# MASS ESTIMATION: {method}') + method = self.code_origin + '-derived relations' + f.write(f'# Mass estimation: {method}') write_markdown_variable_table(f, prob, outputs, self.meta_data) diff --git a/aviary/subsystems/propulsion/propulsion_builder.py b/aviary/subsystems/propulsion/propulsion_builder.py index d59af880a..2da790afd 100644 --- a/aviary/subsystems/propulsion/propulsion_builder.py +++ b/aviary/subsystems/propulsion/propulsion_builder.py @@ -66,9 +66,9 @@ def report(self, prob, reports_folder, **kwargs): Aircraft.Engine.SCALED_SLS_THRUST] with open(filepath, mode='w') as f: - f.write('# PROPULSION') + f.write('# Propulsion') write_markdown_variable_table(f, prob, propulsion_outputs, self.meta_data) - f.write('\n## ENGINES') + f.write('\n## Engines') for engine in prob.aviary_inputs.get_val('engine_models'): f.write(f'\n### {engine.name}') write_markdown_variable_table(f, engine, engine_outputs, self.meta_data) From 5ca688ef1cb5ad33d3e009d83c1ef2675c82ac03 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Fri, 29 Dec 2023 16:17:08 -0500 Subject: [PATCH 02/21] WIP mission report --- aviary/interface/reports.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/aviary/interface/reports.py b/aviary/interface/reports.py index 76889b95e..6e0b3ad17 100644 --- a/aviary/interface/reports.py +++ b/aviary/interface/reports.py @@ -1,7 +1,8 @@ from pathlib import Path - from openmdao.utils.reports_system import register_report +from aviary.interface.utils.markdown_utils import write_markdown_variable_table + def register_custom_reports(): """ @@ -17,12 +18,19 @@ def register_custom_reports(): func=subsystem_report, desc='Generates reports for each subsystem builder in the ' 'Aviary Problem', - class_name='Problem', + class_name='AviaryProblem', method='run_model', pre_or_post='post', # **kwargs ) + register_report(name='mission', + func=mission_report, + desc='Generates report for mission results from Aviary problem', + class_name='AviaryProblem', + method='run_model', + pre_or_post='post') + def subsystem_report(prob, **kwargs): """ @@ -42,3 +50,21 @@ def subsystem_report(prob, **kwargs): for subsystem in core_subsystems.values(): subsystem.report(prob, reports_folder, **kwargs) + + +# TODO update with more detailed mission report file +def mission_report(prob, **kwargs): + """ + Creates a basic mission summary report that is place in the reports folder + + Parameters + ---------- + prob : AviaryProblem + The AviaryProblem that will be used to generate this report + """ + reports_folder = Path(prob.get_reports_dir()) + report_file = reports_folder / 'mission_summary.md' + + with open(report_file, mode='w') as f: + f.write('# MISSION SUMMARY') + write_markdown_variable_table(f, prob, {}) From 1bccd7a1796e5bd53722a26d34aa7c5920903d18 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Wed, 3 Jan 2024 11:41:16 -0500 Subject: [PATCH 03/21] updated report hooks --- setup.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/setup.py b/setup.py index dbe61a520..51062f5af 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ from setuptools import find_packages, setup -from pathlib import Path import re @@ -9,9 +8,6 @@ open('aviary/__init__.py').read(), )[0] -with open(Path(__file__).parent / "README.md", encoding="utf-8") as f: - long_description = f.read() - pkgname = "aviary" extras_require = { "test": ["testflo", "pyxdsm", "pre-commit"], @@ -26,9 +22,7 @@ extras_require["all"] = all_packages setup( - name="om-aviary", - long_description=long_description, - long_description_content_type='text/markdown', + name=pkgname, version=__version__, packages=find_packages(), install_requires=[ @@ -59,7 +53,7 @@ 'aviary=aviary.interface.cmd_entry_points:aviary_cmd', ], 'openmdao_report': [ - 'summary=aviary.interface.reports:register_custom_reports', + 'aviary_reports=aviary.interface.reports:register_custom_reports', ] } ) From e275e8d362aa34d261b5a544d41f1d37892f6999 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Thu, 4 Jan 2024 10:08:08 -0500 Subject: [PATCH 04/21] Changes to report setup --- aviary/interface/methods_for_level2.py | 8 ++++++++ aviary/interface/reports.py | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 511819340..e7c03eb4b 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -127,6 +127,14 @@ class AviaryProblem(om.Problem): """ def __init__(self, phase_info, mission_method, mass_method, analysis_scheme=AnalysisScheme.COLLOCATION, **kwargs): + from openmdao.utils.reports_system import _default_reports + + # Modify OpenMDAO's default_reports for this session. + new_reports = ['subsystems', 'mission'] + for report in new_reports: + if report not in _default_reports: + _default_reports.append(report) + super().__init__(**kwargs) self.timestamp = datetime.now() diff --git a/aviary/interface/reports.py b/aviary/interface/reports.py index 6e0b3ad17..51e796fb3 100644 --- a/aviary/interface/reports.py +++ b/aviary/interface/reports.py @@ -18,7 +18,7 @@ def register_custom_reports(): func=subsystem_report, desc='Generates reports for each subsystem builder in the ' 'Aviary Problem', - class_name='AviaryProblem', + class_name='Problem', method='run_model', pre_or_post='post', # **kwargs @@ -27,7 +27,7 @@ def register_custom_reports(): register_report(name='mission', func=mission_report, desc='Generates report for mission results from Aviary problem', - class_name='AviaryProblem', + class_name='Problem', method='run_model', pre_or_post='post') From 43285b07c9449089c75d6aaac8d21d818d47a442 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Thu, 4 Jan 2024 11:10:25 -0500 Subject: [PATCH 05/21] more report fixes --- aviary/interface/methods_for_level2.py | 6 +----- aviary/interface/reports.py | 2 +- .../benchmark_tests/test_full_mission_solved_level3.py | 3 +-- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index e7c03eb4b..b9c23f95f 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -1,9 +1,7 @@ import csv import warnings -from packaging import version import inspect from copy import deepcopy -from pathlib import Path from datetime import datetime import numpy as np @@ -13,7 +11,7 @@ import openmdao.api as om from openmdao.utils.units import convert_units -from openmdao.utils.units import valid_units +from openmdao.utils.reports_system import _default_reports from aviary.constants import GRAV_ENGLISH_LBM, RHO_SEA_LEVEL_ENGLISH from aviary.mission.flops_based.phases.build_landing import Landing @@ -127,8 +125,6 @@ class AviaryProblem(om.Problem): """ def __init__(self, phase_info, mission_method, mass_method, analysis_scheme=AnalysisScheme.COLLOCATION, **kwargs): - from openmdao.utils.reports_system import _default_reports - # Modify OpenMDAO's default_reports for this session. new_reports = ['subsystems', 'mission'] for report in new_reports: diff --git a/aviary/interface/reports.py b/aviary/interface/reports.py index 51e796fb3..30fe4099a 100644 --- a/aviary/interface/reports.py +++ b/aviary/interface/reports.py @@ -67,4 +67,4 @@ def mission_report(prob, **kwargs): with open(report_file, mode='w') as f: f.write('# MISSION SUMMARY') - write_markdown_variable_table(f, prob, {}) + # write_markdown_variable_table(f, prob, {}) diff --git a/aviary/validation_cases/benchmark_tests/test_full_mission_solved_level3.py b/aviary/validation_cases/benchmark_tests/test_full_mission_solved_level3.py index ed0216b50..3f297c02c 100644 --- a/aviary/validation_cases/benchmark_tests/test_full_mission_solved_level3.py +++ b/aviary/validation_cases/benchmark_tests/test_full_mission_solved_level3.py @@ -18,8 +18,7 @@ class ProblemPhaseTestCase(unittest.TestCase): def bench_test_solved_full_mission(self): # Build problem - prob = AviaryProblem(phase_info, mission_method="solved", - mass_method="GASP", reports='subsystems') + prob = AviaryProblem(phase_info, mission_method="solved", mass_method="GASP") input_file = 'models/test_aircraft/aircraft_for_bench_GwGm.csv' prob.load_inputs(input_file) From 259da9eeb26379829b629f2a24747aca22e87e65 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Thu, 4 Jan 2024 17:04:32 -0500 Subject: [PATCH 06/21] subsystem report bugfixes --- aviary/interface/utils/markdown_utils.py | 7 ++ .../subsystems/geometry/geometry_builder.py | 3 +- aviary/subsystems/propulsion/engine_deck.py | 79 +++++++++++++++++++ aviary/subsystems/propulsion/engine_model.py | 21 +++++ .../propulsion/propulsion_builder.py | 12 ++- 5 files changed, 114 insertions(+), 8 deletions(-) diff --git a/aviary/interface/utils/markdown_utils.py b/aviary/interface/utils/markdown_utils.py index 8c44b9fad..2f9c6a353 100644 --- a/aviary/interface/utils/markdown_utils.py +++ b/aviary/interface/utils/markdown_utils.py @@ -8,6 +8,11 @@ def round_it(x, sig=None): # default sig figs to 2 decimal places out + if isinstance(x, str): + try: + x = float(x) + except ValueError: + return x if not sig: sig = len(str(round(x)))+2 if x != 0: @@ -44,6 +49,8 @@ def write_markdown_variable_table(open_file, problem, outputs, metadata): val = problem.aviary_inputs.get_val(var_name, units) else: val, units = problem.aviary_inputs.get_item(var_name) + if (val, units) == (None, None): + raise KeyError except KeyError: val = 'Not Found in Model' units = None diff --git a/aviary/subsystems/geometry/geometry_builder.py b/aviary/subsystems/geometry/geometry_builder.py index bf4f536ab..1e8f062e0 100644 --- a/aviary/subsystems/geometry/geometry_builder.py +++ b/aviary/subsystems/geometry/geometry_builder.py @@ -98,9 +98,10 @@ def report(self, prob, reports_folder, **kwargs): Aircraft.Fuselage.AVG_DIAMETER] with open(filepath, mode='w') as f: - method = self.code_origin + ' method' if self.use_both_geometries: method = ('FLOPS and GASP methods') + else: + method = self.code_origin + ' method' f.write(f'# Geometry: {method}\n') f.write('## Wing') write_markdown_variable_table(f, prob, wing_outputs, self.meta_data) diff --git a/aviary/subsystems/propulsion/engine_deck.py b/aviary/subsystems/propulsion/engine_deck.py index bb5df6da4..64b1416cb 100644 --- a/aviary/subsystems/propulsion/engine_deck.py +++ b/aviary/subsystems/propulsion/engine_deck.py @@ -40,6 +40,7 @@ from aviary.variable_info.variable_meta_data import _MetaData from aviary.variable_info.variables import Aircraft, Dynamic, Mission from aviary.utils.csv_data_file import read_data_file +from aviary.interface.utils.markdown_utils import round_it MACH = EngineModelVariables.MACH @@ -891,6 +892,84 @@ def build_mission(self, num_nodes, aviary_inputs): return engine_group + def report(self, problem, reports_file, **kwargs): + meta_data = kwargs['meta_data'] + + outputs = [Aircraft.Engine.NUM_ENGINES, + Aircraft.Engine.SCALED_SLS_THRUST, + Aircraft.Engine.SCALE_FACTOR] + + # determine which index in problem-level aviary values corresponds to this engine + engine_idx = None + for idx, engine in enumerate(problem.aviary_inputs.get_val('engine_models')): + if engine.name == self.name: + engine_idx = idx + + if engine_idx is None: + with open(reports_file, mode='a') as f: + f.write(f'\n### {self.name}') + f.write(f'\nEngine deck {self.name} not found\n') + return + + # modified version of markdown table util adjusted to handle engine decks + with open(reports_file, mode='a') as f: + f.write(f'\n### {self.name}') + f.write('\n| Variable Name | Value | Units |\n') + f.write('| :- | :- | :- |\n') + for var_name in outputs: + # get default units from metadata + try: + units = meta_data[var_name]['units'] + except KeyError: + units = None + # try to get value from engine + try: + if units: + val = self.get_val(var_name, units) + else: + val, units = self.get_item(var_name) + if (val, units) == (None, None): + raise KeyError + except KeyError: + # get value from problem + try: + if units: + val = problem.get_val(var_name, units) + else: + # TODO find units for variable in problem? + val = problem.get_val(var_name) + units = 'unknown' + # variable not in problem, get from aviary_inputs instead + except KeyError: + try: + if units: + val = problem.aviary_inputs.get_val(var_name, units) + else: + val, units = problem.aviary_inputs.get_item(var_name) + if (val, units) == (None, None): + raise KeyError + except KeyError: + val = 'Not Found in Model' + units = None + else: + val = val[engine_idx] + else: + val = val[engine_idx] + # handle rounding + formatting + if isinstance(val, (np.ndarray, list, tuple)): + val = [round_it(item) for item in val] + # if an interable with a length of 1, remove bracket/paretheses, etc. + if len(val) == 1: + val = val[0] + else: + round_it(val) + if not units: + units = 'unknown' + if units == 'unitless': + units = '-' + summary_line = f'| {var_name} | {val} | {units} |\n' + f.write(summary_line) + def _set_reference_thrust(self): """ Determine maximum sea-level static thrust produced by the engine (unscaled). diff --git a/aviary/subsystems/propulsion/engine_model.py b/aviary/subsystems/propulsion/engine_model.py index 41346de49..28b2a5c82 100644 --- a/aviary/subsystems/propulsion/engine_model.py +++ b/aviary/subsystems/propulsion/engine_model.py @@ -30,6 +30,7 @@ class EngineModel(SubsystemBuilderBase): build_mission build_post_mission get_val + get_item set_val update """ @@ -178,6 +179,26 @@ def get_val(self, key, units='unitless'): """ return self.options.get_val(key, units) + def get_item(self, key, default=(None, None)): + ''' + Return the named value and its associated units. + + Note, this method never raises `KeyError` or `TypeError`. + + Parameters + ---------- + key : str + the name of the item + + default : OptionalValueAndUnits (None, None) + if the item does not exist, return this object + + Returns + ------- + OptionalValueAndUnits + ''' + return self.options.get_item(key, default) + def set_val(self, key, val, units='unitless'): """ Updates desired value in options with specified units. diff --git a/aviary/subsystems/propulsion/propulsion_builder.py b/aviary/subsystems/propulsion/propulsion_builder.py index 2da790afd..2f691c468 100644 --- a/aviary/subsystems/propulsion/propulsion_builder.py +++ b/aviary/subsystems/propulsion/propulsion_builder.py @@ -61,14 +61,12 @@ def report(self, prob, reports_folder, **kwargs): propulsion_outputs = [Aircraft.Propulsion.TOTAL_NUM_ENGINES, Aircraft.Propulsion.TOTAL_SCALED_SLS_THRUST] - engine_outputs = [Aircraft.Engine.NUM_ENGINES, - Aircraft.Engine.SCALE_FACTOR, - Aircraft.Engine.SCALED_SLS_THRUST] - with open(filepath, mode='w') as f: f.write('# Propulsion') write_markdown_variable_table(f, prob, propulsion_outputs, self.meta_data) f.write('\n## Engines') - for engine in prob.aviary_inputs.get_val('engine_models'): - f.write(f'\n### {engine.name}') - write_markdown_variable_table(f, engine, engine_outputs, self.meta_data) + + # each engine can append to this file + kwargs['meta_data'] = self.meta_data + for engine in prob.aviary_inputs.get_val('engine_models'): + engine.report(prob, filepath, **kwargs) From 54bcdfec1404913a867d564ec611a97dfda3b23c Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Mon, 8 Jan 2024 11:08:05 -0500 Subject: [PATCH 07/21] Sign "error" fix for mission report --- aviary/interface/reports.py | 53 +++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/aviary/interface/reports.py b/aviary/interface/reports.py index 30fe4099a..248149635 100644 --- a/aviary/interface/reports.py +++ b/aviary/interface/reports.py @@ -1,7 +1,10 @@ from pathlib import Path from openmdao.utils.reports_system import register_report +import numpy as np + from aviary.interface.utils.markdown_utils import write_markdown_variable_table +from aviary.utils.named_values import NamedValues def register_custom_reports(): @@ -62,9 +65,55 @@ def mission_report(prob, **kwargs): prob : AviaryProblem The AviaryProblem that will be used to generate this report """ + def _get_phase_value(traj, phase, var_name, units): + try: + vals = prob.get_val( + f"{traj}.{phase}.states:{var_name}", + units=units, + indices=[-1, 0], + ) + except KeyError: + try: + vals = prob.get_val( + f"{traj}.{phase}.timeseries.{var_name}", + units=units, + indices=[-1, 0], + ) + except KeyError: + try: + vals = prob.get_val( + f"{traj}.{phase}.{var_name}", + units=units, + indices=[-1, 0], + ) + except KeyError: + return None + + diff = vals[-1]-vals[0] + if isinstance(diff, np.ndarray): + diff = diff[0] + + return diff + reports_folder = Path(prob.get_reports_dir()) report_file = reports_folder / 'mission_summary.md' with open(report_file, mode='w') as f: - f.write('# MISSION SUMMARY') - # write_markdown_variable_table(f, prob, {}) + f.write('# MISSION SUMMARY\n') + for phase in prob.phase_info: + f.write(f'## {phase}') + # TODO for traj in trajectories, currently assuming single one named "traj" + fuel_burn = _get_phase_value('traj', phase, 'mass', 'lbm') + time = _get_phase_value('traj', phase, 't', 'min') + range = _get_phase_value('traj', phase, 'range', 'nmi') + if range is None: + range = _get_phase_value('traj', phase, 'distance', 'nmi') + outputs = NamedValues() + outputs.set_val('Fuel Burn', fuel_burn, 'lbm') + # Time, range are negative values from prob + outputs.set_val('Elapsed Time', -time, 'min') + outputs.set_val('Ground Distance', -range, 'nmi') + write_markdown_variable_table(f, outputs, ['Fuel Burn', 'Elapsed Time', 'Ground Distance'], + {'Fuel Burn': {'units': 'lbm'}, + 'Elapsed Time': {'units': 'min'}, + 'Ground Distance': {'units': 'nmi'}}) From 5efcd56fdedeeaebb34b40f925b6c4a821114975 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Thu, 11 Jan 2024 11:59:39 -0500 Subject: [PATCH 08/21] Improved mission reports --- aviary/interface/reports.py | 135 ++++++++++++------ aviary/interface/utils/markdown_utils.py | 2 +- .../test/test_solved_aero_group.py | 74 ++++++++++ 3 files changed, 168 insertions(+), 43 deletions(-) diff --git a/aviary/interface/reports.py b/aviary/interface/reports.py index 248149635..143e31eab 100644 --- a/aviary/interface/reports.py +++ b/aviary/interface/reports.py @@ -38,12 +38,12 @@ def register_custom_reports(): def subsystem_report(prob, **kwargs): """ Loops through all subsystem builders in the AviaryProblem calls their write_report - method. All generated report files are placed in the ./reports/subsystem_reports folder + method. All generated report files are placed in the "reports/subsystem_reports" folder Parameters ---------- prob : AviaryProblem - The AviaryProblem that will be used to generate this report + The AviaryProblem used to generate this report """ reports_folder = Path(prob.get_reports_dir() / 'subsystems') reports_folder.mkdir(exist_ok=True) @@ -58,62 +58,113 @@ def subsystem_report(prob, **kwargs): # TODO update with more detailed mission report file def mission_report(prob, **kwargs): """ - Creates a basic mission summary report that is place in the reports folder + Creates a basic mission summary report that is place in the "reports" folder Parameters ---------- prob : AviaryProblem - The AviaryProblem that will be used to generate this report + The AviaryProblem used to generate this report """ - def _get_phase_value(traj, phase, var_name, units): + def _get_phase_value(traj, phase, var_name, units, indices=None): try: - vals = prob.get_val( - f"{traj}.{phase}.states:{var_name}", - units=units, - indices=[-1, 0], - ) + vals = prob.get_val(f"{traj}.{phase}.states:{var_name}", + units=units, + indices=indices,) except KeyError: try: - vals = prob.get_val( - f"{traj}.{phase}.timeseries.{var_name}", - units=units, - indices=[-1, 0], - ) + vals = prob.get_val(f"{traj}.{phase}.timeseries.{var_name}", + units=units, + indices=indices,) except KeyError: try: - vals = prob.get_val( - f"{traj}.{phase}.{var_name}", - units=units, - indices=[-1, 0], - ) + vals = prob.get_val(f"{traj}.{phase}.timeseries.states:{var_name}", + units=units, + indices=indices,) except KeyError: - return None - - diff = vals[-1]-vals[0] - if isinstance(diff, np.ndarray): - diff = diff[0] - - return diff + try: + vals = prob.get_val(f"{traj}.{phase}.{var_name}", + units=units, + indices=indices,) + # 2DOF breguet range cruise sneakily uses time integration to track mass + except TypeError: + vals = prob.get_val(f"{traj}.{phase}.timeseries.time", + units=units, + indices=indices,) + except KeyError: + vals = None + + return vals + + def _get_phase_diff(traj, phase, var_name, units, indices=[0, -1]): + vals = _get_phase_value(traj, phase, var_name, units, indices) + + if vals is not None: + diff = vals[-1]-vals[0] + if isinstance(diff, np.ndarray): + diff = diff[0] + return diff + else: + return None reports_folder = Path(prob.get_reports_dir()) report_file = reports_folder / 'mission_summary.md' + # read per-phase data from trajectory + data = {} + for idx, phase in enumerate(prob.phase_info): + # TODO for traj in trajectories, currently assuming single one named "traj" + # TODO delta mass and fuel consumption need to be tracked separately + fuel_burn = _get_phase_diff('traj', phase, 'mass', 'lbm', [-1, 0]) + time = _get_phase_diff('traj', phase, 't', 'min') + range = _get_phase_diff('traj', phase, 'range', 'nmi') + # both "distance" and "range" currently exist to track ground distance, logic + # here accounts for that + if range is None: + range = _get_phase_diff('traj', phase, 'distance', 'nmi') + + # get initial values, first in traj + if idx == 0: + initial_mass = _get_phase_value('traj', phase, 'mass', 'lbm', 0)[0] + initial_time = _get_phase_value('traj', phase, 't', 'min', 0) + initial_range = _get_phase_value('traj', phase, 'range', 'nmi', 0) + if initial_range is None: + initial_range = _get_phase_value('traj', phase, 'distance', 'nmi', 0) + initial_range = initial_range[0] + + outputs = NamedValues() + # Fuel burn is negative of delta mass + outputs.set_val('Fuel Burn', fuel_burn, 'lbm') + outputs.set_val('Elapsed Time', time, 'min') + outputs.set_val('Ground Distance', range, 'nmi') + data[phase] = outputs + + # get final values, last in traj + final_mass = _get_phase_value('traj', phase, 'mass', 'lbm', [0])[0] + final_time = _get_phase_value('traj', phase, 't', 'min', [0]) + final_range = _get_phase_value('traj', phase, 'range', 'nmi', [0]) + if final_range is None: + final_range = _get_phase_value('traj', phase, 'distance', 'nmi', [0]) + final_range = final_range[0] + + totals = NamedValues() + totals.set_val('Total Fuel Burn', initial_mass - final_mass, 'lbm') + totals.set_val('Total Time', final_time - initial_time, 'min') + totals.set_val('Total Ground Distance', final_range - initial_range, 'nmi') + with open(report_file, mode='w') as f: - f.write('# MISSION SUMMARY\n') - for phase in prob.phase_info: - f.write(f'## {phase}') - # TODO for traj in trajectories, currently assuming single one named "traj" - fuel_burn = _get_phase_value('traj', phase, 'mass', 'lbm') - time = _get_phase_value('traj', phase, 't', 'min') - range = _get_phase_value('traj', phase, 'range', 'nmi') - if range is None: - range = _get_phase_value('traj', phase, 'distance', 'nmi') - outputs = NamedValues() - outputs.set_val('Fuel Burn', fuel_burn, 'lbm') - # Time, range are negative values from prob - outputs.set_val('Elapsed Time', -time, 'min') - outputs.set_val('Ground Distance', -range, 'nmi') - write_markdown_variable_table(f, outputs, ['Fuel Burn', 'Elapsed Time', 'Ground Distance'], + f.write('# MISSION SUMMARY') + write_markdown_variable_table(f, totals, + ['Total Fuel Burn', + 'Total Time', + 'Total Ground Distance'], + {'Total Fuel Burn': {'units': 'lbm'}, + 'Total Time': {'units': 'min'}, + 'Total Ground Distance': {'units': 'nmi'}}) + + f.write('\n# MISSION SEGMENTS') + for phase in data: + f.write(f'\n## {phase}') + write_markdown_variable_table(f, data[phase], ['Fuel Burn', 'Elapsed Time', 'Ground Distance'], {'Fuel Burn': {'units': 'lbm'}, 'Elapsed Time': {'units': 'min'}, 'Ground Distance': {'units': 'nmi'}}) diff --git a/aviary/interface/utils/markdown_utils.py b/aviary/interface/utils/markdown_utils.py index 2f9c6a353..009098174 100644 --- a/aviary/interface/utils/markdown_utils.py +++ b/aviary/interface/utils/markdown_utils.py @@ -61,7 +61,7 @@ def write_markdown_variable_table(open_file, problem, outputs, metadata): if len(val) == 1: val = val[0] else: - round_it(val) + val = round_it(val) if not units: units = 'unknown' if units == 'unitless': 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 59c5dc0b7..89064de19 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 @@ -115,6 +115,80 @@ def test_solved_aero_pass_polar(self): assert_near_equal(CL_pass, CL_base, 1e-6) assert_near_equal(CD_pass, CD_base, 1e-6) + def test_solved_aero_pass_polar_unique_abscissa(self): + # Solved Aero with shortened lists of table abscissa. + prob = AviaryProblem( + phase_info, mission_method="FLOPS", mass_method="FLOPS") + + csv_path = pkg_resources.resource_filename( + "aviary", "subsystems/aerodynamics/flops_based/test/data/high_wing_single_aisle.csv") + + prob.load_inputs(csv_path) + prob.add_pre_mission_systems() + prob.add_phases() + prob.add_post_mission_systems() + + prob.link_phases() + + prob.setup() + + prob.set_initial_guesses() + + prob.run_model() + + 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. + + ph_in = deepcopy(phase_info) + + alt = np.array([0., 3000., 6000., 9000., 12000., 15000., 18000., 21000., + 24000., 27000., 30000., 33000., 36000., 38000., 42000.]) + mach = np.array([0., 0.2, 0.4, 0.5, 0.6, 0.7, 0.75, 0.8, 0.85, 0.9]) + alpha = np.array([-2., 0., 2., 4., 6., 8., 10.]) + + polar_builder = FakeDragPolarBuilder(name="aero", altitude=alt, mach=mach, + alpha=alpha) + aero_data = NamedValues() + aero_data.set_val('altitude', alt, 'ft') + aero_data.set_val('mach', mach, 'unitless') + aero_data.set_val('angle_of_attack', alpha, 'deg') + + subsystem_options = {'method': 'solved_alpha', + 'aero_data': aero_data, + 'training_data': True} + ph_in['pre_mission']['external_subsystems'] = [polar_builder] + + ph_in['cruise']['subsystem_options'] = {'core_aerodynamics': subsystem_options} + + prob = AviaryProblem(ph_in, mission_method="FLOPS", mass_method="FLOPS") + + prob.load_inputs(csv_path) + + prob.aviary_inputs.set_val(Aircraft.Design.LIFT_POLAR, + np.zeros_like(CL), units='unitless') + prob.aviary_inputs.set_val(Aircraft.Design.DRAG_POLAR, + np.zeros_like(CD), units='unitless') + + prob.add_pre_mission_systems() + prob.add_phases() + prob.add_post_mission_systems() + + prob.link_phases() + + prob.setup() + + prob.set_initial_guesses() + + prob.run_model() + + CL_pass = prob.get_val("traj.cruise.rhs_all.core_aerodynamics.tabular_aero.CL") + CD_pass = prob.get_val("traj.cruise.rhs_all.core_aerodynamics.tabular_aero.CD") + + assert_near_equal(CL_pass, CL_base, 1e-6) + assert_near_equal(CD_pass, CD_base, 1e-6) + class FakeCalcDragPolar(om.ExplicitComponent): """ From 66c06fa002a2f0c25fe6f87e07b59b1596ae118b Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Thu, 11 Jan 2024 13:08:41 -0500 Subject: [PATCH 09/21] merge fix --- aviary/interface/methods_for_level2.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 959a1730d..eb6e44166 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -16,6 +16,7 @@ import openmdao.api as om from openmdao.utils.units import convert_units from openmdao.utils.units import valid_units +from openmdao.utils.reports_system import _default_reports from aviary.constants import GRAV_ENGLISH_LBM, RHO_SEA_LEVEL_ENGLISH from aviary.mission.flops_based.phases.build_landing import Landing From d7f1617686f6f1944876696742a8d44ae567b0e0 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Thu, 11 Jan 2024 14:49:55 -0500 Subject: [PATCH 10/21] builder fixes --- aviary/subsystems/geometry/geometry_builder.py | 2 +- aviary/subsystems/mass/mass_builder.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aviary/subsystems/geometry/geometry_builder.py b/aviary/subsystems/geometry/geometry_builder.py index fb916db53..ea8aa4eaa 100644 --- a/aviary/subsystems/geometry/geometry_builder.py +++ b/aviary/subsystems/geometry/geometry_builder.py @@ -105,7 +105,7 @@ def report(self, prob, reports_folder, **kwargs): if self.use_both_geometries: method = ('FLOPS and GASP methods') else: - method = self.code_origin + ' method' + method = self.code_origin.value + ' method' f.write(f'# Geometry: {method}\n') f.write('## Wing') write_markdown_variable_table(f, prob, wing_outputs, self.meta_data) diff --git a/aviary/subsystems/mass/mass_builder.py b/aviary/subsystems/mass/mass_builder.py index 7627655e7..e99e0ac3d 100644 --- a/aviary/subsystems/mass/mass_builder.py +++ b/aviary/subsystems/mass/mass_builder.py @@ -119,6 +119,6 @@ def report(self, prob, reports_folder, **kwargs): ] with open(filepath, mode='w') as f: - method = self.code_origin + '-derived relations' + method = self.code_origin.value + '-derived relations' f.write(f'# Mass estimation: {method}') write_markdown_variable_table(f, prob, outputs, self.meta_data) From 0656c5d32947c730420d8dead287aa6b0b3a3853 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Thu, 1 Feb 2024 16:40:29 -0500 Subject: [PATCH 11/21] Docs update for mission report --- aviary/docs/user_guide/outputs_and_how_to_read_them.md | 1 + 1 file changed, 1 insertion(+) diff --git a/aviary/docs/user_guide/outputs_and_how_to_read_them.md b/aviary/docs/user_guide/outputs_and_how_to_read_them.md index 41c56cb0e..7bc0d67ab 100644 --- a/aviary/docs/user_guide/outputs_and_how_to_read_them.md +++ b/aviary/docs/user_guide/outputs_and_how_to_read_them.md @@ -22,6 +22,7 @@ The dashboard assumes these locations for the various reports that are embedded | Optimization | Desvars, cons, opt plot | Derived from Case Recorder file specified by `driver_recorder` command option | | Results | Trajectory Results Report | ./reports/*name_of_run_script*/traj_results_report.html | | Results | Subsystem Results | ./reports/subsystems/*name_of_subsystem.md (or .html)* | +| Results | Mission Results | ./reports/subsystems/mission_summary.md | As an example of the workflow for the dashboard, assume that the user has run an Aviary script, `test_full_mission_solved_level3`, 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`.) From d96d3af62fba0c79688d6fa5019cd29a66244b33 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Mon, 5 Feb 2024 10:47:27 -0500 Subject: [PATCH 12/21] fixed test --- .../flops_based/test/test_solved_aero_group.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 6189f7572..518b881a5 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 @@ -4,6 +4,7 @@ a file. """ import unittest +import pkg_resources import numpy as np import openmdao.api as om from openmdao.utils.assert_utils import assert_near_equal @@ -189,13 +190,12 @@ def test_solved_aero_pass_polar_unique_abscissa(self): def test_solved_aero_pass_polar_unique_abscissa(self): # Solved Aero with shortened lists of table abscissa. - prob = AviaryProblem( - phase_info, mission_method="FLOPS", mass_method="FLOPS") + prob = AviaryProblem() csv_path = pkg_resources.resource_filename( "aviary", "subsystems/aerodynamics/flops_based/test/data/high_wing_single_aisle.csv") - prob.load_inputs(csv_path) + prob.load_inputs(csv_path, phase_info) prob.add_pre_mission_systems() prob.add_phases() prob.add_post_mission_systems() @@ -234,9 +234,9 @@ def test_solved_aero_pass_polar_unique_abscissa(self): ph_in['cruise']['subsystem_options'] = {'core_aerodynamics': subsystem_options} - prob = AviaryProblem(ph_in, mission_method="FLOPS", mass_method="FLOPS") + prob = AviaryProblem() - prob.load_inputs(csv_path) + prob.load_inputs(csv_path, ph_in) prob.aviary_inputs.set_val(Aircraft.Design.LIFT_POLAR, np.zeros_like(CL), units='unitless') From e5936570cf838ad90da3a6cddc19f5834958ca55 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Mon, 5 Feb 2024 17:34:23 -0500 Subject: [PATCH 13/21] bugfixes --- aviary/interface/methods_for_level2.py | 4 ++-- aviary/interface/reports.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 0884401c9..23fdac964 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -182,7 +182,7 @@ def load_inputs(self, aviary_inputs, phase_info=None, engine_builder=None): self.aviary_inputs = aviary_inputs self.initial_guesses = initial_guesses - if mission_method is TWO_DEGREES_OF_FREEDOM: + if mission_method is TWO_DEGREES_OF_FREEDOM or mission_method is SOLVED: aviary_inputs.set_val(Mission.Summary.CRUISE_MASS_FINAL, val=self.initial_guesses['cruise_mass_final'], units='lbm') aviary_inputs.set_val(Mission.Summary.GROSS_MASS, @@ -1602,7 +1602,7 @@ def add_design_variables(self): self.model.add_constraint("h_fit.h_init_flaps", equals=400.0, units="ft", ref=400.0) - self.problem_type = self.aviary_inputs.get_val('problem_type') + # self.problem_type = self.aviary_inputs.get_val('problem_type') # vehicle sizing problem # size the vehicle (via design GTOW) to meet a target range using all fuel capacity diff --git a/aviary/interface/reports.py b/aviary/interface/reports.py index 143e31eab..9ef1a6749 100644 --- a/aviary/interface/reports.py +++ b/aviary/interface/reports.py @@ -139,11 +139,11 @@ def _get_phase_diff(traj, phase, var_name, units, indices=[0, -1]): data[phase] = outputs # get final values, last in traj - final_mass = _get_phase_value('traj', phase, 'mass', 'lbm', [0])[0] - final_time = _get_phase_value('traj', phase, 't', 'min', [0]) - final_range = _get_phase_value('traj', phase, 'range', 'nmi', [0]) + final_mass = _get_phase_value('traj', phase, 'mass', 'lbm', -1)[0] + final_time = _get_phase_value('traj', phase, 't', 'min', -1)[0] + final_range = _get_phase_value('traj', phase, 'range', 'nmi', -1) if final_range is None: - final_range = _get_phase_value('traj', phase, 'distance', 'nmi', [0]) + final_range = _get_phase_value('traj', phase, 'distance', 'nmi', -1) final_range = final_range[0] totals = NamedValues() From 5f90a8d0a6e69d23e538541cef385286db94a2ec Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Mon, 5 Feb 2024 18:24:19 -0500 Subject: [PATCH 14/21] Fixed duplicate aero test --- aviary/interface/reports.py | 2 +- .../test/test_solved_aero_group.py | 73 ------------------- 2 files changed, 1 insertion(+), 74 deletions(-) diff --git a/aviary/interface/reports.py b/aviary/interface/reports.py index 9ef1a6749..f413e0dc6 100644 --- a/aviary/interface/reports.py +++ b/aviary/interface/reports.py @@ -85,7 +85,7 @@ def _get_phase_value(traj, phase, var_name, units, indices=None): vals = prob.get_val(f"{traj}.{phase}.{var_name}", units=units, indices=indices,) - # 2DOF breguet range cruise sneakily uses time integration to track mass + # 2DOF breguet range cruise uses time integration to track mass except TypeError: vals = prob.get_val(f"{traj}.{phase}.timeseries.time", units=units, 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 518b881a5..0e55ea6ea 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 @@ -188,79 +188,6 @@ def test_solved_aero_pass_polar_unique_abscissa(self): assert_near_equal(CL_pass, CL_base, 1e-6) assert_near_equal(CD_pass, CD_base, 1e-6) - def test_solved_aero_pass_polar_unique_abscissa(self): - # Solved Aero with shortened lists of table abscissa. - prob = AviaryProblem() - - csv_path = pkg_resources.resource_filename( - "aviary", "subsystems/aerodynamics/flops_based/test/data/high_wing_single_aisle.csv") - - prob.load_inputs(csv_path, phase_info) - prob.add_pre_mission_systems() - prob.add_phases() - prob.add_post_mission_systems() - - prob.link_phases() - - prob.setup() - - prob.set_initial_guesses() - - prob.run_model() - - 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. - - ph_in = deepcopy(phase_info) - - alt = np.array([0., 3000., 6000., 9000., 12000., 15000., 18000., 21000., - 24000., 27000., 30000., 33000., 36000., 38000., 42000.]) - mach = np.array([0., 0.2, 0.4, 0.5, 0.6, 0.7, 0.75, 0.8, 0.85, 0.9]) - alpha = np.array([-2., 0., 2., 4., 6., 8., 10.]) - - polar_builder = FakeDragPolarBuilder(name="aero", altitude=alt, mach=mach, - alpha=alpha) - aero_data = NamedValues() - aero_data.set_val('altitude', alt, 'ft') - aero_data.set_val('mach', mach, 'unitless') - aero_data.set_val('angle_of_attack', alpha, 'deg') - - subsystem_options = {'method': 'solved_alpha', - 'aero_data': aero_data, - 'training_data': True} - ph_in['pre_mission']['external_subsystems'] = [polar_builder] - - ph_in['cruise']['subsystem_options'] = {'core_aerodynamics': subsystem_options} - - prob = AviaryProblem() - - prob.load_inputs(csv_path, ph_in) - - prob.aviary_inputs.set_val(Aircraft.Design.LIFT_POLAR, - np.zeros_like(CL), units='unitless') - prob.aviary_inputs.set_val(Aircraft.Design.DRAG_POLAR, - np.zeros_like(CD), units='unitless') - - prob.add_pre_mission_systems() - prob.add_phases() - prob.add_post_mission_systems() - - prob.link_phases() - - prob.setup() - - prob.set_initial_guesses() - - prob.run_model() - - CL_pass = prob.get_val("traj.cruise.rhs_all.core_aerodynamics.tabular_aero.CL") - CD_pass = prob.get_val("traj.cruise.rhs_all.core_aerodynamics.tabular_aero.CD") - - assert_near_equal(CL_pass, CL_base, 1e-6) - assert_near_equal(CD_pass, CD_base, 1e-6) - class FakeCalcDragPolar(om.ExplicitComponent): """ From 75ee980ceabbfef0805527cf120db76c7b65e844 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Tue, 6 Feb 2024 10:37:58 -0500 Subject: [PATCH 15/21] branch cleanup --- aviary/interface/methods_for_level2.py | 2 -- aviary/interface/reports.py | 2 -- .../flops_based/test/test_solved_aero_group.py | 1 - setup.py | 8 +++++++- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 23fdac964..04c927c3b 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -1602,8 +1602,6 @@ def add_design_variables(self): self.model.add_constraint("h_fit.h_init_flaps", equals=400.0, units="ft", ref=400.0) - # self.problem_type = self.aviary_inputs.get_val('problem_type') - # vehicle sizing problem # size the vehicle (via design GTOW) to meet a target range using all fuel capacity if self.problem_type is ProblemType.SIZING: diff --git a/aviary/interface/reports.py b/aviary/interface/reports.py index f413e0dc6..4060ba991 100644 --- a/aviary/interface/reports.py +++ b/aviary/interface/reports.py @@ -13,7 +13,6 @@ def register_custom_reports(): added to the same reports folder as other default reports """ # TODO top-level aircraft report? - # TODO mission report? # TODO add flag to skip registering reports? # register per-subsystem report generation @@ -55,7 +54,6 @@ def subsystem_report(prob, **kwargs): subsystem.report(prob, reports_folder, **kwargs) -# TODO update with more detailed mission report file def mission_report(prob, **kwargs): """ Creates a basic mission summary report that is place in the "reports" folder 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 0e55ea6ea..dd07de05f 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 @@ -4,7 +4,6 @@ a file. """ import unittest -import pkg_resources import numpy as np import openmdao.api as om from openmdao.utils.assert_utils import assert_near_equal diff --git a/setup.py b/setup.py index fed8c5c50..868679a00 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ from setuptools import find_packages, setup +from pathlib import Path import re @@ -8,6 +9,9 @@ open('aviary/__init__.py').read(), )[0] +with open(Path(__file__).parent / "README.md", encoding="utf-8") as f: + long_description = f.read() + pkgname = "aviary" extras_require = { "test": ["testflo", "pre-commit"], @@ -21,7 +25,9 @@ extras_require["all"] = all_packages setup( - name=pkgname, + name="om-aviary", + long_description=long_description, + long_description_content_type='text/markdown', version=__version__, packages=find_packages(), install_requires=[ From e41669e32653080f81956a7b66fbdf704d44005e Mon Sep 17 00:00:00 2001 From: johnjasa Date: Wed, 7 Feb 2024 11:13:59 -0600 Subject: [PATCH 16/21] Fixes for doc build --- aviary/docs/_config.yml | 2 ++ .../examples/coupled_aircraft_mission_optimization.ipynb | 2 +- aviary/interface/reports.py | 7 ++++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/aviary/docs/_config.yml b/aviary/docs/_config.yml index 0de6df752..1fb1677e7 100644 --- a/aviary/docs/_config.yml +++ b/aviary/docs/_config.yml @@ -13,6 +13,8 @@ execute: timeout: 120 # execute_notebooks: auto +exclude_patterns: ["*examples/reports/*", "*getting_started/reports/*"] + # Define the name of the latex output file for PDF builds latex: latex_documents: diff --git a/aviary/docs/examples/coupled_aircraft_mission_optimization.ipynb b/aviary/docs/examples/coupled_aircraft_mission_optimization.ipynb index 6f92f388f..cb33605c1 100644 --- a/aviary/docs/examples/coupled_aircraft_mission_optimization.ipynb +++ b/aviary/docs/examples/coupled_aircraft_mission_optimization.ipynb @@ -451,7 +451,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.8.17" } }, "nbformat": 4, diff --git a/aviary/interface/reports.py b/aviary/interface/reports.py index 4060ba991..0073b9609 100644 --- a/aviary/interface/reports.py +++ b/aviary/interface/reports.py @@ -65,12 +65,13 @@ def mission_report(prob, **kwargs): """ def _get_phase_value(traj, phase, var_name, units, indices=None): try: - vals = prob.get_val(f"{traj}.{phase}.states:{var_name}", + vals = prob.get_val(f"{traj}.{phase}.timeseries.{var_name}", units=units, indices=indices,) + except KeyError: try: - vals = prob.get_val(f"{traj}.{phase}.timeseries.{var_name}", + vals = prob.get_val(f"{traj}.{phase}.states:{var_name}", units=units, indices=indices,) except KeyError: @@ -138,7 +139,7 @@ def _get_phase_diff(traj, phase, var_name, units, indices=[0, -1]): # get final values, last in traj final_mass = _get_phase_value('traj', phase, 'mass', 'lbm', -1)[0] - final_time = _get_phase_value('traj', phase, 't', 'min', -1)[0] + final_time = _get_phase_value('traj', phase, 't', 'min', -1) final_range = _get_phase_value('traj', phase, 'range', 'nmi', -1) if final_range is None: final_range = _get_phase_value('traj', phase, 'distance', 'nmi', -1) From b4e8210a956c701cfcaeb1e54c8472c8a708926c Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Wed, 7 Feb 2024 16:02:00 -0500 Subject: [PATCH 17/21] undoing unecessary flag --- aviary/interface/methods_for_level2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index 04c927c3b..e8dff31c0 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -182,7 +182,7 @@ def load_inputs(self, aviary_inputs, phase_info=None, engine_builder=None): self.aviary_inputs = aviary_inputs self.initial_guesses = initial_guesses - if mission_method is TWO_DEGREES_OF_FREEDOM or mission_method is SOLVED: + if mission_method is TWO_DEGREES_OF_FREEDOM: aviary_inputs.set_val(Mission.Summary.CRUISE_MASS_FINAL, val=self.initial_guesses['cruise_mass_final'], units='lbm') aviary_inputs.set_val(Mission.Summary.GROSS_MASS, From 78f3bd2ea5bc78751d69e3c0e49fd527a419e304 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Fri, 9 Feb 2024 16:22:23 -0500 Subject: [PATCH 18/21] streamlined mission report fixed reports generating before optimization --- aviary/interface/methods_for_level2.py | 1 - aviary/interface/reports.py | 41 ++++++++------------------ 2 files changed, 12 insertions(+), 30 deletions(-) diff --git a/aviary/interface/methods_for_level2.py b/aviary/interface/methods_for_level2.py index f1b9f7a87..22e672bc6 100644 --- a/aviary/interface/methods_for_level2.py +++ b/aviary/interface/methods_for_level2.py @@ -2,7 +2,6 @@ import warnings import inspect from pathlib import Path -from copy import deepcopy from datetime import datetime import importlib.util import sys diff --git a/aviary/interface/reports.py b/aviary/interface/reports.py index 0073b9609..f9c146b4f 100644 --- a/aviary/interface/reports.py +++ b/aviary/interface/reports.py @@ -21,7 +21,7 @@ def register_custom_reports(): desc='Generates reports for each subsystem builder in the ' 'Aviary Problem', class_name='Problem', - method='run_model', + method='run_driver', pre_or_post='post', # **kwargs ) @@ -30,7 +30,7 @@ def register_custom_reports(): func=mission_report, desc='Generates report for mission results from Aviary problem', class_name='Problem', - method='run_model', + method='run_driver', pre_or_post='post') @@ -68,29 +68,18 @@ def _get_phase_value(traj, phase, var_name, units, indices=None): vals = prob.get_val(f"{traj}.{phase}.timeseries.{var_name}", units=units, indices=indices,) - except KeyError: try: - vals = prob.get_val(f"{traj}.{phase}.states:{var_name}", + vals = prob.get_val(f"{traj}.{phase}.{var_name}", + units=units, + indices=indices,) + # 2DOF breguet range cruise uses time integration to track mass + except TypeError: + vals = prob.get_val(f"{traj}.{phase}.timeseries.time", units=units, indices=indices,) except KeyError: - try: - vals = prob.get_val(f"{traj}.{phase}.timeseries.states:{var_name}", - units=units, - indices=indices,) - except KeyError: - try: - vals = prob.get_val(f"{traj}.{phase}.{var_name}", - units=units, - indices=indices,) - # 2DOF breguet range cruise uses time integration to track mass - except TypeError: - vals = prob.get_val(f"{traj}.{phase}.timeseries.time", - units=units, - indices=indices,) - except KeyError: - vals = None + vals = None return vals @@ -115,7 +104,7 @@ def _get_phase_diff(traj, phase, var_name, units, indices=[0, -1]): # TODO delta mass and fuel consumption need to be tracked separately fuel_burn = _get_phase_diff('traj', phase, 'mass', 'lbm', [-1, 0]) time = _get_phase_diff('traj', phase, 't', 'min') - range = _get_phase_diff('traj', phase, 'range', 'nmi') + range = _get_phase_diff('traj', phase, 'distance', 'nmi') # both "distance" and "range" currently exist to track ground distance, logic # here accounts for that if range is None: @@ -125,10 +114,7 @@ def _get_phase_diff(traj, phase, var_name, units, indices=[0, -1]): if idx == 0: initial_mass = _get_phase_value('traj', phase, 'mass', 'lbm', 0)[0] initial_time = _get_phase_value('traj', phase, 't', 'min', 0) - initial_range = _get_phase_value('traj', phase, 'range', 'nmi', 0) - if initial_range is None: - initial_range = _get_phase_value('traj', phase, 'distance', 'nmi', 0) - initial_range = initial_range[0] + initial_range = _get_phase_value('traj', phase, 'distance', 'nmi', 0)[0] outputs = NamedValues() # Fuel burn is negative of delta mass @@ -140,10 +126,7 @@ def _get_phase_diff(traj, phase, var_name, units, indices=[0, -1]): # get final values, last in traj final_mass = _get_phase_value('traj', phase, 'mass', 'lbm', -1)[0] final_time = _get_phase_value('traj', phase, 't', 'min', -1) - final_range = _get_phase_value('traj', phase, 'range', 'nmi', -1) - if final_range is None: - final_range = _get_phase_value('traj', phase, 'distance', 'nmi', -1) - final_range = final_range[0] + final_range = _get_phase_value('traj', phase, 'distance', 'nmi', -1)[0] totals = NamedValues() totals.set_val('Total Fuel Burn', initial_mass - final_mass, 'lbm') From cd18e50eb457c0ec82eb9bfec53a5ba108bceff2 Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Thu, 15 Feb 2024 11:22:42 -0500 Subject: [PATCH 19/21] removed unused report file --- aviary/utils/report.py | 343 ----------------------------------------- 1 file changed, 343 deletions(-) delete mode 100644 aviary/utils/report.py diff --git a/aviary/utils/report.py b/aviary/utils/report.py deleted file mode 100644 index 2e4c07b74..000000000 --- a/aviary/utils/report.py +++ /dev/null @@ -1,343 +0,0 @@ -import numpy as np -from openmdao.utils.assert_utils import assert_near_equal - -from aviary.variable_info.variables import Aircraft, Dynamic, Mission - - -def report_cruise(prob, remote=False, file=None): - variables = [ - ("mass", "lbm", "mass_initial", "mass_final"), - ("alpha", "deg", "aero_initial.alpha", "aero_final.alpha"), - ("CL", "", "aero_initial.CL", "aero_final.CL"), - ("CD", "", "aero_initial.CD", "aero_final.CD"), - (Dynamic.Mission.THRUST_TOTAL, "lbf", "prop_initial.thrust_net_total", - "prop_final.thrust_net_total"), - (Dynamic.Mission.FUEL_FLOW_RATE_TOTAL, "lbm/s", - "fuel_flow_initial", "fuel_flow_final"), - ("TSFC", "1/h", "prop_initial.sfc", "prop_final.sfc"), - ] - - print(60 * "=", file=file) - print("{:^60}".format("cruise performance"), file=file) - print("{:<21} {:>15} {:>15}".format("variable", "initial", "final"), file=file) - print(60 * "-", file=file) - for name, unstr, pathi, pathf in variables: - units = None if unstr == "" else unstr - vali = prob.get_val(f"cruise.{pathi}", units=units, get_remote=remote)[0] - valf = prob.get_val(f"cruise.{pathf}", units=units, get_remote=remote)[0] - print(f"{name:<10} {unstr:>10} | {vali:15.4f} | {valf:15.4f}", file=file) - print(60 * "=", file=file) - - -def report_fuelburn(prob, remote=False, file=None): - traj_phases = { - "traj": [ - "groundroll", - "rotation", - "ascent", - "accel", - "climb1", - "climb2", - "cruise", - "desc1", - "desc2", - ] - } - - print(40 * "=", file=file) - print("{:^40}".format("fuel burn in each phase"), file=file) - print(40 * "-", file=file) - for traj, phaselist in traj_phases.items(): - for phase in phaselist: - if phase == 'cruise': - vals = prob.get_val( - f"{traj}.{phase}.timeseries.mass", - units="lbm", - indices=[-1, 0], - get_remote=remote, - ) - else: - vals = prob.get_val( - f"{traj}.{phase}.states:mass", - units="lbm", - indices=[-1, 0], - get_remote=remote, - ) - diff = np.diff(vals, axis=0)[0, 0] - print(f"{phase:<12} {diff:>8.2f} lbm", file=file) - print(40 * "=", file=file) - - -def report_gasp_comparison(prob, rtol=0.1, remote=False, file=None): - # values from GASP output of large single aisle 1 at constant altitude using (I think) turbofan_23k_1 - expected_vals = [ - ( - "Design GTOW (lb)", - 175400.0, - prob.get_val(Mission.Design.GROSS_MASS, get_remote=remote)[0], - ), - ( - "Actual GTOW (lb)", - 175400.0, - prob.get_val(Mission.Summary.GROSS_MASS, get_remote=remote)[0], - ), - ( - "OEM (lb)", - 96348.0, - prob.get_val(Aircraft.Design.OPERATING_MASS, get_remote=remote)[0], - ), - # mass at end of descent - ( - "final mass (lb)", - 137348.0, - prob.get_val(Mission.Landing.TOUCHDOWN_MASS, get_remote=remote)[0], - ), - # block fuel, includes reserves - ( - "block fuel (lb)", - 43049.6, - prob.get_val(Mission.Summary.TOTAL_FUEL_MASS, get_remote=remote)[0], - ), - ( - "landing distance (ft)", - 2980.0, - prob.get_val('landing.'+Mission.Landing.GROUND_DISTANCE, - get_remote=remote)[0], - ), - ( - "flight distance (NM)", - 3675.0, - prob.get_val( - "traj.desc2.timeseries.distance", units="NM", get_remote=remote - )[-1][0], - ), - # ROC @ 49 seconds to get from 456.3 ft to 500 ft minus liftoff time - ( - "ascent duration (s)", - 49 + (500 - 465.3) / (2674.4 / 60) - 31.2, - prob.get_val("traj.ascent.t_duration", get_remote=remote)[0], - ), - # AEO takeoff table - ( - "gear retraction time (s)", - 37.3, - prob.get_val("h_fit.t_init_gear", get_remote=remote)[0], - ), - ( - "flaps retraction time (s)", - 47.6, - prob.get_val("h_fit.t_init_flaps", get_remote=remote)[0], - ), - # block time minus taxi time, no final descent - ( - "flight time (hr)", - 8.295 - 10 / 60, - prob.get_val("traj.desc2.timeseries.time", units="h", get_remote=remote)[ - -1 - ][0], - ), - ] - - print(80 * "=", file=file) - print("{:^80}".format("comparison with reference GASP Large Single Aisle 1"), file=file) - print( - "{:<28} | {:>15} | {:>15} | {:>10}".format( - "variable", "GASP vals", "Aviary actual", "rel. err." - ), - file=file, - ) - print(80 * "-", file=file) - results = { - item[0]: { - "desired": item[1], - "actual": item[2], - "rerr": (item[2] - item[1]) / item[1], - } - for item in expected_vals - } - for label, desired, actual in expected_vals: - mark = "" - rerr = (actual - desired) / desired - if abs(rerr) > rtol: - mark = "X" - try: - assert_near_equal(actual, desired, tolerance=rtol) - except ValueError: - mark = "X" - print( - f"{label:<28} | {desired:15.4f} | {actual:15.4f} | {rerr:> .2e} {mark}", - file=file, - ) - print(80 * "=", file=file) - return results - - -def report_benchmark_comparison( - prob, rtol=0.1, remote=False, file=None, size_engine=False, base='GASP' -): - # values from GASP output of large single aisle 1 at constant altitude using (I think) turbofan_23k_1 - - no_size = [ - 175310.9512, - 175310.9512, - 96457.2320, - 137455.2320, - 42853.7192, - 2615.5252, - 3675.0000, - 17.3416, - 38.2458, - 48.2047, - 8.1171, - ] - yes_size = [ - 175539.0746, - 175539.0746, - 96587.0999, - 137585.1013, - 42951.9734, - 2568.6457, - 3675.0000, - 21.4275, - 37.9418, - 49.8459, - 8.1145, - ] - # no_size_yes_pyCycle = [ - # 175394.6488, - # 175394.6488, - # 95820.0936, - # 136997.9090, - # 43394.7398, - # 2560.4481, - # 3675.0000, - # 10.0131, - # 33.2441, - # 40.2571, - # 9.5767, - # ] - - FLOPS_base = [ - 173370.02714369, - 0.000001, # not exactly zero to avoid divide by zero errors - 95569.5191, - 136425.51908379, - 39944.5081, - 7732.7314, - 3378.7, - ] - if size_engine: - check_val = yes_size - elif not size_engine: - check_val = no_size - # elif not size_engine and pyCycle_used: - # check_val = no_size_yes_pyCycle - if base == 'FLOPS': - check_val = FLOPS_base - distance_name = 'traj.descent.timeseries.distance' - landing_dist_name = Mission.Landing.GROUND_DISTANCE - else: - distance_name = "traj.desc2.timeseries.distance" - landing_dist_name = 'landing.'+Mission.Landing.GROUND_DISTANCE - - expected_vals = [ - ( - "Design GTOW (lb)", - check_val[0], - prob.get_val(Mission.Design.GROSS_MASS, get_remote=remote)[0], - ), - ( - "Actual GTOW (lb)", - check_val[1], - prob.get_val(Mission.Summary.GROSS_MASS, get_remote=remote)[0], - ), - ( - "OEW (lb)", - check_val[2], - prob.get_val(Aircraft.Design.OPERATING_MASS, get_remote=remote)[0], - ), - # mass at end of descent - ( - "final mass (lb)", - check_val[3], - prob.get_val(Mission.Landing.TOUCHDOWN_MASS, get_remote=remote)[0], - ), - # block fuel, includes reserves - ( - "block fuel (lb)", - check_val[4], - prob.get_val(Mission.Summary.TOTAL_FUEL_MASS, get_remote=remote)[0], - ), - ( - "landing distance (ft)", - check_val[5], - prob.get_val(landing_dist_name, - get_remote=remote)[0], - ), - ( - "flight distance (NM)", - check_val[6], - prob.get_val( - distance_name, units="NM", get_remote=remote - )[-1][0], - ), - ] - - if base != 'FLOPS': - GASP_vals = [ - # ROC @ 49 seconds to get from 456.3 ft to 500 ft minus liftoff time - ( - "ascent duration (s)", - check_val[7], - prob.get_val("traj.ascent.t_duration", get_remote=remote)[0], - ), - # AEO takeoff table - ( - "gear retraction time (s)", - check_val[8], - prob.get_val("h_fit.t_init_gear", get_remote=remote)[0], - ), - ( - "flaps retraction time (s)", - check_val[9], - prob.get_val("h_fit.t_init_flaps", get_remote=remote)[0], - ), - # block time minus taxi time, no final descent - ( - "flight time (hr)", - check_val[10], - prob.get_val("traj.desc2.timeseries.time", units="h", get_remote=remote)[ - -1 - ][0], - ), - ] - expected_vals = expected_vals + GASP_vals - - print(80 * "=", file=file) - print( - "{:^80}".format( - "comparison with reference value (not a validated value, just a point to check against)" - ), - file=file, - ) - print( - "{:<28} | {:>15} | {:>15} | {:>10}".format( - "variable", "original vals", "Aviary actual", "rel. err." - ), - file=file, - ) - print(80 * "-", file=file) - for label, desired, actual in expected_vals: - mark = "" - rerr = (actual - desired) / desired - if abs(rerr) > rtol: - mark = "X" - try: - assert_near_equal(actual, desired, tolerance=rtol) - except ValueError: - mark = "X" - print( - f"{label:<28} | {desired:15.4f} | {actual:15.4f} | {rerr:> .2e} {mark}", - file=file, - ) - print(80 * "=", file=file) From 951888cc4f5a2dbe18feabdc7508b6c21371c3ec Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Thu, 15 Feb 2024 16:04:45 -0500 Subject: [PATCH 20/21] fixed bug with reports not generated for L1 updated docs to ignore new reports --- aviary/docs/_config.yml | 2 +- aviary/interface/reports.py | 4 ++-- aviary/validation_cases/benchmark_tests/test_bench_FwFm.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/aviary/docs/_config.yml b/aviary/docs/_config.yml index 1fb1677e7..688a3daba 100644 --- a/aviary/docs/_config.yml +++ b/aviary/docs/_config.yml @@ -13,7 +13,7 @@ execute: timeout: 120 # execute_notebooks: auto -exclude_patterns: ["*examples/reports/*", "*getting_started/reports/*"] +exclude_patterns: ["*/reports/*"] # Define the name of the latex output file for PDF builds latex: diff --git a/aviary/interface/reports.py b/aviary/interface/reports.py index f9c146b4f..52896fc01 100644 --- a/aviary/interface/reports.py +++ b/aviary/interface/reports.py @@ -20,7 +20,7 @@ def register_custom_reports(): func=subsystem_report, desc='Generates reports for each subsystem builder in the ' 'Aviary Problem', - class_name='Problem', + class_name='AviaryProblem', method='run_driver', pre_or_post='post', # **kwargs @@ -29,7 +29,7 @@ def register_custom_reports(): register_report(name='mission', func=mission_report, desc='Generates report for mission results from Aviary problem', - class_name='Problem', + class_name='AviaryProblem', method='run_driver', pre_or_post='post') diff --git a/aviary/validation_cases/benchmark_tests/test_bench_FwFm.py b/aviary/validation_cases/benchmark_tests/test_bench_FwFm.py index 8774def13..5a5d0d775 100644 --- a/aviary/validation_cases/benchmark_tests/test_bench_FwFm.py +++ b/aviary/validation_cases/benchmark_tests/test_bench_FwFm.py @@ -389,6 +389,6 @@ def test_bench_FwFm_SNOPT_MPI(self): if __name__ == '__main__': - test = ProblemPhaseTestCase() + test = TestBenchFwFmSerial() test.setUp() test.test_bench_FwFm_SNOPT() From a97264d2cfb8e8a21791039761af17e738a109bd Mon Sep 17 00:00:00 2001 From: jkirk5 Date: Thu, 15 Feb 2024 16:11:39 -0500 Subject: [PATCH 21/21] removed duplicate code --- aviary/interface/reports.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/aviary/interface/reports.py b/aviary/interface/reports.py index 52896fc01..9238ddcd9 100644 --- a/aviary/interface/reports.py +++ b/aviary/interface/reports.py @@ -105,10 +105,6 @@ def _get_phase_diff(traj, phase, var_name, units, indices=[0, -1]): fuel_burn = _get_phase_diff('traj', phase, 'mass', 'lbm', [-1, 0]) time = _get_phase_diff('traj', phase, 't', 'min') range = _get_phase_diff('traj', phase, 'distance', 'nmi') - # both "distance" and "range" currently exist to track ground distance, logic - # here accounts for that - if range is None: - range = _get_phase_diff('traj', phase, 'distance', 'nmi') # get initial values, first in traj if idx == 0: