Skip to content

Commit

Permalink
Merge pull request #126 from jkirk5/reports
Browse files Browse the repository at this point in the history
Mission Report
  • Loading branch information
johnjasa authored Feb 15, 2024
2 parents c33a564 + a97264d commit 4881260
Show file tree
Hide file tree
Showing 14 changed files with 250 additions and 376 deletions.
2 changes: 2 additions & 0 deletions aviary/docs/_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ execute:
timeout: 120
# execute_notebooks: auto

exclude_patterns: ["*/reports/*"]

# Define the name of the latex output file for PDF builds
latex:
latex_documents:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.10"
"version": "3.8.17"
}
},
"nbformat": 4,
Expand Down
3 changes: 2 additions & 1 deletion aviary/docs/user_guide/outputs_and_how_to_read_them.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ 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)* |
| 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`.)

Expand Down
9 changes: 7 additions & 2 deletions aviary/interface/methods_for_level2.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from openmdao.core.component import Component
from openmdao.utils.mpi import MPI
from openmdao.utils.units import convert_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
Expand Down Expand Up @@ -210,6 +211,12 @@ class AviaryProblem(om.Problem):
"""

def __init__(self, analysis_scheme=AnalysisScheme.COLLOCATION, **kwargs):
# 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()
Expand Down Expand Up @@ -1668,8 +1675,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:
Expand Down
116 changes: 110 additions & 6 deletions aviary/interface/reports.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,48 @@
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():
"""
Registers Aviary reports with openMDAO, so they are automatically generated and
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
register_report(name='subsystems',
func=subsystem_report,
desc='Generates reports for each subsystem builder in the '
'Aviary Problem',
class_name='Problem',
method='run_model',
class_name='AviaryProblem',
method='run_driver',
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_driver',
pre_or_post='post')


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)
Expand All @@ -42,3 +52,97 @@ def subsystem_report(prob, **kwargs):

for subsystem in core_subsystems.values():
subsystem.report(prob, reports_folder, **kwargs)


def mission_report(prob, **kwargs):
"""
Creates a basic mission summary report that is place in the "reports" folder
Parameters
----------
prob : AviaryProblem
The AviaryProblem used to generate this report
"""
def _get_phase_value(traj, phase, var_name, units, indices=None):
try:
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=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

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, '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, 'distance', 'nmi', 0)[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', -1)[0]
final_time = _get_phase_value('traj', phase, 't', 'min', -1)
final_range = _get_phase_value('traj', phase, 'distance', 'nmi', -1)[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')
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'}})
20 changes: 13 additions & 7 deletions aviary/interface/utils/markdown_utils.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
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):
# 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:
Expand Down Expand Up @@ -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
Expand All @@ -54,11 +61,10 @@ 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'
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)
7 changes: 4 additions & 3 deletions aviary/subsystems/geometry/geometry_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,11 @@ 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')
f.write(f'# GEOMETRY: {method}\n')
method = ('FLOPS and GASP methods')
else:
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)
f.write('\n## Empennage\n')
Expand Down
4 changes: 2 additions & 2 deletions aviary/subsystems/mass/mass_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,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.value + '-derived relations'
f.write(f'# Mass estimation: {method}')
write_markdown_variable_table(f, prob, outputs, self.meta_data)
79 changes: 79 additions & 0 deletions aviary/subsystems/propulsion/engine_deck.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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).
Expand Down
Loading

0 comments on commit 4881260

Please sign in to comment.