From 638263a575ab3667f50b27e6157de803d3376057 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Mon, 6 Mar 2023 17:09:23 -0500 Subject: [PATCH 01/35] start up updated timeseries report. --- .../test/test_balanced_field_length.py | 8 +- dymos/run_problem.py | 9 +- dymos/visualization/timeseries/__init__.py | 0 .../timeseries/bokeh_timeseries_report.py | 96 +++++++++++++++++++ .../timeseries/timeseries_report.py | 27 ++++++ 5 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 dymos/visualization/timeseries/__init__.py create mode 100644 dymos/visualization/timeseries/bokeh_timeseries_report.py create mode 100644 dymos/visualization/timeseries/timeseries_report.py diff --git a/dymos/examples/balanced_field/test/test_balanced_field_length.py b/dymos/examples/balanced_field/test/test_balanced_field_length.py index 7e373c76a..821453e87 100644 --- a/dymos/examples/balanced_field/test/test_balanced_field_length.py +++ b/dymos/examples/balanced_field/test/test_balanced_field_length.py @@ -10,11 +10,11 @@ from dymos.examples.balanced_field.balanced_field_ode import BalancedFieldODEComp -@use_tempdirs +# @use_tempdirs class TestBalancedFieldLengthRestart(unittest.TestCase): def _make_problem(self): - p = om.Problem() + p = om.Problem(reports=True) p.driver = om.pyOptSparseDriver() p.driver.options['optimizer'] = 'IPOPT' @@ -217,6 +217,10 @@ def _make_problem(self): return p + def test_make_plots(self): + p = self._make_problem() + dm.run_problem(p, run_driver=True, simulate=True, make_plots=True) + @require_pyoptsparse(optimizer='IPOPT') def test_restart_from_sol(self): p = self._make_problem() diff --git a/dymos/run_problem.py b/dymos/run_problem.py index b8af20903..b0b2704b0 100755 --- a/dymos/run_problem.py +++ b/dymos/run_problem.py @@ -109,8 +109,11 @@ def run_problem(problem, refine_method='hp', refine_iteration_limit=0, run_drive subsys.simulate(record_file=simulation_record_file, case_prefix=case_prefix, **_simulate_kwargs) if make_plots: - _sim_record_file = None if not simulate else simulation_record_file - timeseries_plots(solution_record_file, simulation_record_file=_sim_record_file, - plot_dir=plot_dir, problem=problem) + # _sim_record_file = None if not simulate else simulation_record_file + # timeseries_plots(solution_record_file, simulation_record_file=_sim_record_file, + # plot_dir=plot_dir, problem=problem) + from dymos.visualization.timeseries.bokeh_timeseries_report import make_timeseries_report + make_timeseries_report(prob=problem, solution_record_file=solution_record_file, + simulation_record_file=simulation_record_file) return failed diff --git a/dymos/visualization/timeseries/__init__.py b/dymos/visualization/timeseries/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dymos/visualization/timeseries/bokeh_timeseries_report.py b/dymos/visualization/timeseries/bokeh_timeseries_report.py new file mode 100644 index 000000000..6a662ae19 --- /dev/null +++ b/dymos/visualization/timeseries/bokeh_timeseries_report.py @@ -0,0 +1,96 @@ +from pathlib import Path + +from bokeh.io import output_notebook, output_file, save, show +from bokeh.layouts import gridplot, column +from bokeh.models import Legend +from bokeh.plotting import figure +import bokeh.palettes as bp + +import dymos as dm +from dymos.options import options as dymos_options + +import openmdao.utils.reports_system as rptsys + + +_default_timeseries_report_filename = 'timeseries_report.html' + + +def _meta_tree_subsys_iter(tree, recurse=True, cls=None): + """ + Yield a generator of local subsystems of this system. + + Parameters + ---------- + include_self : bool + If True, include this system in the iteration. + recurse : bool + If True, iterate over the whole tree under this system. + typ : str or None + The type of the nodes to be iterated. + cls : None, str, or Sequence + The class of the nodes to be iterated + + Yields + ------ + type or None + """ + _cls = [cls] if isinstance(cls, str) else cls + + for s in tree['children']: + if s['type'] != 'subsystem': + continue + if cls is None or s['class'] in _cls: + yield s + if recurse: + for child in _meta_tree_subsys_iter(s, recurse=True, cls=_cls): + yield child + + +def make_timeseries_report(prob, solution_record_file=None, simulation_record_file=None, solution_history=False): + """ + + Parameters + ---------- + prob + solution_record_file + simulation_record_file + solution_history + + Returns + ------- + + """ + # For the primary timeseries in each phase in each trajectory, build a set of the pathnames + # to be plotted. + parameters_by_phase = {} + timeseries_by_phase = {} + + for traj in prob.model.system_iter(include_self=True, recurse=True, typ=dm.Trajectory): + report_filename = f'{traj.pathname}_{_default_timeseries_report_filename}' + report_path = str(Path(prob.get_reports_dir()) / report_filename) + output_file(report_path) + for phase in traj.system_iter(include_self=True, recurse=True, typ=dm.Phase): + phase_name = phase.pathname.split()[-1] + parameters_by_phase[phase_name] = {} + timeseries_by_phase[phase_name] = {} + + for path, meta in phase.list_inputs(out_stream=None, prom_name=True, units=True, val=True): + if meta['prom_name'].startswith('parameters:'): + parameters_by_phase[phase_name][meta['prom_name']] = {'val': meta['val'], 'units': meta['units']} + + for path, meta in phase.timeseries.list_outputs(out_stream=None, prom_name=True, units=True): + if not meta['prom_name'].startswith('parameters:'): + timeseries_by_phase[phase_name][meta['prom_name']] = {'val': meta['val'], 'units': meta['units']} + +if __name__ == '__main__': + import openmdao.api as om + cr = om.CaseReader('/Users/rfalck/Projects/dymos.git/dymos/examples/balanced_field/test/dymos_solution.db') + for traj_tree in _meta_tree_subsys_iter(cr.problem_metadata['tree'], recurse=True, cls='Trajectory'): + for phase_tree in _meta_tree_subsys_iter(traj_tree, recurse=True, cls=['Phase', 'AnalyticPhase']): + print(phase_tree['name']) + timeseries_meta = [child for child in phase_tree['children'] if child['name'] == 'timeseries'][0] + print(timeseries_meta) + + + + diff --git a/dymos/visualization/timeseries/timeseries_report.py b/dymos/visualization/timeseries/timeseries_report.py new file mode 100644 index 000000000..91d1b25ee --- /dev/null +++ b/dymos/visualization/timeseries/timeseries_report.py @@ -0,0 +1,27 @@ +import dymos as dm +from collections import OrderedDict +import inspect +import re +from pathlib import Path +from openmdao.visualization.htmlpp import HtmlPreprocessor +import openmdao.utils.reports_system as rptsys + +_default_timeseries_report_title = 'Dymos Timeseries Report' +_default_timeseries_report_filename = 'timeseries_report.html' + +def _run_timeseries_report(prob): + """ Function invoked by the reports system """ + + # Find all Trajectory objects in the Problem. Usually, there's only one + for traj in prob.model.system_iter(include_self=True, recurse=True, typ=dm.Trajectory): + report_filename = f'{traj.pathname}_{_default_timeseries_report_filename}' + report_path = str(Path(prob.get_reports_dir()) / report_filename) + create_timeseries_report(traj, report_path) + + +# def _timeseries_report_register(): +# rptsys.register_report('dymos.timeseries', _run_timeseries_report, _default_timeseries_report_title, +# 'prob', 'run_driver', 'post') +# rptsys.register_report('dymos.timeseries', _run_timeseries_report, _default_timeseries_report_title, +# 'prob', 'run_model', 'post') +# rptsys._default_reports.append('dymos.timeseries') From 43de3eaac122320df456b3e875ea91ce8e4eeb2f Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Wed, 15 Mar 2023 15:37:16 -0400 Subject: [PATCH 02/35] dymos now puts a report in the reports directory for each phase that shows the timeseries plots and parameter values for that phase. --- .../doc/test_doc_balanced_field_length.py | 55 +---- dymos/phase/options.py | 2 +- .../timeseries/bokeh_timeseries_report.py | 230 +++++++++++++++--- dymos/visualization/timeseries_plots.py | 215 ++++++++-------- 4 files changed, 326 insertions(+), 176 deletions(-) diff --git a/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py b/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py index 37d55b62a..a9d3b03d8 100644 --- a/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py +++ b/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py @@ -6,15 +6,13 @@ SHOW_PLOTS = True -@use_tempdirs +# @use_tempdirs class TestBalancedFieldLengthForDocs(unittest.TestCase): @require_pyoptsparse(optimizer='IPOPT') def test_balanced_field_length_for_docs(self): - import matplotlib.pyplot as plt import openmdao.api as om from openmdao.utils.general_utils import set_pyoptsparse_opt - from openmdao.utils.assert_utils import assert_near_equal import dymos as dm from dymos.examples.balanced_field.balanced_field_ode import BalancedFieldODEComp @@ -25,6 +23,8 @@ def test_balanced_field_length_for_docs(self): p.driver = om.pyOptSparseDriver() p.driver.declare_coloring() + dm.options['plots'] = 'bokeh' + # Use IPOPT if available, with fallback to SLSQP p.driver.options['optimizer'] = optimizer p.driver.options['print_results'] = False @@ -196,6 +196,9 @@ def test_balanced_field_length_for_docs(self): rto.add_objective('r', loc='final', ref=1.0) + for phase_name, phase in traj._phases.items(): + phase.add_timeseries_output('alpha') + # # Setup the problem and set the initial guess # @@ -233,48 +236,4 @@ def test_balanced_field_length_for_docs(self): p.set_val('traj.climb.states:gam', climb.interp('gam', [0, 5.0]), units='deg') p.set_val('traj.climb.controls:alpha', 5.0, units='deg') - dm.run_problem(p, run_driver=True, simulate=True) - - # Test this example in Dymos' continuous integration - assert_near_equal(p.get_val('traj.rto.states:r')[-1], 2188.2, tolerance=0.01) - - sim_case = om.CaseReader('dymos_solution.db').get_case('final') - - fig, axes = plt.subplots(2, 1, sharex=True, gridspec_kw={'top': 0.92}) - for phase in ['br_to_v1', 'rto', 'v1_to_vr', 'rotate', 'climb']: - r = sim_case.get_val(f'traj.{phase}.timeseries.states:r', units='ft') - v = sim_case.get_val(f'traj.{phase}.timeseries.states:v', units='kn') - t = sim_case.get_val(f'traj.{phase}.timeseries.time', units='s') - axes[0].plot(t, r, '-', label=phase) - axes[1].plot(t, v, '-', label=phase) - fig.suptitle('Balanced Field Length') - axes[1].set_xlabel('time (s)') - axes[0].set_ylabel('range (ft)') - axes[1].set_ylabel('airspeed (kts)') - axes[0].grid(True) - axes[1].grid(True) - - tv1 = sim_case.get_val('traj.br_to_v1.timeseries.time', units='s')[-1, 0] - v1 = sim_case.get_val('traj.br_to_v1.timeseries.states:v', units='kn')[-1, 0] - - tf_rto = sim_case.get_val('traj.rto.timeseries.time', units='s')[-1, 0] - rf_rto = sim_case.get_val('traj.rto.timeseries.states:r', units='ft')[-1, 0] - - axes[0].annotate(f'field length = {r[-1, 0]:5.1f} ft', xy=(t[-1, 0], r[-1, 0]), - xycoords='data', xytext=(0.7, 0.5), - textcoords='axes fraction', arrowprops=dict(arrowstyle='->'), - horizontalalignment='center', verticalalignment='top') - - axes[0].annotate(f'', xy=(tf_rto, rf_rto), - xycoords='data', xytext=(0.7, 0.5), - textcoords='axes fraction', arrowprops=dict(arrowstyle='->'), - horizontalalignment='center', verticalalignment='top') - - axes[1].annotate(f'$v1$ = {v1:5.1f} kts', xy=(tv1, v1), xycoords='data', xytext=(0.5, 0.5), - textcoords='axes fraction', arrowprops=dict(arrowstyle='->'), - horizontalalignment='center', verticalalignment='top') - - plt.legend() - - if SHOW_PLOTS: - plt.show() + dm.run_problem(p, run_driver=True, simulate=True, make_plots=True) diff --git a/dymos/phase/options.py b/dymos/phase/options.py index a0f45b93d..a7f3f3a38 100644 --- a/dymos/phase/options.py +++ b/dymos/phase/options.py @@ -307,7 +307,7 @@ def __init__(self, read_only=False): desc='The unit-reference value of the parameter. This ' 'option is invalid if opt=False.') - self.declare(name='include_timeseries', types=bool, default=True, + self.declare(name='include_timeseries', types=bool, default=False, desc='True if the static parameters should be included in output timeseries, else False') diff --git a/dymos/visualization/timeseries/bokeh_timeseries_report.py b/dymos/visualization/timeseries/bokeh_timeseries_report.py index 6a662ae19..0901d9b5b 100644 --- a/dymos/visualization/timeseries/bokeh_timeseries_report.py +++ b/dymos/visualization/timeseries/bokeh_timeseries_report.py @@ -1,18 +1,20 @@ +import datetime from pathlib import Path from bokeh.io import output_notebook, output_file, save, show -from bokeh.layouts import gridplot, column -from bokeh.models import Legend -from bokeh.plotting import figure +from bokeh.layouts import gridplot, column, grid, GridBox, layout, row +from bokeh.models import Legend, DataTable, Div, ColumnDataSource, Paragraph, TableColumn, TabPanel, Tabs +from bokeh.plotting import figure, curdoc import bokeh.palettes as bp import dymos as dm from dymos.options import options as dymos_options +import openmdao.api as om import openmdao.utils.reports_system as rptsys -_default_timeseries_report_filename = 'timeseries_report.html' +_default_timeseries_report_filename = 'dymos_results_{traj_name}.html' def _meta_tree_subsys_iter(tree, recurse=True, cls=None): @@ -21,12 +23,8 @@ def _meta_tree_subsys_iter(tree, recurse=True, cls=None): Parameters ---------- - include_self : bool - If True, include this system in the iteration. recurse : bool If True, iterate over the whole tree under this system. - typ : str or None - The type of the nodes to be iterated. cls : None, str, or Sequence The class of the nodes to be iterated @@ -46,7 +44,60 @@ def _meta_tree_subsys_iter(tree, recurse=True, cls=None): yield child -def make_timeseries_report(prob, solution_record_file=None, simulation_record_file=None, solution_history=False): +def _load_data_sources(prob, solution_record_file, simulation_record_file): + + data_dict = {} + + sol_cr = om.CaseReader(solution_record_file) + sol_case = sol_cr.get_case('final') + sim_case = om.CaseReader(simulation_record_file).get_case('final') + sol_outputs = {name: meta for name, meta in sol_case.list_outputs(units=True, out_stream=None)} + + abs2prom_map = sol_cr.problem_metadata['abs2prom'] + + for traj in prob.model.system_iter(include_self=True, recurse=True, typ=dm.Trajectory): + traj_name = traj.pathname.split('.')[-1] + data_dict[traj_name] = {'param_data_by_phase': {}, + 'sol_data_by_phase': {}, + 'sim_data_by_phase': {}, + 'timeseries_units': {}} + + for phase in traj.system_iter(include_self=True, recurse=True, typ=dm.Phase): + phase_name = phase.pathname.split('.')[-1] + + data_dict[traj_name]['param_data_by_phase'][phase_name] = {'param': [], 'val': [], 'units': []} + phase_sol_data = data_dict[traj_name]['sol_data_by_phase'][phase_name] = {} + phase_sim_data = data_dict[traj_name]['sim_data_by_phase'][phase_name] = {} + ts_units_dict = data_dict[traj_name]['timeseries_units'] + + param_outputs = {op: meta for op, meta in sol_outputs.items() if op.startswith(f'{phase.pathname}.param_comp.parameter_vals')} + + for output_name, meta in dict(sorted(param_outputs.items())).items(): + param_dict = data_dict[traj_name]['param_data_by_phase'][phase_name] + + prom_name = abs2prom_map['output'][output_name] + param_name = prom_name.split(':')[-1] + + param_dict['param'].append(param_name) + param_dict['units'].append(meta['units']) + param_dict['val'].append(sol_case.get_val(prom_name, units=meta['units'])) + + ts_outputs = {op: meta for op, meta in sol_outputs.items() if op.startswith(f'{phase.pathname}.timeseries')} + + for output_name, meta in ts_outputs.items(): + + prom_name = abs2prom_map['output'][output_name] + var_name = prom_name.split('.')[-1] + + if meta['units'] not in ts_units_dict: + ts_units_dict[var_name] = meta['units'] + phase_sol_data[var_name] = sol_case.get_val(prom_name, units=meta['units']) + phase_sim_data[var_name] = sim_case.get_val(prom_name, units=meta['units']) + + return data_dict + +def make_timeseries_report(prob, solution_record_file=None, simulation_record_file=None, solution_history=False, x_name='time', + ncols=2, min_fig_height=250, max_fig_height=300, margin=10, theme='light_minimal'): """ Parameters @@ -55,6 +106,8 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi solution_record_file simulation_record_file solution_history + x_name : str + Name of the horizontal axis variable in the timeseries. Returns ------- @@ -62,34 +115,155 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi """ # For the primary timeseries in each phase in each trajectory, build a set of the pathnames # to be plotted. - parameters_by_phase = {} - timeseries_by_phase = {} + source_data = _load_data_sources(prob, solution_record_file, simulation_record_file) + + # Colors of each phase in the plot. Start with the bright colors followed by the faded ones. + colors = bp.d3['Category20'][20][0::2] + bp.d3['Category20'][20][1::2] + + curdoc().theme = theme for traj in prob.model.system_iter(include_self=True, recurse=True, typ=dm.Trajectory): - report_filename = f'{traj.pathname}_{_default_timeseries_report_filename}' + traj_name = traj.pathname.split('.')[-1] + report_filename = f'dymos_traj_report_{traj.pathname}.html' report_path = str(Path(prob.get_reports_dir()) / report_filename) - output_file(report_path) + + param_tables = [] + phase_names = [] + for phase in traj.system_iter(include_self=True, recurse=True, typ=dm.Phase): - phase_name = phase.pathname.split()[-1] - parameters_by_phase[phase_name] = {} - timeseries_by_phase[phase_name] = {} + phase_name = phase.pathname.split('.')[-1] + phase_names.append(phase_name) + + # Make the parameter table + source = ColumnDataSource(source_data[traj_name]['param_data_by_phase'][phase_name]) + columns = [ + TableColumn(field='param', title=f'{phase_name} Parameters'), + TableColumn(field='val', title='Value'), + TableColumn(field='units', title='Units'), + ] + param_tables.append(DataTable(source=source, columns=columns, index_position=None, sizing_mode='stretch_width')) + + # Plot the timeseries + ts_units_dict = source_data[traj_name]['timeseries_units'] + + figures = [] + x_range = None + + for var_name in ts_units_dict.keys(): + fig_kwargs = {'x_range': x_range} if x_range is not None else {} + fig = figure(tools='pan,box_zoom,xwheel_zoom,undo,reset,save', + x_axis_label=f'{x_name} ({ts_units_dict[x_name]})', + y_axis_label=f'{var_name} ({ts_units_dict[var_name]})', + toolbar_location='above', + sizing_mode='stretch_both', + min_height=250, max_height=300, + margin=margin, + **fig_kwargs) + legend_data = [] + if x_range is None: + x_range = fig.x_range + for i, phase_name in enumerate(phase_names): + color = colors[i % 20] + sol_data = source_data[traj_name]['sol_data_by_phase'][phase_name] + sim_data = source_data[traj_name]['sim_data_by_phase'][phase_name] + sol_source = ColumnDataSource(sol_data) + sim_source = ColumnDataSource(sim_data) + if x_name in sol_data and var_name in sol_data: + sol_plot = fig.circle(x='time', y=var_name, source=sol_source, color=color) + sim_plot = fig.line(x='time', y=var_name, source=sim_source, color=color) + legend_data.append((phase_name, [sol_plot, sim_plot])) + + legend = Legend(items=legend_data, location='center') + fig.add_layout(legend, 'right') + figures.append(fig) + + if len(figures) % 2 == 1: + figures.append(None) + + # Put the DataTables in a GridBox so we can control the spacing + gbc = [] + row = 0 + col = 0 + for i, table in enumerate(param_tables): + gbc.append((table, row, col, 1, 1)) + col += 1 + if col >= ncols: + col = 0 + row += 1 + param_panel = GridBox(children=gbc, spacing=50, sizing_mode='stretch_both') + + timeseries_panel = grid(children=figures, ncols=ncols, sizing_mode='stretch_both') + + tab_panes = Tabs(tabs=[TabPanel(child=timeseries_panel, title='Timeseries'), + TabPanel(child=param_panel, title='Parameters')], + sizing_mode='stretch_both', + active=0) + + summary = rf'Results of {prob._name}
Creation Date: {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}' + + report_layout = column(children=[Div(text=summary), tab_panes], sizing_mode='stretch_both') + + save(report_layout, filename=report_path, title=f'trajectory results for {traj_name}') + + + + # for traj in prob.model.system_iter(include_self=True, recurse=True, typ=dm.Trajectory): + # report_filename = f'{traj.pathname}_{_default_timeseries_report_filename}' + # report_path = str(Path(prob.get_reports_dir()) / report_filename) + # output_file(report_path) + # for phase in traj.system_iter(include_self=True, recurse=True, typ=dm.Phase): + # phase_name = phase.pathname.split()[-1] + # + # # if param_comp: + # + # param_outputs = param_comp.list_outputs(prom_name=True, units=True, out_stream=None) + # + # + # for abs_name, units in param_units_by_phase.items(): + # param_vals_by_phase[phase_name][f'sol::{prom_name}'] = sol_case.get_val(prom_name, units=units) + # param_vals_by_phase[phase_name][f'sim::{prom_name}'] = sim_case.get_val(prom_name, units=units) + + # timeseries_sys = phase._get_subsystem('timeseries_sys') + # if timeseries_sys: + # param_units_by_phase = {meta['prom_name']: meta['units'] for op, meta in timeseries_sys.list_outputs(prom_name=True, + # units=True, + # out_stream=None)} + # for prom_name, units in param_units_by_phase.items(): + # timeseries_vals_by_phase[phase_name][f'sol::{prom_name}'] = sol_case.get_val(prom_name, units=units) + # timeseries_vals_by_phase[phase_name][f'sim::{prom_name}'] = sim_case.get_val(prom_name, units=units) + + + # timeseries_sys = phase._get_subsystem('timeseries') + # if timeseries_sys: + # output_data = timeseries_sys.list_outputs(prom_name=True, units=True, val=True, out_stream=None) + # timeseries_vals_by_phase[phase_name] = {meta['prom_name']: meta['val'] for _, meta in output_data} + # timeseries_units_by_phase[phase_name] = {meta['prom_name']: meta['units'] for _, meta in output_data} + # + + + + # for param_comp in traj.system_iter(include_self=True, recurse=True, typ=dm.Phase): - for path, meta in phase.list_inputs(out_stream=None, prom_name=True, units=True, val=True): - if meta['prom_name'].startswith('parameters:'): - parameters_by_phase[phase_name][meta['prom_name']] = {'val': meta['val'], 'units': meta['units']} + # print(phase.pathname) - for path, meta in phase.timeseries.list_outputs(out_stream=None, prom_name=True, units=True): - if not meta['prom_name'].startswith('parameters:'): - timeseries_by_phase[phase_name][meta['prom_name']] = {'val': meta['val'], 'units': meta['units']} + # for path, meta in phase.list_inputs(out_stream=None, prom_name=True, units=True, val=True): + # if meta['prom_name'].startswith('parameters:'): + # parameters_by_phase[phase_name][meta['prom_name']] = {'val': meta['val'], 'units': meta['units']} + # + # for path, meta in phase.timeseries.list_outputs(out_stream=None, prom_name=True, units=True): + # if not meta['prom_name'].startswith('parameters:'): + # timeseries_by_phase[phase_name][meta['prom_name']] = {'val': meta['val'], 'units': meta['units']} if __name__ == '__main__': import openmdao.api as om - cr = om.CaseReader('/Users/rfalck/Projects/dymos.git/dymos/examples/balanced_field/test/dymos_solution.db') - for traj_tree in _meta_tree_subsys_iter(cr.problem_metadata['tree'], recurse=True, cls='Trajectory'): - for phase_tree in _meta_tree_subsys_iter(traj_tree, recurse=True, cls=['Phase', 'AnalyticPhase']): - print(phase_tree['name']) - timeseries_meta = [child for child in phase_tree['children'] if child['name'] == 'timeseries'][0] - print(timeseries_meta) + cr = om.CaseReader('/Users/rfalck/Projects/dymos.git/dymos/examples/balanced_field/doc/dymos_solution.db') + # print(cr.problem_metadata['tree']) + # print(cr.problem_metadata['tree']) + # for traj_tree in _meta_tree_subsys_iter(cr.problem_metadata['tree'], recurse=True, cls='Trajectory'): + # for phase_tree in _meta_tree_subsys_iter(traj_tree, recurse=True, cls=['Phase', 'AnalyticPhase']): + # print(phase_tree['name']) + # timeseries_meta = [child for child in phase_tree['children'] if child['name'] == 'timeseries'][0] + # print(timeseries_meta) diff --git a/dymos/visualization/timeseries_plots.py b/dymos/visualization/timeseries_plots.py index 9eba3e9c5..be9bf3884 100644 --- a/dymos/visualization/timeseries_plots.py +++ b/dymos/visualization/timeseries_plots.py @@ -147,7 +147,7 @@ def _bokeh_timeseries_plots(time_units, var_units, phase_names, phases_node_path bg_fill_color='#282828', grid_line_color='#666666', open_browser=False): from bokeh.io import output_notebook, output_file, save, show from bokeh.layouts import gridplot, column - from bokeh.models import Legend + from bokeh.models import Legend, ColumnDataSource from bokeh.plotting import figure import bokeh.palettes as bp @@ -157,7 +157,7 @@ def _bokeh_timeseries_plots(time_units, var_units, phase_names, phases_node_path output_file(os.path.join(plot_dir_path, 'plots.html')) # Prune the edges from the color map - cmap = bp.turbo(len(phase_names) + 2)[1:-1] + phase_color_map = bp.d3['Category20b'] figures = [] colors = {} sol_plots = {} @@ -168,107 +168,124 @@ def _bokeh_timeseries_plots(time_units, var_units, phase_names, phases_node_path min_time = 1.0E21 max_time = -1.0E21 - for iphase, phase_name in enumerate(phase_names): - if phases_node_path: - time_name = f'{phases_node_path}.{phase_name}.timeseries.time' - else: - time_name = f'{phase_name}.timeseries.time' - min_time = min(min_time, np.min(last_solution_case.outputs[time_name])) - max_time = max(max_time, np.max(last_solution_case.outputs[time_name])) - colors[phase_name] = cmap[iphase] - - for var_name, var_unit in var_units.items(): - # Get the labels - time_label = f'time ({time_units[var_name]})' - var_label = f'{var_name} ({var_unit})' - title = f'timeseries.{var_name}' + # for iphase, phase_name in enumerate(phase_names): + # if phases_node_path: + # time_name = f'{phases_node_path}.{phase_name}.timeseries.time' + # else: + # time_name = f'{phase_name}.timeseries.time' + # # min_time = min(min_time, np.min(last_solution_case.outputs[time_name])) + # # max_time = max(max_time, np.max(last_solution_case.outputs[time_name])) + # colors[phase_name] = phase_color_map[iphase + 3] - # add labels, title, and legend - padding = 0.05 * (max_time - min_time) - fig = figure(title=title, background_fill_color=bg_fill_color, - x_range=(min_time - padding, max_time + padding), - width=180, height=180) - fig.xaxis.axis_label = time_label - fig.yaxis.axis_label = var_label - fig.xgrid.grid_line_color = grid_line_color - fig.ygrid.grid_line_color = grid_line_color + print(var_units) - # Plot each phase - for iphase, phase_name in enumerate(phase_names): - sol_color = cmap[iphase] - sim_color = cmap[iphase] + # Build the ColumnDataSource + data = {} - if phases_node_path: - var_name_full = f'{phases_node_path}.{phase_name}.timeseries.{var_name}' - time_name = f'{phases_node_path}.{phase_name}.timeseries.time' - else: - var_name_full = f'{phase_name}.timeseries.{var_name}' - time_name = f'{phase_name}.timeseries.time' + last_solution_case.list_outputs() - # Get values - if var_name_full not in last_solution_case.outputs: - continue - - var_val = last_solution_case.outputs[var_name_full] - time_val = last_solution_case.outputs[time_name] - - for idxs, i in np.ndenumerate(np.zeros(var_val.shape[1:])): - var_val_i = var_val[:, idxs].ravel() - sol_plots[phase_name] = fig.circle(time_val.ravel(), var_val_i, size=5, - color=sol_color, name='sol:' + phase_name) - - # get simulation values, if plotting simulation - if last_simulation_case: - # if the phases_node_path is empty, need to pre-pend names with "sim_traj." - # as that is pre-pended in Trajectory.simulate code - sim_prefix = '' if phases_node_path else 'sim_traj.' - var_val_simulate = last_simulation_case.outputs[sim_prefix + var_name_full] - time_val_simulate = last_simulation_case.outputs[sim_prefix + time_name] - for idxs, i in np.ndenumerate(np.zeros(var_val_simulate.shape[1:])): - var_val_i = var_val_simulate[:, idxs].ravel() - sim_plots[phase_name] = fig.line(time_val_simulate.ravel(), var_val_i, - line_dash='solid', line_width=0.5, color=sim_color, - name='sim:' + phase_name) - figures.append(fig) - - # Implement a single legend for all figures using the example here: - # https://stackoverflow.com/a/56825812/754536 - - # ## Use a dummy figure for the LEGEND - dum_fig = figure(outline_line_alpha=0, toolbar_location=None, - background_fill_color=bg_fill_color, width=250, max_width=250) - - # set the components of the figure invisible - for fig_component in [dum_fig.grid, dum_fig.ygrid, dum_fig.xaxis, dum_fig.yaxis]: - fig_component.visible = False - - # The glyphs referred by the legend need to be present in the figure that holds the legend, - # so we must add them to the figure renderers. - sol_legend_items = [(phase_name + ' solution', [dum_fig.circle([0], [0], - size=5, - color=colors[phase_name], - tags=['sol:' + phase_name])]) for phase_name in phase_names] - sim_legend_items = [(phase_name + ' simulation', [dum_fig.line([0], [0], - line_dash='solid', - line_width=0.5, - color=colors[phase_name], - tags=['sim:' + phase_name])]) for phase_name in phase_names] - legend_items = [j for i in zip(sol_legend_items, sim_legend_items) for j in i] - - # # set the figure range outside of the range of all glyphs - dum_fig.x_range.end = 1005 - dum_fig.x_range.start = 1000 - - legend = Legend(click_policy='hide', location='top_left', border_line_alpha=0, items=legend_items, - background_fill_alpha=0.0, label_text_color='white', label_width=120, spacing=10) - - dum_fig.add_layout(legend, place='center') - - gd = gridplot(figures, ncols=num_cols, sizing_mode='scale_both') - - plots = gridplot([[gd, column(dum_fig, sizing_mode='stretch_height')]], - toolbar_location=None, - sizing_mode='scale_both') + for var_name, units in var_units.items(): + if last_solution_case: + data[f'sol:{var_name}'] = last_solution_case.get_val(var_name, units=units) + if last_simulation_case: + data[f'sim:{var_name}'] = last_simulation_case.get_val(var_name, units=units) + + data_source = ColumnDataSource(data=data) + + # for var_name, var_unit in var_units.items(): + # # Get the labels + # time_label = f'time ({time_units[var_name]})' + # var_label = f'{var_name} ({var_unit})' + # title = f'timeseries.{var_name}' + # + # + # + # # add labels, title, and legend + # padding = 0.05 * (max_time - min_time) + # fig = figure(title=title, background_fill_color=bg_fill_color, + # x_range=(min_time - padding, max_time + padding), + # width=180, height=180) + # fig.xaxis.axis_label = time_label + # fig.yaxis.axis_label = var_label + # fig.xgrid.grid_line_color = grid_line_color + # fig.ygrid.grid_line_color = grid_line_color + # + # # Plot each phase + # for iphase, phase_name in enumerate(phase_names): + # sol_color = phase_color_map[iphase] + # sim_color = phase_color_map[iphase] + # + # if phases_node_path: + # var_name_full = f'{phases_node_path}.{phase_name}.timeseries.{var_name}' + # time_name = f'{phases_node_path}.{phase_name}.timeseries.time' + # else: + # var_name_full = f'{phase_name}.timeseries.{var_name}' + # time_name = f'{phase_name}.timeseries.time' + # + # # Get values + # if var_name_full not in last_solution_case.outputs: + # continue + # + # var_val = last_solution_case.outputs[var_name_full] + # time_val = last_solution_case.outputs[time_name] + # + # for idxs, i in np.ndenumerate(np.zeros(var_val.shape[1:])): + # var_val_i = var_val[:, idxs].ravel() + # sol_plots[phase_name] = fig.circle(time_val.ravel(), var_val_i, size=5, + # color=sol_color, name='sol:' + phase_name) + # + # # get simulation values, if plotting simulation + # if last_simulation_case: + # # if the phases_node_path is empty, need to pre-pend names with "sim_traj." + # # as that is pre-pended in Trajectory.simulate code + # sim_prefix = '' if phases_node_path else 'sim_traj.' + # var_val_simulate = last_simulation_case.outputs[sim_prefix + var_name_full] + # time_val_simulate = last_simulation_case.outputs[sim_prefix + time_name] + # for idxs, i in np.ndenumerate(np.zeros(var_val_simulate.shape[1:])): + # var_val_i = var_val_simulate[:, idxs].ravel() + # sim_plots[phase_name] = fig.line(time_val_simulate.ravel(), var_val_i, + # line_dash='solid', line_width=0.5, color=sim_color, + # name='sim:' + phase_name) + # figures.append(fig) + # + # # Implement a single legend for all figures using the example here: + # # https://stackoverflow.com/a/56825812/754536 + # + # # ## Use a dummy figure for the LEGEND + # dum_fig = figure(outline_line_alpha=0, toolbar_location=None, + # background_fill_color=bg_fill_color, width=250, max_width=250) + # + # # set the components of the figure invisible + # for fig_component in [dum_fig.grid, dum_fig.ygrid, dum_fig.xaxis, dum_fig.yaxis]: + # fig_component.visible = False + # + # # The glyphs referred by the legend need to be present in the figure that holds the legend, + # # so we must add them to the figure renderers. + # sol_legend_items = [(phase_name + ' solution', [dum_fig.circle([0], [0], + # size=5, + # color=colors[phase_name], + # tags=['sol:' + phase_name])]) for phase_name in phase_names] + # sim_legend_items = [(phase_name + ' simulation', [dum_fig.line([0], [0], + # line_dash='solid', + # line_width=0.5, + # color=colors[phase_name], + # tags=['sim:' + phase_name])]) for phase_name in phase_names] + # legend_items = [j for i in zip(sol_legend_items, sim_legend_items) for j in i] + # + # # # set the figure range outside of the range of all glyphs + # dum_fig.x_range.end = 1005 + # dum_fig.x_range.start = 1000 + # + # legend = Legend(click_policy='hide', location='top_left', border_line_alpha=0, items=legend_items, + # background_fill_alpha=0.0, label_text_color='white', label_width=120, spacing=10) + # + # dum_fig.add_layout(legend, place='center') + # + # gd = gridplot(figures, ncols=num_cols, sizing_mode='scale_both') + # + # plots = gridplot([[gd, column(dum_fig, sizing_mode='stretch_height')]], + # toolbar_location=None, + # sizing_mode='scale_both') if dymos_options['notebook_mode'] or open_browser: show(plots) From f0c639da388cf444d33061df320dfb98d727ea68 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Thu, 16 Mar 2023 16:08:34 -0400 Subject: [PATCH 03/35] Moved parameter tables to a different tab for each phase. Added timeseries_options to Phase to allow the option of including control rates and state rates in the timeseries outputs (non-inclusion is the default). Added use_prefix as an option in timeseries_options to remove the prefixed variable class from the timeseries outputs (e.g. 'states:x' is now just 'x', 'controls:u' is now just 'u'). --- .../doc/test_doc_balanced_field_length.py | 7 +- dymos/phase/options.py | 32 +++++++ dymos/phase/phase.py | 3 +- dymos/phase/simulation_phase.py | 3 +- dymos/trajectory/trajectory.py | 15 ++-- dymos/transcriptions/transcription_base.py | 57 ++++++++---- dymos/utils/introspection.py | 56 +++++++----- .../timeseries/bokeh_timeseries_report.py | 88 +++++-------------- 8 files changed, 140 insertions(+), 121 deletions(-) diff --git a/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py b/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py index a9d3b03d8..9a438188c 100644 --- a/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py +++ b/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py @@ -20,16 +20,15 @@ def test_balanced_field_length_for_docs(self): _, optimizer = set_pyoptsparse_opt('IPOPT', fallback=True) - p.driver = om.pyOptSparseDriver() - p.driver.declare_coloring() - dm.options['plots'] = 'bokeh' # Use IPOPT if available, with fallback to SLSQP + p.driver = om.pyOptSparseDriver(optimizer='SNOPT') p.driver.options['optimizer'] = optimizer + p.driver.declare_coloring() p.driver.options['print_results'] = False if optimizer == 'IPOPT': - p.driver.opt_settings['print_level'] = 0 + p.driver.opt_settings['print_level'] = 5 p.driver.opt_settings['derivative_test'] = 'first-order' # First Phase: Brake release to V1 - both engines operable diff --git a/dymos/phase/options.py b/dymos/phase/options.py index a7f3f3a38..8a8145674 100644 --- a/dymos/phase/options.py +++ b/dymos/phase/options.py @@ -739,3 +739,35 @@ def __init__(self, read_only=False): self.declare(name='expr_kwargs', default={}, allow_none=False, desc='Options to be passed to the timeseries expression comp when adding the expression.') + + +class PhaseTimeseriesOptionsDictionary(om.OptionsDictionary): + """ + An OptionsDictionary for phase options related to timeseries.. + + Parameters + ---------- + read_only : bool + If True, setting (via __setitem__ or update) is not permitted. + """ + def __init__(self, read_only=False): + super().__init__(read_only) + + self.declare(name='include_state_rates', types=bool, default=False, + desc='If True, include state rates in the timeseries outputs by default.') + + self.declare(name='include_control_rates', types=bool, default=False, + desc='If True, include control rates in the timeseries outputs by default.') + + self.declare(name='include_t_phase', types=bool, default=False, + desc='If True, include the elapsed phase time in the timeseries outputs by default.') + + self.declare(name='include_states', types=bool, default=True, + desc='If True, include the states in the timeseries outputs by default.') + + self.declare(name='include_controls', types=bool, default=True, + desc='If True, include the controls in the timeseries outputs by default.') + + self.declare(name='use_prefix', types=bool, default=False, + desc='If True, prefix the timeseries outputs with the variable type for compatibility ' + 'with earlier versions of dymos..') diff --git a/dymos/phase/phase.py b/dymos/phase/phase.py index 10322b5d2..0d07e5049 100644 --- a/dymos/phase/phase.py +++ b/dymos/phase/phase.py @@ -15,7 +15,7 @@ from .options import ControlOptionsDictionary, ParameterOptionsDictionary, \ StateOptionsDictionary, TimeOptionsDictionary, ConstraintOptionsDictionary, \ PolynomialControlOptionsDictionary, GridRefinementOptionsDictionary, SimulateOptionsDictionary, \ - TimeseriesOutputOptionsDictionary + TimeseriesOutputOptionsDictionary, PhaseTimeseriesOptionsDictionary from ..transcriptions.transcription_base import TranscriptionBase from ..transcriptions.grid_data import GaussLobattoGrid, RadauGrid, UniformGrid @@ -68,6 +68,7 @@ def __init__(self, from_phase=None, **kwargs): self.refine_options = GridRefinementOptionsDictionary() self.simulate_options = SimulateOptionsDictionary() self.timeseries_ec_vars = {} + self.timeseries_options = PhaseTimeseriesOptionsDictionary() # Dictionaries of variable options that are set by the user via the API # These will be applied over any defaults specified by decorators on the ODE diff --git a/dymos/phase/simulation_phase.py b/dymos/phase/simulation_phase.py index 642dfd632..fe247a4cf 100644 --- a/dymos/phase/simulation_phase.py +++ b/dymos/phase/simulation_phase.py @@ -161,7 +161,8 @@ def initialize_values_from_phase(self, prob, from_phase, phase_path=''): # Assign initial state values for name in phs.state_options: - op = op_dict[f'timeseries.timeseries_comp.states:{name}'] + prefix = 'states:' if from_phase.timeseries_options['use_prefix'] else '' + op = op_dict[f'timeseries.timeseries_comp.{prefix}{name}'] prob[f'{self_path}states:{name}'][...] = op['val'][0, ...] # Assign control values diff --git a/dymos/trajectory/trajectory.py b/dymos/trajectory/trajectory.py index f6b3a6912..3221a3ade 100644 --- a/dymos/trajectory/trajectory.py +++ b/dymos/trajectory/trajectory.py @@ -543,26 +543,31 @@ def _update_linkage_options_configure(self, linkage_options): units[i] = phases[i].time_options['units'] shapes[i] = (1,) elif classes[i] == 'state': - sources[i] = f'timeseries.states:{vars[i]}' + prefix = 'states:' if phases[i].timeseries_options['use_prefix'] else '' + sources[i] = f'timeseries.{prefix}{vars[i]}' units[i] = phases[i].state_options[vars[i]]['units'] shapes[i] = phases[i].state_options[vars[i]]['shape'] elif classes[i] in {'indep_control', 'input_control'}: - sources[i] = f'timeseries.controls:{vars[i]}' + prefix = 'controls:' if phases[i].timeseries_options['use_prefix'] else '' + sources[i] = f'timeseries.{prefix}{vars[i]}' units[i] = phases[i].control_options[vars[i]]['units'] shapes[i] = phases[i].control_options[vars[i]]['shape'] elif classes[i] in {'control_rate', 'control_rate2'}: - sources[i] = f'timeseries.control_rates:{vars[i]}' + prefix = 'control_rates:' if phases[i].timeseries_options['use_prefix'] else '' + sources[i] = f'timeseries.{prefix}:{vars[i]}' control_name = vars[i][:-5] if classes[i] == 'control_rate' else vars[i][:-6] units[i] = phases[i].control_options[control_name]['units'] deriv = 1 if classes[i].endswith('rate') else 2 units[i] = get_rate_units(units[i], phases[i].time_options['units'], deriv=deriv) shapes[i] = phases[i].control_options[control_name]['shape'] elif classes[i] in {'indep_polynomial_control', 'input_polynomial_control'}: - sources[i] = f'timeseries.polynomial_controls:{vars[i]}' + prefix = 'controls:' if phases[i].timeseries_options['use_prefix'] else '' + sources[i] = f'timeseries.{prefix}{vars[i]}' units[i] = phases[i].polynomial_control_options[vars[i]]['units'] shapes[i] = phases[i].polynomial_control_options[vars[i]]['shape'] elif classes[i] in {'polynomial_control_rate', 'polynomial_control_rate2'}: - sources[i] = f'timeseries.polynomial_control_rates:{vars[i]}' + prefix = 'polynomial_control_rates:' if phases[i].timeseries_options['use_prefix'] else '' + sources[i] = f'timeseries.{prefix}{vars[i]}' control_name = vars[i][:-5] if classes[i] == 'polynomial_control_rate' else vars[i][:-6] control_units = phases[i].polynomial_control_options[control_name]['units'] time_units = phases[i].time_options['units'] diff --git a/dymos/transcriptions/transcription_base.py b/dymos/transcriptions/transcription_base.py index ee057cb14..f1ef38414 100644 --- a/dymos/transcriptions/transcription_base.py +++ b/dymos/transcriptions/transcription_base.py @@ -86,7 +86,7 @@ def setup_time(self, phase): for ts_name, ts_options in phase._timeseries.items(): if t_name not in ts_options['outputs']: phase.add_timeseries_output(t_name, timeseries=ts_name) - if t_phase_name not in ts_options['outputs']: + if t_phase_name not in ts_options['outputs'] and phase.timeseries_options['include_t_phase']: phase.add_timeseries_output(t_phase_name, timeseries=ts_name) def configure_time(self, phase): @@ -169,16 +169,22 @@ def setup_controls(self, phase): phase.add_subsystem('control_group', subsys=control_group) + control_prefix = 'controls:' if phase.timeseries_options['use_prefix'] else '' + control_rate_prefix = 'control_rates:' if phase.timeseries_options['use_prefix'] else '' + for name, options in phase.control_options.items(): for ts_name, ts_options in phase._timeseries.items(): - if f'controls:{name}' not in ts_options['outputs']: - phase.add_timeseries_output(name, output_name=f'controls:{name}', + if f'{control_prefix}{name}' not in ts_options['outputs'] and \ + phase.timeseries_options['include_controls']: + phase.add_timeseries_output(name, output_name=f'{control_prefix}{name}', timeseries=ts_name) - if f'control_rates:{name}_rate' not in ts_options['outputs']: - phase.add_timeseries_output(f'{name}_rate', output_name=f'control_rates:{name}_rate', + if f'{control_rate_prefix}{name}_rate' not in ts_options['outputs'] and \ + phase.timeseries_options['include_control_rates']: + phase.add_timeseries_output(f'{name}_rate', output_name=f'{control_rate_prefix}{name}_rate', timeseries=ts_name) - if f'control_rates:{name}_rate2' not in ts_options['outputs']: - phase.add_timeseries_output(f'{name}_rate2', output_name=f'control_rates:{name}_rate2', + if f'{control_rate_prefix}{name}_rate2' not in ts_options['outputs'] and \ + phase.timeseries_options['include_control_rates']: + phase.add_timeseries_output(f'{name}_rate2', output_name=f'{control_rate_prefix}{name}_rate2', timeseries=ts_name) def configure_controls(self, phase): @@ -208,16 +214,22 @@ def setup_polynomial_controls(self, phase): phase.add_subsystem('polynomial_control_group', subsys=sys, promotes_inputs=['*'], promotes_outputs=['*']) + prefix = 'polynomial_controls:' if phase.timeseries_options['use_prefix'] else '' + rate_prefix = 'polynomial_control_rates:' if phase.timeseries_options['use_prefix'] else '' + for name, options in phase.polynomial_control_options.items(): for ts_name, ts_options in phase._timeseries.items(): - if f'polynomial_controls:{name}' not in ts_options['outputs']: - phase.add_timeseries_output(name, output_name=f'polynomial_controls:{name}', + if f'polynomial_controls:{name}' not in ts_options['outputs'] and \ + phase.timeseries_options['include_controls']: + phase.add_timeseries_output(name, output_name=f'{prefix}{name}', timeseries=ts_name) - if f'polynomial_control_rates:{name}_rate' not in ts_options['outputs']: - phase.add_timeseries_output(f'{name}_rate', output_name=f'polynomial_control_rates:{name}_rate', + if f'polynomial_control_rates:{name}_rate' not in ts_options['outputs'] and \ + phase.timeseries_options['include_control_rates']: + phase.add_timeseries_output(f'{name}_rate', output_name=f'{rate_prefix}{name}_rate', timeseries=ts_name) - if f'polynomial_control_rates:{name}_rate2' not in ts_options['outputs']: - phase.add_timeseries_output(f'{name}_rate2', output_name=f'polynomial_control_rates:{name}_rate2', + if f'polynomial_control_rates:{name}_rate2' not in ts_options['outputs'] and \ + phase.timeseries_options['include_control_rates']: + phase.add_timeseries_output(f'{name}_rate2', output_name=f'{rate_prefix}{name}_rate2', timeseries=ts_name) def configure_polynomial_controls(self, phase): @@ -242,6 +254,7 @@ def setup_parameters(self, phase): The phase object to which this transcription instance applies. """ phase._check_parameter_options() + param_prefix = 'parameters:' if phase.timeseries_options['use_prefix'] else '' if phase.parameter_options: param_comp = ParameterComp() @@ -250,8 +263,8 @@ def setup_parameters(self, phase): for name, options in phase.parameter_options.items(): if options['include_timeseries']: for ts_name, ts_options in phase._timeseries.items(): - if f'parameters:{name}' not in ts_options['outputs']: - phase.add_timeseries_output(name, output_name=f'parameters:{name}', + if f'{param_prefix}{name}' not in ts_options['outputs']: + phase.add_timeseries_output(name, output_name=f'{param_prefix}{name}', timeseries=ts_name) def configure_parameters(self, phase): @@ -341,13 +354,19 @@ def configure_states(self, phase): phase : dymos.Phase The phase object to which this transcription instance applies. """ + state_prefix = 'states:' if phase.timeseries_options['use_prefix'] else '' + state_rate_prefix = 'state_rates:' if phase.timeseries_options['use_prefix'] else '' + for name, options in phase.state_options.items(): for ts_name, ts_options in phase._timeseries.items(): - if f'states:{name}' not in ts_options['outputs']: - phase.add_timeseries_output(name, output_name=f'states:{name}', + if f'{state_prefix}{name}' not in ts_options['outputs'] and \ + phase.timeseries_options['include_states']: + phase.add_timeseries_output(name, output_name=f'{state_prefix}{name}', timeseries=ts_name) - if options['rate_source'] and f'state_rates:{name}' not in ts_options['outputs']: - phase.add_timeseries_output(name=options['rate_source'], output_name=f'state_rates:{name}', + if options['rate_source'] and \ + f'{state_rate_prefix}{name}' not in ts_options['outputs'] and \ + phase.timeseries_options['include_state_rates']: + phase.add_timeseries_output(name=options['rate_source'], output_name=f'{state_rate_prefix}{name}', timeseries=ts_name) def setup_ode(self, phase): diff --git a/dymos/utils/introspection.py b/dymos/utils/introspection.py index cebe6d7b2..7222c5d79 100644 --- a/dymos/utils/introspection.py +++ b/dymos/utils/introspection.py @@ -206,11 +206,12 @@ def _configure_constraint_introspection(phase): con['constraint_path'] = f'timeseries.{time_name}_phase' elif var_type == 'state': + prefix = 'states:' if phase.timeseries_options['use_prefix'] else '' state_shape = phase.state_options[var]['shape'] state_units = phase.state_options[var]['units'] con['shape'] = state_shape con['units'] = state_units if con['units'] is None else con['units'] - con['constraint_path'] = f'timeseries.states:{var}' + con['constraint_path'] = f'timeseries.{prefix}{var}' elif var_type == 'parameter': param_shape = phase.parameter_options[var]['shape'] @@ -219,71 +220,78 @@ def _configure_constraint_introspection(phase): con['units'] = param_units if con['units'] is None else con['units'] con['constraint_path'] = f'parameter_vals:{var}' - elif var_type == 'indep_control': + elif var_type in ['indep_control', 'input_control']: + prefix = 'controls:' if phase.timeseries_options['use_prefix'] else '' control_shape = phase.control_options[var]['shape'] control_units = phase.control_options[var]['units'] con['shape'] = control_shape con['units'] = control_units if con['units'] is None else con['units'] - con['constraint_path'] = f'timeseries.controls:{var}' - - elif var_type == 'input_control': - control_shape = phase.control_options[var]['shape'] - control_units = phase.control_options[var]['units'] - - con['shape'] = control_shape - con['units'] = control_units if con['units'] is None else con['units'] - con['constraint_path'] = f'timeseries.controls:{var}' - - elif var_type == 'indep_polynomial_control': + con['constraint_path'] = f'timeseries.{prefix}{var}' + + # elif var_type == 'input_control': + # prefix = 'controls:' if phase.timeseries_options['use_prefix'] else '' + # control_shape = phase.control_options[var]['shape'] + # control_units = phase.control_options[var]['units'] + # + # con['shape'] = control_shape + # con['units'] = control_units if con['units'] is None else con['units'] + # con['constraint_path'] = f'timeseries.{prefix}{var}' + + elif var_type in ['indep_polynomial_control', 'input_polynomial_control']: + prefix = 'polynomial_controls:' if phase.timeseries_options['use_prefix'] else '' control_shape = phase.polynomial_control_options[var]['shape'] control_units = phase.polynomial_control_options[var]['units'] con['shape'] = control_shape con['units'] = control_units if con['units'] is None else con['units'] - con['constraint_path'] = f'timeseries.polynomial_controls:{var}' + con['constraint_path'] = f'timeseries.{prefix}{var}' - elif var_type == 'input_polynomial_control': - control_shape = phase.polynomial_control_options[var]['shape'] - control_units = phase.polynomial_control_options[var]['units'] - con['shape'] = control_shape - con['units'] = control_units if con['units'] is None else con['units'] - con['constraint_path'] = f'timeseries.polynomial_controls:{var}' + # elif var_type == 'input_polynomial_control': + # control_shape = phase.polynomial_control_options[var]['shape'] + # control_units = phase.polynomial_control_options[var]['units'] + # con['shape'] = control_shape + # con['units'] = control_units if con['units'] is None else con['units'] + # con['constraint_path'] = f'timeseries.polynomial_controls:{var}' elif var_type == 'control_rate': + prefix = 'control_rates:' if phase.timeseries_options['use_prefix'] else '' control_name = var[:-5] control_shape = phase.control_options[control_name]['shape'] control_units = phase.control_options[control_name]['units'] con['shape'] = control_shape con['units'] = get_rate_units(control_units, time_units, deriv=1) \ if con['units'] is None else con['units'] - con['constraint_path'] = f'timeseries.control_rates:{var}' + con['constraint_path'] = f'timeseries.{prefix}{var}' elif var_type == 'control_rate2': + prefix = 'control_rates:' if phase.timeseries_options['use_prefix'] else '' control_name = var[:-6] control_shape = phase.control_options[control_name]['shape'] control_units = phase.control_options[control_name]['units'] con['shape'] = control_shape con['units'] = get_rate_units(control_units, time_units, deriv=2) \ if con['units'] is None else con['units'] - con['constraint_path'] = f'timeseries.control_rates:{var}' + con['constraint_path'] = f'timeseries.{prefix}{var}' elif var_type == 'polynomial_control_rate': + prefix = 'polynomial_control_rates:' if phase.timeseries_options['use_prefix'] else '' control_name = var[:-5] control_shape = phase.polynomial_control_options[control_name]['shape'] control_units = phase.polynomial_control_options[control_name]['units'] con['shape'] = control_shape con['units'] = get_rate_units(control_units, time_units, deriv=1) \ if con['units'] is None else con['units'] - con['constraint_path'] = f'timeseries.polynomial_control_rates:{var}' + con['constraint_path'] = f'timeseries.{prefix}{var}' elif var_type == 'polynomial_control_rate2': + prefix = 'polynomial_control_rates:' if phase.timeseries_options['use_prefix'] else '' control_name = var[:-6] control_shape = phase.polynomial_control_options[control_name]['shape'] control_units = phase.polynomial_control_options[control_name]['units'] con['shape'] = control_shape con['units'] = get_rate_units(control_units, time_units, deriv=2) \ if con['units'] is None else con['units'] - con['constraint_path'] = f'timeseries.polynomial_control_rates:{var}' + con['constraint_path'] = f'timeseries.{prefix}{var}' elif var_type == 'timeseries_exec_comp_output': con['shape'] = (1,) diff --git a/dymos/visualization/timeseries/bokeh_timeseries_report.py b/dymos/visualization/timeseries/bokeh_timeseries_report.py index 0901d9b5b..32cb5d9a4 100644 --- a/dymos/visualization/timeseries/bokeh_timeseries_report.py +++ b/dymos/visualization/timeseries/bokeh_timeseries_report.py @@ -72,7 +72,8 @@ def _load_data_sources(prob, solution_record_file, simulation_record_file): param_outputs = {op: meta for op, meta in sol_outputs.items() if op.startswith(f'{phase.pathname}.param_comp.parameter_vals')} - for output_name, meta in dict(sorted(param_outputs.items())).items(): + for output_name in sorted(param_outputs.keys(), key=str.casefold): + meta = param_outputs[output_name] param_dict = data_dict[traj_name]['param_data_by_phase'][phase_name] prom_name = abs2prom_map['output'][output_name] @@ -84,8 +85,10 @@ def _load_data_sources(prob, solution_record_file, simulation_record_file): ts_outputs = {op: meta for op, meta in sol_outputs.items() if op.startswith(f'{phase.pathname}.timeseries')} - for output_name, meta in ts_outputs.items(): + + for output_name in sorted(ts_outputs.keys(), key=str.casefold): + meta = ts_outputs[output_name] prom_name = abs2prom_map['output'][output_name] var_name = prom_name.split('.')[-1] @@ -149,9 +152,13 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi figures = [] x_range = None - for var_name in ts_units_dict.keys(): + for var_name in sorted(ts_units_dict.keys(), key=str.casefold): fig_kwargs = {'x_range': x_range} if x_range is not None else {} - fig = figure(tools='pan,box_zoom,xwheel_zoom,undo,reset,save', + + tool_tips = [(f'{x_name}', '$x'), (f'{var_name}', '$y')] + + fig = figure(tools='pan,box_zoom,xwheel_zoom,hover,undo,reset,save', + tooltips=tool_tips, x_axis_label=f'{x_name} ({ts_units_dict[x_name]})', y_axis_label=f'{var_name} ({ts_units_dict[var_name]})', toolbar_location='above', @@ -159,6 +166,8 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi min_height=250, max_height=300, margin=margin, **fig_kwargs) + fig.xaxis.axis_label_text_font_size = '10pt' + fig.yaxis.axis_label_text_font_size = '10pt' legend_data = [] if x_range is None: x_range = fig.x_range @@ -173,29 +182,23 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi sim_plot = fig.line(x='time', y=var_name, source=sim_source, color=color) legend_data.append((phase_name, [sol_plot, sim_plot])) - legend = Legend(items=legend_data, location='center') + legend = Legend(items=legend_data, location='center', label_text_font_size='8pt') fig.add_layout(legend, 'right') figures.append(fig) + # Since we're putting figures in two columns, make sure we have an even number of things to put in the layout. if len(figures) % 2 == 1: figures.append(None) - # Put the DataTables in a GridBox so we can control the spacing - gbc = [] - row = 0 - col = 0 - for i, table in enumerate(param_tables): - gbc.append((table, row, col, 1, 1)) - col += 1 - if col >= ncols: - col = 0 - row += 1 - param_panel = GridBox(children=gbc, spacing=50, sizing_mode='stretch_both') + param_panels = [TabPanel(child=table, title=f'{phase_names[i]} parameters') + for i, table in enumerate(param_tables)] timeseries_panel = grid(children=figures, ncols=ncols, sizing_mode='stretch_both') - tab_panes = Tabs(tabs=[TabPanel(child=timeseries_panel, title='Timeseries'), - TabPanel(child=param_panel, title='Parameters')], + + + tab_panes = Tabs(tabs=[TabPanel(child=timeseries_panel, title='Timeseries')] + param_panels, + # TabPanel(child=param_panel, title='Parameters')], sizing_mode='stretch_both', active=0) @@ -205,55 +208,6 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi save(report_layout, filename=report_path, title=f'trajectory results for {traj_name}') - - - # for traj in prob.model.system_iter(include_self=True, recurse=True, typ=dm.Trajectory): - # report_filename = f'{traj.pathname}_{_default_timeseries_report_filename}' - # report_path = str(Path(prob.get_reports_dir()) / report_filename) - # output_file(report_path) - # for phase in traj.system_iter(include_self=True, recurse=True, typ=dm.Phase): - # phase_name = phase.pathname.split()[-1] - # - # # if param_comp: - # - # param_outputs = param_comp.list_outputs(prom_name=True, units=True, out_stream=None) - # - # - # for abs_name, units in param_units_by_phase.items(): - # param_vals_by_phase[phase_name][f'sol::{prom_name}'] = sol_case.get_val(prom_name, units=units) - # param_vals_by_phase[phase_name][f'sim::{prom_name}'] = sim_case.get_val(prom_name, units=units) - - # timeseries_sys = phase._get_subsystem('timeseries_sys') - # if timeseries_sys: - # param_units_by_phase = {meta['prom_name']: meta['units'] for op, meta in timeseries_sys.list_outputs(prom_name=True, - # units=True, - # out_stream=None)} - # for prom_name, units in param_units_by_phase.items(): - # timeseries_vals_by_phase[phase_name][f'sol::{prom_name}'] = sol_case.get_val(prom_name, units=units) - # timeseries_vals_by_phase[phase_name][f'sim::{prom_name}'] = sim_case.get_val(prom_name, units=units) - - - # timeseries_sys = phase._get_subsystem('timeseries') - # if timeseries_sys: - # output_data = timeseries_sys.list_outputs(prom_name=True, units=True, val=True, out_stream=None) - # timeseries_vals_by_phase[phase_name] = {meta['prom_name']: meta['val'] for _, meta in output_data} - # timeseries_units_by_phase[phase_name] = {meta['prom_name']: meta['units'] for _, meta in output_data} - # - - - - # for param_comp in traj.system_iter(include_self=True, recurse=True, typ=dm.Phase): - - # print(phase.pathname) - - # for path, meta in phase.list_inputs(out_stream=None, prom_name=True, units=True, val=True): - # if meta['prom_name'].startswith('parameters:'): - # parameters_by_phase[phase_name][meta['prom_name']] = {'val': meta['val'], 'units': meta['units']} - # - # for path, meta in phase.timeseries.list_outputs(out_stream=None, prom_name=True, units=True): - # if not meta['prom_name'].startswith('parameters:'): - # timeseries_by_phase[phase_name][meta['prom_name']] = {'val': meta['val'], 'units': meta['units']} - if __name__ == '__main__': import openmdao.api as om cr = om.CaseReader('/Users/rfalck/Projects/dymos.git/dymos/examples/balanced_field/doc/dymos_solution.db') From 20983ee81ab7ddd13d2e9ce065963374dc2d5728 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Fri, 17 Mar 2023 09:05:28 -0400 Subject: [PATCH 04/35] user can now toggle view of solution/simulation --- .../timeseries/bokeh_timeseries_report.py | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/dymos/visualization/timeseries/bokeh_timeseries_report.py b/dymos/visualization/timeseries/bokeh_timeseries_report.py index 32cb5d9a4..1f52ad8b9 100644 --- a/dymos/visualization/timeseries/bokeh_timeseries_report.py +++ b/dymos/visualization/timeseries/bokeh_timeseries_report.py @@ -3,18 +3,34 @@ from bokeh.io import output_notebook, output_file, save, show from bokeh.layouts import gridplot, column, grid, GridBox, layout, row -from bokeh.models import Legend, DataTable, Div, ColumnDataSource, Paragraph, TableColumn, TabPanel, Tabs +from bokeh.models import Legend, DataTable, Div, ColumnDataSource, TableColumn, TabPanel, Tabs, CheckboxButtonGroup, CustomJS from bokeh.plotting import figure, curdoc import bokeh.palettes as bp -import dymos as dm -from dymos.options import options as dymos_options - import openmdao.api as om -import openmdao.utils.reports_system as rptsys - +import dymos as dm -_default_timeseries_report_filename = 'dymos_results_{traj_name}.html' +# Javascript Callback when the solution/simulation checkbox buttons are toggled +# args: (figures) +_SOL_SIM_TOGGLE_JS = """ +// Loop through figures and toggle the visibility of the renderers +const active = cb_obj.active; +var figures = figures; +var renderer; +for (var i = 0; i < figures.length; i++) { + if (figures[i]) { + for (var j =0; j < figures[i].renderers.length; j++) { + renderer = figures[i].renderers[j] + if (renderer.tags.includes('sol')) { + renderer.visible = active.includes(0); + } + else if (renderer.tags.includes('sim')) { + renderer.visible = active.includes(1); + } + } + } +} +""" def _meta_tree_subsys_iter(tree, recurse=True, cls=None): @@ -127,7 +143,7 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi for traj in prob.model.system_iter(include_self=True, recurse=True, typ=dm.Trajectory): traj_name = traj.pathname.split('.')[-1] - report_filename = f'dymos_traj_report_{traj.pathname}.html' + report_filename = f'{traj.pathname}_results_report.html' report_path = str(Path(prob.get_reports_dir()) / report_filename) param_tables = [] @@ -168,6 +184,7 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi **fig_kwargs) fig.xaxis.axis_label_text_font_size = '10pt' fig.yaxis.axis_label_text_font_size = '10pt' + fig.toolbar.autohide = True legend_data = [] if x_range is None: x_range = fig.x_range @@ -180,6 +197,8 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi if x_name in sol_data and var_name in sol_data: sol_plot = fig.circle(x='time', y=var_name, source=sol_source, color=color) sim_plot = fig.line(x='time', y=var_name, source=sim_source, color=color) + sol_plot.tags.append('sol') + sim_plot.tags.append('sim') legend_data.append((phase_name, [sol_plot, sim_plot])) legend = Legend(items=legend_data, location='center', label_text_font_size='8pt') @@ -193,12 +212,15 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi param_panels = [TabPanel(child=table, title=f'{phase_names[i]} parameters') for i, table in enumerate(param_tables)] - timeseries_panel = grid(children=figures, ncols=ncols, sizing_mode='stretch_both') + sol_sim_toggle = CheckboxButtonGroup(labels=['Solution', 'Simulation'], active=[0, 1]) + sol_sim_toggle.js_on_change("active", CustomJS(code=_SOL_SIM_TOGGLE_JS, args=dict(figures=figures))) + timeseries_panel = grid(children=figures, ncols=ncols, sizing_mode='stretch_both') + + ts_layout = column(children=[sol_sim_toggle, timeseries_panel], sizing_mode='stretch_both') - tab_panes = Tabs(tabs=[TabPanel(child=timeseries_panel, title='Timeseries')] + param_panels, - # TabPanel(child=param_panel, title='Parameters')], + tab_panes = Tabs(tabs=[TabPanel(child=ts_layout, title='Timeseries')] + param_panels, sizing_mode='stretch_both', active=0) @@ -211,14 +233,5 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi if __name__ == '__main__': import openmdao.api as om cr = om.CaseReader('/Users/rfalck/Projects/dymos.git/dymos/examples/balanced_field/doc/dymos_solution.db') - # print(cr.problem_metadata['tree']) - # print(cr.problem_metadata['tree']) - # for traj_tree in _meta_tree_subsys_iter(cr.problem_metadata['tree'], recurse=True, cls='Trajectory'): - # for phase_tree in _meta_tree_subsys_iter(traj_tree, recurse=True, cls=['Phase', 'AnalyticPhase']): - # print(phase_tree['name']) - # timeseries_meta = [child for child in phase_tree['children'] if child['name'] == 'timeseries'][0] - # print(timeseries_meta) - - From ab82d22977956c5cab7088ebf599172631a0b5ec Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Fri, 17 Mar 2023 09:35:43 -0400 Subject: [PATCH 05/35] Moved phase.timeseries_options['use_prefix'] to global dymos option ['use_timeseries_prefix'] Removed 'include_states' and 'include_controls' timeseries options for now. --- dymos/options.py | 4 ++++ dymos/phase/options.py | 10 --------- dymos/phase/simulation_phase.py | 3 ++- dymos/trajectory/trajectory.py | 11 +++++----- dymos/transcriptions/transcription_base.py | 21 +++++++++---------- dymos/utils/introspection.py | 17 ++++++++------- .../timeseries/bokeh_timeseries_report.py | 14 ++++++------- 7 files changed, 38 insertions(+), 42 deletions(-) diff --git a/dymos/options.py b/dymos/options.py index 893b7a79e..382adc6ad 100644 --- a/dymos/options.py +++ b/dymos/options.py @@ -16,3 +16,7 @@ options.declare('notebook_mode', default=False, types=bool, desc='If True, provide notebook-enhanced plots and outputs.') + +options.declare('use_timeseries_prefix', default=False, types=bool, + desc='If True, prefix timeseries outputs with the variable type for states, times, controls,' + 'and parameters.') diff --git a/dymos/phase/options.py b/dymos/phase/options.py index 8a8145674..50f8ceb06 100644 --- a/dymos/phase/options.py +++ b/dymos/phase/options.py @@ -761,13 +761,3 @@ def __init__(self, read_only=False): self.declare(name='include_t_phase', types=bool, default=False, desc='If True, include the elapsed phase time in the timeseries outputs by default.') - - self.declare(name='include_states', types=bool, default=True, - desc='If True, include the states in the timeseries outputs by default.') - - self.declare(name='include_controls', types=bool, default=True, - desc='If True, include the controls in the timeseries outputs by default.') - - self.declare(name='use_prefix', types=bool, default=False, - desc='If True, prefix the timeseries outputs with the variable type for compatibility ' - 'with earlier versions of dymos..') diff --git a/dymos/phase/simulation_phase.py b/dymos/phase/simulation_phase.py index fe247a4cf..dc7f20a8d 100644 --- a/dymos/phase/simulation_phase.py +++ b/dymos/phase/simulation_phase.py @@ -1,6 +1,7 @@ from openmdao.utils.mpi import MPI from .phase import Phase +from ..options import options as dymos_options from ..transcriptions.grid_data import GaussLobattoGrid, RadauGrid, UniformGrid from ..transcriptions import ExplicitShooting, GaussLobatto, Radau @@ -161,7 +162,7 @@ def initialize_values_from_phase(self, prob, from_phase, phase_path=''): # Assign initial state values for name in phs.state_options: - prefix = 'states:' if from_phase.timeseries_options['use_prefix'] else '' + prefix = 'states:' if dymos_options['use_timeseries_prefix'] else '' op = op_dict[f'timeseries.timeseries_comp.{prefix}{name}'] prob[f'{self_path}states:{name}'][...] = op['val'][0, ...] diff --git a/dymos/trajectory/trajectory.py b/dymos/trajectory/trajectory.py index 3221a3ade..4eb8abd90 100644 --- a/dymos/trajectory/trajectory.py +++ b/dymos/trajectory/trajectory.py @@ -21,6 +21,7 @@ from ..transcriptions.common import ParameterComp from ..utils.misc import get_rate_units, _unspecified from ..utils.introspection import get_promoted_vars, get_source_metadata +from ..options import options as dymos_options class Trajectory(om.Group): @@ -543,17 +544,17 @@ def _update_linkage_options_configure(self, linkage_options): units[i] = phases[i].time_options['units'] shapes[i] = (1,) elif classes[i] == 'state': - prefix = 'states:' if phases[i].timeseries_options['use_prefix'] else '' + prefix = 'states:' if dymos_options['use_timeseries_prefix'] else '' sources[i] = f'timeseries.{prefix}{vars[i]}' units[i] = phases[i].state_options[vars[i]]['units'] shapes[i] = phases[i].state_options[vars[i]]['shape'] elif classes[i] in {'indep_control', 'input_control'}: - prefix = 'controls:' if phases[i].timeseries_options['use_prefix'] else '' + prefix = 'controls:' if dymos_options['use_timeseries_prefix'] else '' sources[i] = f'timeseries.{prefix}{vars[i]}' units[i] = phases[i].control_options[vars[i]]['units'] shapes[i] = phases[i].control_options[vars[i]]['shape'] elif classes[i] in {'control_rate', 'control_rate2'}: - prefix = 'control_rates:' if phases[i].timeseries_options['use_prefix'] else '' + prefix = 'control_rates:' if dymos_options['use_timeseries_prefix'] else '' sources[i] = f'timeseries.{prefix}:{vars[i]}' control_name = vars[i][:-5] if classes[i] == 'control_rate' else vars[i][:-6] units[i] = phases[i].control_options[control_name]['units'] @@ -561,12 +562,12 @@ def _update_linkage_options_configure(self, linkage_options): units[i] = get_rate_units(units[i], phases[i].time_options['units'], deriv=deriv) shapes[i] = phases[i].control_options[control_name]['shape'] elif classes[i] in {'indep_polynomial_control', 'input_polynomial_control'}: - prefix = 'controls:' if phases[i].timeseries_options['use_prefix'] else '' + prefix = 'controls:' if dymos_options['use_timeseries_prefix'] else '' sources[i] = f'timeseries.{prefix}{vars[i]}' units[i] = phases[i].polynomial_control_options[vars[i]]['units'] shapes[i] = phases[i].polynomial_control_options[vars[i]]['shape'] elif classes[i] in {'polynomial_control_rate', 'polynomial_control_rate2'}: - prefix = 'polynomial_control_rates:' if phases[i].timeseries_options['use_prefix'] else '' + prefix = 'polynomial_control_rates:' if dymos_options['use_timeseries_prefix'] else '' sources[i] = f'timeseries.{prefix}{vars[i]}' control_name = vars[i][:-5] if classes[i] == 'polynomial_control_rate' else vars[i][:-6] control_units = phases[i].polynomial_control_options[control_name]['units'] diff --git a/dymos/transcriptions/transcription_base.py b/dymos/transcriptions/transcription_base.py index f1ef38414..81382c893 100644 --- a/dymos/transcriptions/transcription_base.py +++ b/dymos/transcriptions/transcription_base.py @@ -10,6 +10,7 @@ from ..utils.misc import _unspecified from ..utils.introspection import configure_states_introspection, get_promoted_vars, get_target_metadata, \ configure_states_discovery +from ..options import options as dymos_options class TranscriptionBase(object): @@ -169,8 +170,8 @@ def setup_controls(self, phase): phase.add_subsystem('control_group', subsys=control_group) - control_prefix = 'controls:' if phase.timeseries_options['use_prefix'] else '' - control_rate_prefix = 'control_rates:' if phase.timeseries_options['use_prefix'] else '' + control_prefix = 'controls:' if dymos_options['use_timeseries_prefix'] else '' + control_rate_prefix = 'control_rates:' if dymos_options['use_timeseries_prefix'] else '' for name, options in phase.control_options.items(): for ts_name, ts_options in phase._timeseries.items(): @@ -214,13 +215,12 @@ def setup_polynomial_controls(self, phase): phase.add_subsystem('polynomial_control_group', subsys=sys, promotes_inputs=['*'], promotes_outputs=['*']) - prefix = 'polynomial_controls:' if phase.timeseries_options['use_prefix'] else '' - rate_prefix = 'polynomial_control_rates:' if phase.timeseries_options['use_prefix'] else '' + prefix = 'polynomial_controls:' if dymos_options['use_timeseries_prefix'] else '' + rate_prefix = 'polynomial_control_rates:' if dymos_options['use_timeseries_prefix'] else '' for name, options in phase.polynomial_control_options.items(): for ts_name, ts_options in phase._timeseries.items(): - if f'polynomial_controls:{name}' not in ts_options['outputs'] and \ - phase.timeseries_options['include_controls']: + if f'polynomial_controls:{name}' not in ts_options['outputs']: phase.add_timeseries_output(name, output_name=f'{prefix}{name}', timeseries=ts_name) if f'polynomial_control_rates:{name}_rate' not in ts_options['outputs'] and \ @@ -254,7 +254,7 @@ def setup_parameters(self, phase): The phase object to which this transcription instance applies. """ phase._check_parameter_options() - param_prefix = 'parameters:' if phase.timeseries_options['use_prefix'] else '' + param_prefix = 'parameters:' if dymos_options['use_timeseries_prefix'] else '' if phase.parameter_options: param_comp = ParameterComp() @@ -354,13 +354,12 @@ def configure_states(self, phase): phase : dymos.Phase The phase object to which this transcription instance applies. """ - state_prefix = 'states:' if phase.timeseries_options['use_prefix'] else '' - state_rate_prefix = 'state_rates:' if phase.timeseries_options['use_prefix'] else '' + state_prefix = 'states:' if dymos_options['use_timeseries_prefix'] else '' + state_rate_prefix = 'state_rates:' if dymos_options['use_timeseries_prefix'] else '' for name, options in phase.state_options.items(): for ts_name, ts_options in phase._timeseries.items(): - if f'{state_prefix}{name}' not in ts_options['outputs'] and \ - phase.timeseries_options['include_states']: + if f'{state_prefix}{name}' not in ts_options['outputs']: phase.add_timeseries_output(name, output_name=f'{state_prefix}{name}', timeseries=ts_name) if options['rate_source'] and \ diff --git a/dymos/utils/introspection.py b/dymos/utils/introspection.py index 7222c5d79..aaaa14175 100644 --- a/dymos/utils/introspection.py +++ b/dymos/utils/introspection.py @@ -6,6 +6,7 @@ import numpy as np from openmdao.utils.array_utils import shape_to_len from dymos.utils.misc import _unspecified +from dymos.options import options as dymos_options from ..phase.options import StateOptionsDictionary, TimeseriesOutputOptionsDictionary from .misc import get_rate_units @@ -206,7 +207,7 @@ def _configure_constraint_introspection(phase): con['constraint_path'] = f'timeseries.{time_name}_phase' elif var_type == 'state': - prefix = 'states:' if phase.timeseries_options['use_prefix'] else '' + prefix = 'states:' if dymos_options['use_timeseries_prefix'] else '' state_shape = phase.state_options[var]['shape'] state_units = phase.state_options[var]['units'] con['shape'] = state_shape @@ -221,7 +222,7 @@ def _configure_constraint_introspection(phase): con['constraint_path'] = f'parameter_vals:{var}' elif var_type in ['indep_control', 'input_control']: - prefix = 'controls:' if phase.timeseries_options['use_prefix'] else '' + prefix = 'controls:' if dymos_options['use_timeseries_prefix'] else '' control_shape = phase.control_options[var]['shape'] control_units = phase.control_options[var]['units'] @@ -230,7 +231,7 @@ def _configure_constraint_introspection(phase): con['constraint_path'] = f'timeseries.{prefix}{var}' # elif var_type == 'input_control': - # prefix = 'controls:' if phase.timeseries_options['use_prefix'] else '' + # prefix = 'controls:' if dymos_options['use_timeseries_prefix'] else '' # control_shape = phase.control_options[var]['shape'] # control_units = phase.control_options[var]['units'] # @@ -239,7 +240,7 @@ def _configure_constraint_introspection(phase): # con['constraint_path'] = f'timeseries.{prefix}{var}' elif var_type in ['indep_polynomial_control', 'input_polynomial_control']: - prefix = 'polynomial_controls:' if phase.timeseries_options['use_prefix'] else '' + prefix = 'polynomial_controls:' if dymos_options['use_timeseries_prefix'] else '' control_shape = phase.polynomial_control_options[var]['shape'] control_units = phase.polynomial_control_options[var]['units'] con['shape'] = control_shape @@ -254,7 +255,7 @@ def _configure_constraint_introspection(phase): # con['constraint_path'] = f'timeseries.polynomial_controls:{var}' elif var_type == 'control_rate': - prefix = 'control_rates:' if phase.timeseries_options['use_prefix'] else '' + prefix = 'control_rates:' if dymos_options['use_timeseries_prefix'] else '' control_name = var[:-5] control_shape = phase.control_options[control_name]['shape'] control_units = phase.control_options[control_name]['units'] @@ -264,7 +265,7 @@ def _configure_constraint_introspection(phase): con['constraint_path'] = f'timeseries.{prefix}{var}' elif var_type == 'control_rate2': - prefix = 'control_rates:' if phase.timeseries_options['use_prefix'] else '' + prefix = 'control_rates:' if dymos_options['use_timeseries_prefix'] else '' control_name = var[:-6] control_shape = phase.control_options[control_name]['shape'] control_units = phase.control_options[control_name]['units'] @@ -274,7 +275,7 @@ def _configure_constraint_introspection(phase): con['constraint_path'] = f'timeseries.{prefix}{var}' elif var_type == 'polynomial_control_rate': - prefix = 'polynomial_control_rates:' if phase.timeseries_options['use_prefix'] else '' + prefix = 'polynomial_control_rates:' if dymos_options['use_timeseries_prefix'] else '' control_name = var[:-5] control_shape = phase.polynomial_control_options[control_name]['shape'] control_units = phase.polynomial_control_options[control_name]['units'] @@ -284,7 +285,7 @@ def _configure_constraint_introspection(phase): con['constraint_path'] = f'timeseries.{prefix}{var}' elif var_type == 'polynomial_control_rate2': - prefix = 'polynomial_control_rates:' if phase.timeseries_options['use_prefix'] else '' + prefix = 'polynomial_control_rates:' if dymos_options['use_timeseries_prefix'] else '' control_name = var[:-6] control_shape = phase.polynomial_control_options[control_name]['shape'] control_units = phase.polynomial_control_options[control_name]['units'] diff --git a/dymos/visualization/timeseries/bokeh_timeseries_report.py b/dymos/visualization/timeseries/bokeh_timeseries_report.py index 1f52ad8b9..803c0e39e 100644 --- a/dymos/visualization/timeseries/bokeh_timeseries_report.py +++ b/dymos/visualization/timeseries/bokeh_timeseries_report.py @@ -213,12 +213,16 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi for i, table in enumerate(param_tables)] sol_sim_toggle = CheckboxButtonGroup(labels=['Solution', 'Simulation'], active=[0, 1]) - sol_sim_toggle.js_on_change("active", CustomJS(code=_SOL_SIM_TOGGLE_JS, args=dict(figures=figures))) - timeseries_panel = grid(children=figures, ncols=ncols, sizing_mode='stretch_both') + sol_sim_row = row(children=[Div(text='Display data:', sizing_mode='stretch_height'), + sol_sim_toggle], + sizing_mode='stretch_both', + max_height=50) + + figures_grid = grid(children=figures, ncols=ncols, sizing_mode='stretch_both') - ts_layout = column(children=[sol_sim_toggle, timeseries_panel], sizing_mode='stretch_both') + ts_layout = column(children=[sol_sim_row, figures_grid], sizing_mode='stretch_both') tab_panes = Tabs(tabs=[TabPanel(child=ts_layout, title='Timeseries')] + param_panels, sizing_mode='stretch_both', @@ -230,8 +234,4 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi save(report_layout, filename=report_path, title=f'trajectory results for {traj_name}') -if __name__ == '__main__': - import openmdao.api as om - cr = om.CaseReader('/Users/rfalck/Projects/dymos.git/dymos/examples/balanced_field/doc/dymos_solution.db') - From 5ae4e43cd66b42309fd90663d6033eb2324a45d8 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Fri, 17 Mar 2023 10:29:07 -0400 Subject: [PATCH 06/35] timeseries outputs now stored with full name as key rather than output name --- .../doc/test_doc_balanced_field_length.py | 4 ++- dymos/phase/phase.py | 25 ++++++++++--------- dymos/transcriptions/transcription_base.py | 3 +-- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py b/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py index 9a438188c..0decd3e8e 100644 --- a/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py +++ b/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py @@ -196,7 +196,9 @@ def test_balanced_field_length_for_docs(self): rto.add_objective('r', loc='final', ref=1.0) for phase_name, phase in traj._phases.items(): - phase.add_timeseries_output('alpha') + phase.add_timeseries_output('T_nominal', output_name='T') + phase.add_timeseries_output('T_engine_out', output_name='T') + phase.add_timeseries_output('T_shutdown', output_name='T') # # Setup the problem and set the initial guess diff --git a/dymos/phase/phase.py b/dymos/phase/phase.py index 0d07e5049..647ccbba9 100644 --- a/dymos/phase/phase.py +++ b/dymos/phase/phase.py @@ -1451,17 +1451,18 @@ def _add_timeseries_output(self, name, output_name=None, units=_unspecified, sha if timeseries not in self._timeseries: raise ValueError(f'Timeseries {timeseries} does not exist in phase {self.pathname}') - if expr: - output_name = name.split('=')[0].strip() - elif '*' in name: - output_name = name - elif output_name is None: - output_name = name.rpartition('.')[-1] - - if rate: - output_name = output_name + '_rate' - - if output_name not in self._timeseries[timeseries]['outputs']: + if output_name is None: + if expr: + output_name = name.split('=')[0].strip() + elif '*' in name: + output_name = name + elif output_name is None: + output_name = name.rpartition('.')[-1] + + if rate: + output_name = output_name + '_rate' + + if name not in self._timeseries[timeseries]['outputs']: ts_output = TimeseriesOutputOptionsDictionary() ts_output['name'] = name ts_output['output_name'] = output_name @@ -1472,7 +1473,7 @@ def _add_timeseries_output(self, name, output_name=None, units=_unspecified, sha ts_output['is_expr'] = expr ts_output['expr_kwargs'] = expr_kwargs - self._timeseries[timeseries]['outputs'][output_name] = ts_output + self._timeseries[timeseries]['outputs'][name] = ts_output return output_name diff --git a/dymos/transcriptions/transcription_base.py b/dymos/transcriptions/transcription_base.py index 81382c893..3705579d2 100644 --- a/dymos/transcriptions/transcription_base.py +++ b/dymos/transcriptions/transcription_base.py @@ -175,8 +175,7 @@ def setup_controls(self, phase): for name, options in phase.control_options.items(): for ts_name, ts_options in phase._timeseries.items(): - if f'{control_prefix}{name}' not in ts_options['outputs'] and \ - phase.timeseries_options['include_controls']: + if f'{control_prefix}{name}' not in ts_options['outputs']: phase.add_timeseries_output(name, output_name=f'{control_prefix}{name}', timeseries=ts_name) if f'{control_rate_prefix}{name}_rate' not in ts_options['outputs'] and \ From d32eeae34c65ba779999044e1cd6ac3eb869d9af Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Fri, 17 Mar 2023 16:13:30 -0400 Subject: [PATCH 07/35] one more fix for the unfound timeseries issue --- dymos/utils/introspection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dymos/utils/introspection.py b/dymos/utils/introspection.py index aaaa14175..c34de8a11 100644 --- a/dymos/utils/introspection.py +++ b/dymos/utils/introspection.py @@ -777,7 +777,7 @@ def configure_timeseries_output_introspection(phase): not_found = set() - for output_name, output_options in ts_opts['outputs'].items(): + for name, output_options in ts_opts['outputs'].items(): if output_options['is_expr']: output_meta = phase.timeseries_ec_vars[ts_name][output_name]['meta_data'] else: @@ -786,7 +786,7 @@ def configure_timeseries_output_introspection(phase): output_options['output_name'], phase=phase) except ValueError as e: - not_found.add(output_name) + not_found.add(name) continue output_options['src'] = output_meta['src'] From c111892b4a084cdd3e23e45df1e78e4a77d9744b Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Fri, 17 Mar 2023 19:51:29 -0400 Subject: [PATCH 08/35] phase selection in the trajectory results report --- .../doc/test_doc_balanced_field_length.py | 1 + .../timeseries/bokeh_timeseries_report.py | 58 +++++++++++++++++-- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py b/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py index 0decd3e8e..f29eff5b8 100644 --- a/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py +++ b/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py @@ -199,6 +199,7 @@ def test_balanced_field_length_for_docs(self): phase.add_timeseries_output('T_nominal', output_name='T') phase.add_timeseries_output('T_engine_out', output_name='T') phase.add_timeseries_output('T_shutdown', output_name='T') + phase.add_timeseries_output('alpha') # # Setup the problem and set the initial guess diff --git a/dymos/visualization/timeseries/bokeh_timeseries_report.py b/dymos/visualization/timeseries/bokeh_timeseries_report.py index 803c0e39e..428baca3d 100644 --- a/dymos/visualization/timeseries/bokeh_timeseries_report.py +++ b/dymos/visualization/timeseries/bokeh_timeseries_report.py @@ -3,7 +3,8 @@ from bokeh.io import output_notebook, output_file, save, show from bokeh.layouts import gridplot, column, grid, GridBox, layout, row -from bokeh.models import Legend, DataTable, Div, ColumnDataSource, TableColumn, TabPanel, Tabs, CheckboxButtonGroup, CustomJS +from bokeh.models import Legend, DataTable, Div, ColumnDataSource, TableColumn, TabPanel, Tabs, CheckboxButtonGroup,\ + CustomJS, MultiChoice from bokeh.plotting import figure, curdoc import bokeh.palettes as bp @@ -17,6 +18,7 @@ const active = cb_obj.active; var figures = figures; var renderer; + for (var i = 0; i < figures.length; i++) { if (figures[i]) { for (var j =0; j < figures[i].renderers.length; j++) { @@ -32,6 +34,40 @@ } """ +# Javascript Callback when the solution/simulation checkbox buttons are toggled +# args: (figures) +_PHASE_SELECT_JS = """ +// Loop through figures and toggle the visibility of the renderers +const phases_to_show = cb_obj.value; +const kinds_to_show = sol_sim_toggle.active; +var figures = figures; +var renderer; +var renderer_phase; + +function show_renderer(renderer, phases_to_show, kinds_to_show) { + var tags = renderer.tags; + for(var k=0; k < tags.length; k++) { + if (tags[k].substring(0, 6) == 'phase:') { + renderer_phase = tags[k].substring(6); + break; + } + } + return ((tags.includes('sol') && kinds_to_show.includes(0)) || + (tags.includes('sim') && kinds_to_show.includes(1))) && + phases_to_show.includes(renderer_phase); +} + +for (var i = 0; i < figures.length; i++) { + if (figures[i]) { + for (var j=0; j < figures[i].renderers.length; j++) { + renderer = figures[i].renderers[j]; + // Get the phase with which this renderer is associated + renderer.visible = show_renderer(renderer, phases_to_show, kinds_to_show); + } + } +} +""" + def _meta_tree_subsys_iter(tree, recurse=True, cls=None): """ @@ -197,8 +233,8 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi if x_name in sol_data and var_name in sol_data: sol_plot = fig.circle(x='time', y=var_name, source=sol_source, color=color) sim_plot = fig.line(x='time', y=var_name, source=sim_source, color=color) - sol_plot.tags.append('sol') - sim_plot.tags.append('sim') + sol_plot.tags.extend(['sol', f'phase:{phase_name}']) + sim_plot.tags.extend(['sim', f'phase:{phase_name}']) legend_data.append((phase_name, [sol_plot, sim_plot])) legend = Legend(items=legend_data, location='center', label_text_font_size='8pt') @@ -220,9 +256,23 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi sizing_mode='stretch_both', max_height=50) + phase_select = MultiChoice(options=[phase_name for phase_name in phase_names], + value=[phase_name for phase_name in phase_names], + sizing_mode='stretch_both', + min_width=400, min_height=50) + phase_select.js_on_change("value", CustomJS(code=_PHASE_SELECT_JS, + args=dict(figures=figures, + sol_sim_toggle=sol_sim_toggle))) + + phase_select_row = row(children=[Div(text='Plot phases:'), phase_select], + sizing_mode='stretch_width') + figures_grid = grid(children=figures, ncols=ncols, sizing_mode='stretch_both') - ts_layout = column(children=[sol_sim_row, figures_grid], sizing_mode='stretch_both') + ts_layout = column(children=[sol_sim_row, + phase_select_row, + figures_grid], + sizing_mode='stretch_both') tab_panes = Tabs(tabs=[TabPanel(child=ts_layout, title='Timeseries')] + param_panels, sizing_mode='stretch_both', From 453d1aa334163b2a30276bcc8c525c760993753f Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Fri, 17 Mar 2023 21:52:22 -0400 Subject: [PATCH 09/35] cleanup of traj_results_report --- .../timeseries/bokeh_timeseries_report.py | 44 +++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/dymos/visualization/timeseries/bokeh_timeseries_report.py b/dymos/visualization/timeseries/bokeh_timeseries_report.py index 428baca3d..6a483cab3 100644 --- a/dymos/visualization/timeseries/bokeh_timeseries_report.py +++ b/dymos/visualization/timeseries/bokeh_timeseries_report.py @@ -11,6 +11,23 @@ import openmdao.api as om import dymos as dm + +_js_show_renderer = """ +function show_renderer(renderer, phases_to_show, kinds_to_show) { + var tags = renderer.tags; + for(var k=0; k < tags.length; k++) { + if (tags[k].substring(0, 6) == 'phase:') { + renderer_phase = tags[k].substring(6); + break; + } + } + return ((tags.includes('sol') && kinds_to_show.includes(0)) || + (tags.includes('sim') && kinds_to_show.includes(1))) && + phases_to_show.includes(renderer_phase); +} + +""" + # Javascript Callback when the solution/simulation checkbox buttons are toggled # args: (figures) _SOL_SIM_TOGGLE_JS = """ @@ -36,9 +53,8 @@ # Javascript Callback when the solution/simulation checkbox buttons are toggled # args: (figures) -_PHASE_SELECT_JS = """ -// Loop through figures and toggle the visibility of the renderers -const phases_to_show = cb_obj.value; +_js_show_figures = """ +const phases_to_show = phase_select.value; const kinds_to_show = sol_sim_toggle.active; var figures = figures; var renderer; @@ -192,7 +208,7 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi # Make the parameter table source = ColumnDataSource(source_data[traj_name]['param_data_by_phase'][phase_name]) columns = [ - TableColumn(field='param', title=f'{phase_name} Parameters'), + TableColumn(field='param', title='Parameter'), TableColumn(field='val', title='Value'), TableColumn(field='units', title='Units'), ] @@ -249,7 +265,6 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi for i, table in enumerate(param_tables)] sol_sim_toggle = CheckboxButtonGroup(labels=['Solution', 'Simulation'], active=[0, 1]) - sol_sim_toggle.js_on_change("active", CustomJS(code=_SOL_SIM_TOGGLE_JS, args=dict(figures=figures))) sol_sim_row = row(children=[Div(text='Display data:', sizing_mode='stretch_height'), sol_sim_toggle], @@ -260,9 +275,6 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi value=[phase_name for phase_name in phase_names], sizing_mode='stretch_both', min_width=400, min_height=50) - phase_select.js_on_change("value", CustomJS(code=_PHASE_SELECT_JS, - args=dict(figures=figures, - sol_sim_toggle=sol_sim_toggle))) phase_select_row = row(children=[Div(text='Plot phases:'), phase_select], sizing_mode='stretch_width') @@ -282,6 +294,22 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi report_layout = column(children=[Div(text=summary), tab_panes], sizing_mode='stretch_both') + # Assign callbacks + + sol_sim_toggle.js_on_change("active", + CustomJS(code=_js_show_figures, + args=dict(figures=figures, + sol_sim_toggle=sol_sim_toggle, + phase_select=phase_select))) + + phase_select.js_on_change("value", + CustomJS(code=_js_show_figures, + args=dict(figures=figures, + sol_sim_toggle=sol_sim_toggle, + phase_select=phase_select))) + + # Save + save(report_layout, filename=report_path, title=f'trajectory results for {traj_name}') From f419f99a91ae8f12e40d3431815d50a9f3d5ae0d Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Fri, 17 Mar 2023 22:07:53 -0400 Subject: [PATCH 10/35] handle case where either solution or simulation data is not present --- .../doc/test_doc_balanced_field_length.py | 2 +- .../timeseries/bokeh_timeseries_report.py | 62 ++++++++++++------- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py b/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py index f29eff5b8..83899b56a 100644 --- a/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py +++ b/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py @@ -238,4 +238,4 @@ def test_balanced_field_length_for_docs(self): p.set_val('traj.climb.states:gam', climb.interp('gam', [0, 5.0]), units='deg') p.set_val('traj.climb.controls:alpha', 5.0, units='deg') - dm.run_problem(p, run_driver=True, simulate=True, make_plots=True) + dm.run_problem(p, run_driver=True, simulate=False, make_plots=True) diff --git a/dymos/visualization/timeseries/bokeh_timeseries_report.py b/dymos/visualization/timeseries/bokeh_timeseries_report.py index 6a483cab3..b81c14332 100644 --- a/dymos/visualization/timeseries/bokeh_timeseries_report.py +++ b/dymos/visualization/timeseries/bokeh_timeseries_report.py @@ -112,16 +112,29 @@ def _meta_tree_subsys_iter(tree, recurse=True, cls=None): yield child -def _load_data_sources(prob, solution_record_file, simulation_record_file): +def _load_data_sources(prob, solution_record_file=None, simulation_record_file=None): data_dict = {} - sol_cr = om.CaseReader(solution_record_file) - sol_case = sol_cr.get_case('final') - sim_case = om.CaseReader(simulation_record_file).get_case('final') - sol_outputs = {name: meta for name, meta in sol_case.list_outputs(units=True, out_stream=None)} - - abs2prom_map = sol_cr.problem_metadata['abs2prom'] + if Path(solution_record_file).is_file(): + sol_cr = om.CaseReader(solution_record_file) + sol_case = sol_cr.get_case('final') + outputs = {name: meta for name, meta in sol_case.list_outputs(units=True, out_stream=None)} + abs2prom_map = sol_cr.problem_metadata['abs2prom'] + else: + sol_case = None + + if Path(simulation_record_file).is_file(): + sim_cr = om.CaseReader(simulation_record_file) + sim_case = sim_cr.get_case('final') + outputs = {name: meta for name, meta in sim_case.list_outputs(units=True, out_stream=None)} + abs2prom_map = sim_cr.problem_metadata['abs2prom'] + else: + sim_case = None + + if sol_cr is None and sim_cr is None: + om.issue_warning('No recorded data provided. Trajectory results report will not be created.') + return for traj in prob.model.system_iter(include_self=True, recurse=True, typ=dm.Trajectory): traj_name = traj.pathname.split('.')[-1] @@ -138,7 +151,8 @@ def _load_data_sources(prob, solution_record_file, simulation_record_file): phase_sim_data = data_dict[traj_name]['sim_data_by_phase'][phase_name] = {} ts_units_dict = data_dict[traj_name]['timeseries_units'] - param_outputs = {op: meta for op, meta in sol_outputs.items() if op.startswith(f'{phase.pathname}.param_comp.parameter_vals')} + param_outputs = {op: meta for op, meta in outputs.items() if op.startswith(f'{phase.pathname}.param_comp.parameter_vals')} + param_case = sol_case if sol_case else sim_case for output_name in sorted(param_outputs.keys(), key=str.casefold): meta = param_outputs[output_name] @@ -149,11 +163,9 @@ def _load_data_sources(prob, solution_record_file, simulation_record_file): param_dict['param'].append(param_name) param_dict['units'].append(meta['units']) - param_dict['val'].append(sol_case.get_val(prom_name, units=meta['units'])) - - ts_outputs = {op: meta for op, meta in sol_outputs.items() if op.startswith(f'{phase.pathname}.timeseries')} - + param_dict['val'].append(param_case.get_val(prom_name, units=meta['units'])) + ts_outputs = {op: meta for op, meta in outputs.items() if op.startswith(f'{phase.pathname}.timeseries')} for output_name in sorted(ts_outputs.keys(), key=str.casefold): meta = ts_outputs[output_name] @@ -162,13 +174,16 @@ def _load_data_sources(prob, solution_record_file, simulation_record_file): if meta['units'] not in ts_units_dict: ts_units_dict[var_name] = meta['units'] - phase_sol_data[var_name] = sol_case.get_val(prom_name, units=meta['units']) - phase_sim_data[var_name] = sim_case.get_val(prom_name, units=meta['units']) + + if sol_case: + phase_sol_data[var_name] = sol_case.get_val(prom_name, units=meta['units']) + if sim_case: + phase_sim_data[var_name] = sim_case.get_val(prom_name, units=meta['units']) return data_dict -def make_timeseries_report(prob, solution_record_file=None, simulation_record_file=None, solution_history=False, x_name='time', - ncols=2, min_fig_height=250, max_fig_height=300, margin=10, theme='light_minimal'): +def make_timeseries_report(prob, solution_record_file=None, simulation_record_file=None, solution_history=False, + x_name='time', ncols=2, margin=10, theme='light_minimal'): """ Parameters @@ -247,11 +262,16 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi sol_source = ColumnDataSource(sol_data) sim_source = ColumnDataSource(sim_data) if x_name in sol_data and var_name in sol_data: - sol_plot = fig.circle(x='time', y=var_name, source=sol_source, color=color) - sim_plot = fig.line(x='time', y=var_name, source=sim_source, color=color) - sol_plot.tags.extend(['sol', f'phase:{phase_name}']) - sim_plot.tags.extend(['sim', f'phase:{phase_name}']) - legend_data.append((phase_name, [sol_plot, sim_plot])) + legend_items = [] + if sol_data: + sol_plot = fig.circle(x='time', y=var_name, source=sol_source, color=color) + sol_plot.tags.extend(['sol', f'phase:{phase_name}']) + legend_items.append(sol_plot) + if sim_data: + sim_plot = fig.line(x='time', y=var_name, source=sim_source, color=color) + sim_plot.tags.extend(['sim', f'phase:{phase_name}']) + legend_items.append(sim_plot) + legend_data.append((phase_name, legend_items)) legend = Legend(items=legend_data, location='center', label_text_font_size='8pt') fig.add_layout(legend, 'right') From 6b8518d73e00973cfafacb4c47df5a99db5a23d3 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Fri, 17 Mar 2023 22:10:25 -0400 Subject: [PATCH 11/35] handle case of missing simulation or solution data --- .../balanced_field/doc/test_doc_balanced_field_length.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py b/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py index 83899b56a..f29eff5b8 100644 --- a/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py +++ b/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py @@ -238,4 +238,4 @@ def test_balanced_field_length_for_docs(self): p.set_val('traj.climb.states:gam', climb.interp('gam', [0, 5.0]), units='deg') p.set_val('traj.climb.controls:alpha', 5.0, units='deg') - dm.run_problem(p, run_driver=True, simulate=False, make_plots=True) + dm.run_problem(p, run_driver=True, simulate=True, make_plots=True) From cacfd89e694c280ddd90d3340672cf9172c47fb9 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Mon, 20 Mar 2023 19:36:33 -0400 Subject: [PATCH 12/35] explicit shooting fix --- .../explicit_shooting/explicit_shooting.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/dymos/transcriptions/explicit_shooting/explicit_shooting.py b/dymos/transcriptions/explicit_shooting/explicit_shooting.py index 127e5e76f..fc2f6bfb9 100644 --- a/dymos/transcriptions/explicit_shooting/explicit_shooting.py +++ b/dymos/transcriptions/explicit_shooting/explicit_shooting.py @@ -139,7 +139,7 @@ def setup_time(self, phase): for ts_name, ts_options in phase._timeseries.items(): if t_name not in ts_options['outputs']: phase.add_timeseries_output(t_name, timeseries=ts_name) - if t_phase_name not in ts_options['outputs']: + if t_phase_name not in ts_options['outputs'] and phase.timeseries_options['include_t_phase']: phase.add_timeseries_output(t_phase_name, timeseries=ts_name) # if times_per_seg is None: @@ -363,10 +363,12 @@ def setup_controls(self, phase): if f'controls:{name}' not in ts_options['outputs']: phase.add_timeseries_output(name, output_name=f'controls:{name}', timeseries=ts_name) - if f'control_rates:{name}_rate' not in ts_options['outputs']: + if f'control_rates:{name}_rate' not in ts_options['outputs'] \ + and phase.timeseries_options['include_control_rates']: phase.add_timeseries_output(f'{name}_rate', output_name=f'control_rates:{name}_rate', timeseries=ts_name) - if f'control_rates:{name}_rate2' not in ts_options['outputs']: + if f'control_rates:{name}_rate2' not in ts_options['outputs'] \ + and phase.timeseries_options['include_control_rates']: phase.add_timeseries_output(f'{name}_rate2', output_name=f'control_rates:{name}_rate2', timeseries=ts_name) @@ -450,10 +452,12 @@ def setup_polynomial_controls(self, phase): if f'polynomial_controls:{name}' not in ts_options['outputs']: phase.add_timeseries_output(name, output_name=f'polynomial_controls:{name}', timeseries=ts_name) - if f'polynomial_control_rates:{name}_rate' not in ts_options['outputs']: + if f'polynomial_control_rates:{name}_rate' not in ts_options['outputs'] \ + and phase.timeseries_options['include_control_rates']: phase.add_timeseries_output(f'{name}_rate', output_name=f'polynomial_control_rates:{name}_rate', timeseries=ts_name) - if f'polynomial_control_rates:{name}_rate2' not in ts_options['outputs']: + if f'polynomial_control_rates:{name}_rate2' not in ts_options['outputs'] \ + and phase.timeseries_options['include_control_rates']: phase.add_timeseries_output(f'{name}_rate2', output_name=f'polynomial_control_rates:{name}_rate2', timeseries=ts_name) @@ -570,17 +574,17 @@ def configure_defects(self, phase): if options['continuity'] and any_control_cnty: controls_to_enforce.add(control_name) - phase.connect(f'timeseries.controls:{control_name}', + phase.connect(f'controls:{control_name}', f'continuity_comp.controls:{control_name}', src_indices=src_idxs) if options['rate_continuity'] and any_rate_cnty: control_rates_to_enforce.add(control_name) - phase.connect(f'timeseries.control_rates:{control_name}_rate', + phase.connect(f'control_rates:{control_name}_rate', f'continuity_comp.control_rates:{control_name}_rate', src_indices=src_idxs) if options['rate2_continuity'] and any_rate_cnty: control_rates2_to_enforce.add(control_name) - phase.connect(f'timeseries.control_rates:{control_name}_rate2', + phase.connect(f'control_rates:{control_name}_rate2', f'continuity_comp.control_rates:{control_name}_rate2', src_indices=src_idxs) From 8c644d405da30f5e0c89d19eae10e70f8b1b933c Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Wed, 22 Mar 2023 08:25:29 -0400 Subject: [PATCH 13/35] adding some doc strings --- .../timeseries/bokeh_timeseries_report.py | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/dymos/visualization/timeseries/bokeh_timeseries_report.py b/dymos/visualization/timeseries/bokeh_timeseries_report.py index b81c14332..85c06c467 100644 --- a/dymos/visualization/timeseries/bokeh_timeseries_report.py +++ b/dymos/visualization/timeseries/bokeh_timeseries_report.py @@ -2,7 +2,7 @@ from pathlib import Path from bokeh.io import output_notebook, output_file, save, show -from bokeh.layouts import gridplot, column, grid, GridBox, layout, row +from bokeh.layouts import column, grid, row from bokeh.models import Legend, DataTable, Div, ColumnDataSource, TableColumn, TabPanel, Tabs, CheckboxButtonGroup,\ CustomJS, MultiChoice from bokeh.plotting import figure, curdoc @@ -182,22 +182,28 @@ def _load_data_sources(prob, solution_record_file=None, simulation_record_file=N return data_dict -def make_timeseries_report(prob, solution_record_file=None, simulation_record_file=None, solution_history=False, + +def make_timeseries_report(prob, solution_record_file=None, simulation_record_file=None, x_name='time', ncols=2, margin=10, theme='light_minimal'): """ + Create the bokeh-based timeseries results report. Parameters ---------- - prob - solution_record_file - simulation_record_file - solution_history + prob : om.Problem + The problem instance for which the timeseries plots are being created. + solution_record_file : str + The path to the solution record file, if available. + simulation_record_file : str + The path to the simulation record file, if available. x_name : str Name of the horizontal axis variable in the timeseries. - - Returns - ------- - + ncols : int + The number of columns of timeseries output plots. + margin : int + A margin to be placed between the plot figures. + theme : str + A valid bokeh theme name to style the report. """ # For the primary timeseries in each phase in each trajectory, build a set of the pathnames # to be plotted. From fdd92a6e0de7a5a570290c3cbcaf633b86689191 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Fri, 24 Mar 2023 16:16:37 -0400 Subject: [PATCH 14/35] implemented a new bokeh based trajectory results report --- dymos/__init__.py | 2 +- dymos/{options.py => _options.py} | 0 dymos/phase/options.py | 2 +- dymos/phase/phase.py | 4 +- dymos/phase/simulation_phase.py | 2 +- dymos/trajectory/phase_linkage_comp.py | 2 +- dymos/trajectory/trajectory.py | 9 ++-- .../transcriptions/common/continuity_comp.py | 2 +- dymos/transcriptions/common/control_group.py | 2 +- dymos/transcriptions/common/parameter_comp.py | 2 +- .../common/polynomial_control_group.py | 2 +- dymos/transcriptions/common/time_comp.py | 2 +- .../common/timeseries_output_comp.py | 2 +- .../explicit_shooting/explicit_shooting.py | 41 ++++++++++++------- .../explicit_shooting_timeseries_comp.py | 2 +- .../explicit_shooting/ode_evaluation_group.py | 2 +- .../explicit_shooting/ode_integration_comp.py | 2 +- .../state_rate_collector_comp.py | 2 +- .../explicit_shooting/tau_comp.py | 2 +- .../components/collocation_comp.py | 2 +- .../control_endpoint_defect_comp.py | 2 +- .../gauss_lobatto_interleave_comp.py | 2 +- .../components/state_independents.py | 2 +- .../components/state_interp_comp.py | 2 +- .../components/state_rate_collector_comp.py | 2 +- dymos/transcriptions/transcription_base.py | 2 +- dymos/utils/introspection.py | 10 +++-- .../timeseries/bokeh_timeseries_report.py | 29 +++++++++---- dymos/visualization/timeseries_plots.py | 2 +- 29 files changed, 84 insertions(+), 55 deletions(-) rename dymos/{options.py => _options.py} (100%) diff --git a/dymos/__init__.py b/dymos/__init__.py index b66a1fe8c..31cc1655f 100644 --- a/dymos/__init__.py +++ b/dymos/__init__.py @@ -6,4 +6,4 @@ from .trajectory.trajectory import Trajectory from .run_problem import run_problem from .load_case import load_case -from .options import options +from ._options import options diff --git a/dymos/options.py b/dymos/_options.py similarity index 100% rename from dymos/options.py rename to dymos/_options.py diff --git a/dymos/phase/options.py b/dymos/phase/options.py index 50f8ceb06..24493aa64 100644 --- a/dymos/phase/options.py +++ b/dymos/phase/options.py @@ -753,7 +753,7 @@ class PhaseTimeseriesOptionsDictionary(om.OptionsDictionary): def __init__(self, read_only=False): super().__init__(read_only) - self.declare(name='include_state_rates', types=bool, default=False, + self.declare(name='include_state_rates', types=bool, default=True, desc='If True, include state rates in the timeseries outputs by default.') self.declare(name='include_control_rates', types=bool, default=False, diff --git a/dymos/phase/phase.py b/dymos/phase/phase.py index 647ccbba9..96f173433 100644 --- a/dymos/phase/phase.py +++ b/dymos/phase/phase.py @@ -1462,7 +1462,7 @@ def _add_timeseries_output(self, name, output_name=None, units=_unspecified, sha if rate: output_name = output_name + '_rate' - if name not in self._timeseries[timeseries]['outputs']: + if output_name not in self._timeseries[timeseries]['outputs']: ts_output = TimeseriesOutputOptionsDictionary() ts_output['name'] = name ts_output['output_name'] = output_name @@ -1473,7 +1473,7 @@ def _add_timeseries_output(self, name, output_name=None, units=_unspecified, sha ts_output['is_expr'] = expr ts_output['expr_kwargs'] = expr_kwargs - self._timeseries[timeseries]['outputs'][name] = ts_output + self._timeseries[timeseries]['outputs'][output_name] = ts_output return output_name diff --git a/dymos/phase/simulation_phase.py b/dymos/phase/simulation_phase.py index dc7f20a8d..352014441 100644 --- a/dymos/phase/simulation_phase.py +++ b/dymos/phase/simulation_phase.py @@ -1,7 +1,7 @@ from openmdao.utils.mpi import MPI from .phase import Phase -from ..options import options as dymos_options +from .._options import options as dymos_options from ..transcriptions.grid_data import GaussLobattoGrid, RadauGrid, UniformGrid from ..transcriptions import ExplicitShooting, GaussLobatto, Radau diff --git a/dymos/trajectory/phase_linkage_comp.py b/dymos/trajectory/phase_linkage_comp.py index 2e32c9cc1..dfdd242e8 100644 --- a/dymos/trajectory/phase_linkage_comp.py +++ b/dymos/trajectory/phase_linkage_comp.py @@ -2,7 +2,7 @@ import openmdao.api as om -from ..options import options as dymos_options +from .._options import options as dymos_options from ..utils.misc import _unspecified diff --git a/dymos/trajectory/trajectory.py b/dymos/trajectory/trajectory.py index 4eb8abd90..1b8e9907f 100644 --- a/dymos/trajectory/trajectory.py +++ b/dymos/trajectory/trajectory.py @@ -21,7 +21,7 @@ from ..transcriptions.common import ParameterComp from ..utils.misc import get_rate_units, _unspecified from ..utils.introspection import get_promoted_vars, get_source_metadata -from ..options import options as dymos_options +from .._options import options as dymos_options class Trajectory(om.Group): @@ -562,7 +562,7 @@ def _update_linkage_options_configure(self, linkage_options): units[i] = get_rate_units(units[i], phases[i].time_options['units'], deriv=deriv) shapes[i] = phases[i].control_options[control_name]['shape'] elif classes[i] in {'indep_polynomial_control', 'input_polynomial_control'}: - prefix = 'controls:' if dymos_options['use_timeseries_prefix'] else '' + prefix = 'polynomial_controls:' if dymos_options['use_timeseries_prefix'] else '' sources[i] = f'timeseries.{prefix}{vars[i]}' units[i] = phases[i].polynomial_control_options[vars[i]]['units'] shapes[i] = phases[i].polynomial_control_options[vars[i]]['shape'] @@ -726,7 +726,10 @@ def _print_on_rank(rank=0, *args, **kwargs): } def _get_prefixed_var(var, phase): - return f'{prefixes[phase.classify_var(var)]}{var}' + if dymos_options['use_timeseries_prefix']: + return f'{prefixes[phase.classify_var(var)]}{var}' + else: + return var # First, if the user requested all states and time be continuous ('*', '*'), then # expand it out. diff --git a/dymos/transcriptions/common/continuity_comp.py b/dymos/transcriptions/common/continuity_comp.py index d7ac7b610..b43deaef3 100644 --- a/dymos/transcriptions/common/continuity_comp.py +++ b/dymos/transcriptions/common/continuity_comp.py @@ -3,7 +3,7 @@ from ..grid_data import GridData from ...utils.misc import get_rate_units -from ...options import options as dymos_options +from ..._options import options as dymos_options class ContinuityCompBase(om.ExplicitComponent): diff --git a/dymos/transcriptions/common/control_group.py b/dymos/transcriptions/common/control_group.py index 64feee0f3..34db4658b 100644 --- a/dymos/transcriptions/common/control_group.py +++ b/dymos/transcriptions/common/control_group.py @@ -9,7 +9,7 @@ from ...utils.lagrange import lagrange_matrices from ...utils.indexing import get_desvar_indices from ...utils.constants import INF_BOUND -from ...options import options as dymos_options +from ..._options import options as dymos_options class ControlInterpComp(om.ExplicitComponent): diff --git a/dymos/transcriptions/common/parameter_comp.py b/dymos/transcriptions/common/parameter_comp.py index 580e34e81..5eda9b1be 100644 --- a/dymos/transcriptions/common/parameter_comp.py +++ b/dymos/transcriptions/common/parameter_comp.py @@ -5,7 +5,7 @@ from openmdao.core.explicitcomponent import ExplicitComponent from ...utils.misc import _unspecified -from ...options import options as dymos_options +from ..._options import options as dymos_options class ParameterComp(ExplicitComponent): diff --git a/dymos/transcriptions/common/polynomial_control_group.py b/dymos/transcriptions/common/polynomial_control_group.py index 9de34132e..967b2a13a 100644 --- a/dymos/transcriptions/common/polynomial_control_group.py +++ b/dymos/transcriptions/common/polynomial_control_group.py @@ -8,7 +8,7 @@ from ...utils.misc import get_rate_units, reshape_val from ...utils.constants import INF_BOUND -from ...options import options as dymos_options +from ..._options import options as dymos_options class LGLPolynomialControlComp(om.ExplicitComponent): diff --git a/dymos/transcriptions/common/time_comp.py b/dymos/transcriptions/common/time_comp.py index f862e426d..00bf4a2a9 100644 --- a/dymos/transcriptions/common/time_comp.py +++ b/dymos/transcriptions/common/time_comp.py @@ -2,7 +2,7 @@ import openmdao.api as om -from ...options import options as dymos_options +from ..._options import options as dymos_options class TimeComp(om.ExplicitComponent): diff --git a/dymos/transcriptions/common/timeseries_output_comp.py b/dymos/transcriptions/common/timeseries_output_comp.py index 7cda66d44..447f25dce 100644 --- a/dymos/transcriptions/common/timeseries_output_comp.py +++ b/dymos/transcriptions/common/timeseries_output_comp.py @@ -1,7 +1,7 @@ import openmdao.api as om from ...transcriptions.grid_data import GridData -from ...options import options as dymos_options +from ..._options import options as dymos_options class TimeseriesOutputCompBase(om.ExplicitComponent): diff --git a/dymos/transcriptions/explicit_shooting/explicit_shooting.py b/dymos/transcriptions/explicit_shooting/explicit_shooting.py index fc2f6bfb9..b144f43e2 100644 --- a/dymos/transcriptions/explicit_shooting/explicit_shooting.py +++ b/dymos/transcriptions/explicit_shooting/explicit_shooting.py @@ -10,6 +10,7 @@ from ..transcription_base import TranscriptionBase from ..grid_data import GaussLobattoGrid, RadauGrid, UniformGrid from .ode_integration_comp import ODEIntegrationComp +from ..._options import options as dymos_options from ...utils.misc import get_rate_units, CoerceDesvar from ...utils.indexing import get_src_indices_by_row from ...utils.introspection import get_promoted_vars, get_source_metadata, get_targets @@ -259,14 +260,16 @@ def configure_states(self, phase): integ = phase._get_subsystem('integrator') integ._configure_states() + state_prefix = 'states:' if dymos_options['use_timeseries_prefix'] else '' + for name, options in phase.state_options.items(): phase.promotes('integrator', inputs=[f'states:{name}']) for ts_name, ts_options in phase._timeseries.items(): - if f'states:{name}' not in ts_options['outputs']: - phase.add_timeseries_output(name, output_name=f'states:{name}', + if f'{state_prefix}{name}' not in ts_options['outputs']: + phase.add_timeseries_output(name, output_name=f'{state_prefix}{name}', timeseries=ts_name) - # Add the appropriate design parameters + # Add the appropriate design variables for state_name, options in phase.state_options.items(): if options['fix_final']: raise ValueError('fix_final is not a valid option for states when using the ' @@ -358,18 +361,22 @@ def setup_controls(self, phase): subsys=control_group, promotes=['dt_dstau', 'controls:*', 'control_values:*', 'control_rates:*']) + control_prefix = 'controls:' if dymos_options['use_timeseries_prefix'] else '' + control_rate_prefix = 'control_rates:' if dymos_options['use_timeseries_prefix'] else '' + for name, options in phase.control_options.items(): for ts_name, ts_options in phase._timeseries.items(): - if f'controls:{name}' not in ts_options['outputs']: - phase.add_timeseries_output(name, output_name=f'controls:{name}', + + if f'{control_prefix}{name}' not in ts_options['outputs']: + phase.add_timeseries_output(name, output_name=f'{control_prefix}{name}', timeseries=ts_name) - if f'control_rates:{name}_rate' not in ts_options['outputs'] \ + if f'{control_rate_prefix}{name}_rate' not in ts_options['outputs'] \ and phase.timeseries_options['include_control_rates']: - phase.add_timeseries_output(f'{name}_rate', output_name=f'control_rates:{name}_rate', + phase.add_timeseries_output(f'{name}_rate', output_name=f'{control_rate_prefix}{name}_rate', timeseries=ts_name) - if f'control_rates:{name}_rate2' not in ts_options['outputs'] \ + if f'{control_rate_prefix}{name}_rate2' not in ts_options['outputs'] \ and phase.timeseries_options['include_control_rates']: - phase.add_timeseries_output(f'{name}_rate2', output_name=f'control_rates:{name}_rate2', + phase.add_timeseries_output(f'{name}_rate2', output_name=f'{control_rate_prefix}{name}_rate2', timeseries=ts_name) def configure_controls(self, phase): @@ -447,18 +454,22 @@ def setup_polynomial_controls(self, phase): phase.add_subsystem('polynomial_control_group', subsys=sys, promotes_inputs=['*'], promotes_outputs=['*']) + control_prefix = 'controls:' if dymos_options['use_timeseries_prefix'] else '' + control_rate_prefix = 'control_rates:' if dymos_options['use_timeseries_prefix'] else '' + for name, options in phase.polynomial_control_options.items(): + for ts_name, ts_options in phase._timeseries.items(): - if f'polynomial_controls:{name}' not in ts_options['outputs']: - phase.add_timeseries_output(name, output_name=f'polynomial_controls:{name}', + if f'{control_prefix}{name}' not in ts_options['outputs']: + phase.add_timeseries_output(name, output_name=f'{control_prefix}{name}', timeseries=ts_name) - if f'polynomial_control_rates:{name}_rate' not in ts_options['outputs'] \ + if f'{control_rate_prefix}{name}_rate' not in ts_options['outputs'] \ and phase.timeseries_options['include_control_rates']: - phase.add_timeseries_output(f'{name}_rate', output_name=f'polynomial_control_rates:{name}_rate', + phase.add_timeseries_output(f'{name}_rate', output_name=f'{control_rate_prefix}{name}_rate', timeseries=ts_name) - if f'polynomial_control_rates:{name}_rate2' not in ts_options['outputs'] \ + if f'{control_rate_prefix}{name}_rate2' not in ts_options['outputs'] \ and phase.timeseries_options['include_control_rates']: - phase.add_timeseries_output(f'{name}_rate2', output_name=f'polynomial_control_rates:{name}_rate2', + phase.add_timeseries_output(f'{name}_rate2', output_name=f'{control_rate_prefix}{name}_rate2', timeseries=ts_name) def configure_polynomial_controls(self, phase): diff --git a/dymos/transcriptions/explicit_shooting/explicit_shooting_timeseries_comp.py b/dymos/transcriptions/explicit_shooting/explicit_shooting_timeseries_comp.py index b0374a539..786adceb2 100644 --- a/dymos/transcriptions/explicit_shooting/explicit_shooting_timeseries_comp.py +++ b/dymos/transcriptions/explicit_shooting/explicit_shooting_timeseries_comp.py @@ -1,7 +1,7 @@ import numpy as np from openmdao.utils.units import unit_conversion -from ...options import options as dymos_options +from ..._options import options as dymos_options from ..common.timeseries_output_comp import TimeseriesOutputCompBase diff --git a/dymos/transcriptions/explicit_shooting/ode_evaluation_group.py b/dymos/transcriptions/explicit_shooting/ode_evaluation_group.py index 1387f0411..4a0e0e411 100644 --- a/dymos/transcriptions/explicit_shooting/ode_evaluation_group.py +++ b/dymos/transcriptions/explicit_shooting/ode_evaluation_group.py @@ -215,7 +215,7 @@ def _configure_params(self): self.add_design_var(var_name) if options['static_target']: - src_idxs = None + src_idxs = om.slicer[0, ...] else: src_rows = np.zeros(vec_size, dtype=int) src_idxs = om.slicer[src_rows, ...] diff --git a/dymos/transcriptions/explicit_shooting/ode_integration_comp.py b/dymos/transcriptions/explicit_shooting/ode_integration_comp.py index 53ff9c388..43ace34f0 100644 --- a/dymos/transcriptions/explicit_shooting/ode_integration_comp.py +++ b/dymos/transcriptions/explicit_shooting/ode_integration_comp.py @@ -2,7 +2,7 @@ import openmdao.api as om from scipy.integrate import solve_ivp -from ...options import options as dymos_options +from ..._options import options as dymos_options from .ode_evaluation_group import ODEEvaluationGroup diff --git a/dymos/transcriptions/explicit_shooting/state_rate_collector_comp.py b/dymos/transcriptions/explicit_shooting/state_rate_collector_comp.py index 859a5b918..55f864787 100644 --- a/dymos/transcriptions/explicit_shooting/state_rate_collector_comp.py +++ b/dymos/transcriptions/explicit_shooting/state_rate_collector_comp.py @@ -3,7 +3,7 @@ import openmdao.api as om from dymos.utils.misc import get_rate_units -from dymos.options import options as dymos_options +from ..._options import options as dymos_options class StateRateCollectorComp(om.ExplicitComponent): diff --git a/dymos/transcriptions/explicit_shooting/tau_comp.py b/dymos/transcriptions/explicit_shooting/tau_comp.py index b42286143..d8a6425b6 100644 --- a/dymos/transcriptions/explicit_shooting/tau_comp.py +++ b/dymos/transcriptions/explicit_shooting/tau_comp.py @@ -2,7 +2,7 @@ import openmdao.api as om -from ...options import options as dymos_options +from ..._options import options as dymos_options class TauComp(om.ExplicitComponent): diff --git a/dymos/transcriptions/pseudospectral/components/collocation_comp.py b/dymos/transcriptions/pseudospectral/components/collocation_comp.py index d43f6bbb3..bfa9dffc5 100644 --- a/dymos/transcriptions/pseudospectral/components/collocation_comp.py +++ b/dymos/transcriptions/pseudospectral/components/collocation_comp.py @@ -5,7 +5,7 @@ from ...grid_data import GridData from ....utils.misc import get_rate_units -from ....options import options as dymos_options +from ...._options import options as dymos_options class CollocationComp(om.ExplicitComponent): diff --git a/dymos/transcriptions/pseudospectral/components/control_endpoint_defect_comp.py b/dymos/transcriptions/pseudospectral/components/control_endpoint_defect_comp.py index 5b714e2c2..c67f97ae2 100644 --- a/dymos/transcriptions/pseudospectral/components/control_endpoint_defect_comp.py +++ b/dymos/transcriptions/pseudospectral/components/control_endpoint_defect_comp.py @@ -1,7 +1,7 @@ import numpy as np import openmdao.api as om from ...grid_data import GridData -from ....options import options as dymos_options +from ...._options import options as dymos_options class ControlEndpointDefectComp(om.ExplicitComponent): diff --git a/dymos/transcriptions/pseudospectral/components/gauss_lobatto_interleave_comp.py b/dymos/transcriptions/pseudospectral/components/gauss_lobatto_interleave_comp.py index 9574b4b3a..745e6ccaa 100644 --- a/dymos/transcriptions/pseudospectral/components/gauss_lobatto_interleave_comp.py +++ b/dymos/transcriptions/pseudospectral/components/gauss_lobatto_interleave_comp.py @@ -3,7 +3,7 @@ from openmdao.utils.units import unit_conversion from ...grid_data import GridData -from ....options import options as dymos_options +from ...._options import options as dymos_options class GaussLobattoInterleaveComp(om.ExplicitComponent): diff --git a/dymos/transcriptions/pseudospectral/components/state_independents.py b/dymos/transcriptions/pseudospectral/components/state_independents.py index d79efff22..45a8ffb3f 100644 --- a/dymos/transcriptions/pseudospectral/components/state_independents.py +++ b/dymos/transcriptions/pseudospectral/components/state_independents.py @@ -5,7 +5,7 @@ import openmdao.api as om from ....transcriptions.grid_data import GridData -from ....options import options as dymos_options +from ...._options import options as dymos_options class StateIndependentsComp(om.ImplicitComponent): diff --git a/dymos/transcriptions/pseudospectral/components/state_interp_comp.py b/dymos/transcriptions/pseudospectral/components/state_interp_comp.py index 3fd80b12a..4704a3b59 100644 --- a/dymos/transcriptions/pseudospectral/components/state_interp_comp.py +++ b/dymos/transcriptions/pseudospectral/components/state_interp_comp.py @@ -3,7 +3,7 @@ import openmdao.api as om from ...grid_data import GridData from ....utils.misc import get_rate_units -from ....options import options as dymos_options +from ...._options import options as dymos_options class StateInterpComp(om.ExplicitComponent): diff --git a/dymos/transcriptions/solve_ivp/components/state_rate_collector_comp.py b/dymos/transcriptions/solve_ivp/components/state_rate_collector_comp.py index 36ab5d573..4c96bccaa 100644 --- a/dymos/transcriptions/solve_ivp/components/state_rate_collector_comp.py +++ b/dymos/transcriptions/solve_ivp/components/state_rate_collector_comp.py @@ -1,6 +1,6 @@ import numpy as np from ....utils.misc import get_rate_units -from ....options import options as dymos_options +from ...._options import options as dymos_options import openmdao.api as om diff --git a/dymos/transcriptions/transcription_base.py b/dymos/transcriptions/transcription_base.py index 3705579d2..8cab18a2c 100644 --- a/dymos/transcriptions/transcription_base.py +++ b/dymos/transcriptions/transcription_base.py @@ -10,7 +10,7 @@ from ..utils.misc import _unspecified from ..utils.introspection import configure_states_introspection, get_promoted_vars, get_target_metadata, \ configure_states_discovery -from ..options import options as dymos_options +from .._options import options as dymos_options class TranscriptionBase(object): diff --git a/dymos/utils/introspection.py b/dymos/utils/introspection.py index c34de8a11..e9b2cf2a5 100644 --- a/dymos/utils/introspection.py +++ b/dymos/utils/introspection.py @@ -6,7 +6,7 @@ import numpy as np from openmdao.utils.array_utils import shape_to_len from dymos.utils.misc import _unspecified -from dymos.options import options as dymos_options +from .._options import options as dymos_options from ..phase.options import StateOptionsDictionary, TimeseriesOutputOptionsDictionary from .misc import get_rate_units @@ -777,7 +777,9 @@ def configure_timeseries_output_introspection(phase): not_found = set() - for name, output_options in ts_opts['outputs'].items(): + for output_name, output_options in ts_opts['outputs'].items(): + name = output_options['name'] + print(phase.pathname, name, output_name) if output_options['is_expr']: output_meta = phase.timeseries_ec_vars[ts_name][output_name]['meta_data'] else: @@ -786,7 +788,7 @@ def configure_timeseries_output_introspection(phase): output_options['output_name'], phase=phase) except ValueError as e: - not_found.add(name) + not_found.add(output_name) continue output_options['src'] = output_meta['src'] @@ -799,7 +801,7 @@ def configure_timeseries_output_introspection(phase): output_options['units'] = output_meta['units'] if not_found: - sorted_list = ', '.join(sorted(not_found)) + sorted_list = ', '.join(sorted([ts_opts['outputs'][output_name]['name'] for output_name in not_found])) om.issue_warning(f'{phase.pathname}: The following timeseries outputs were requested but not found in the ' f'ODE: {sorted_list}') diff --git a/dymos/visualization/timeseries/bokeh_timeseries_report.py b/dymos/visualization/timeseries/bokeh_timeseries_report.py index 85c06c467..03d373c4e 100644 --- a/dymos/visualization/timeseries/bokeh_timeseries_report.py +++ b/dymos/visualization/timeseries/bokeh_timeseries_report.py @@ -1,12 +1,17 @@ import datetime from pathlib import Path -from bokeh.io import output_notebook, output_file, save, show -from bokeh.layouts import column, grid, row -from bokeh.models import Legend, DataTable, Div, ColumnDataSource, TableColumn, TabPanel, Tabs, CheckboxButtonGroup,\ - CustomJS, MultiChoice -from bokeh.plotting import figure, curdoc -import bokeh.palettes as bp +try: + from bokeh.io import output_notebook, output_file, save, show + from bokeh.layouts import column, grid, row + from bokeh.models import Legend, DataTable, Div, ColumnDataSource, TableColumn, TabPanel, Tabs,\ + CheckboxButtonGroup, CustomJS, MultiChoice + from bokeh.plotting import figure, curdoc + import bokeh.palettes as bp + import bokeh.resources as bokeh_resources + _NO_BOKEH = False +except ImportError: + _NO_BOKEH = True import openmdao.api as om import dymos as dm @@ -210,7 +215,8 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi source_data = _load_data_sources(prob, solution_record_file, simulation_record_file) # Colors of each phase in the plot. Start with the bright colors followed by the faded ones. - colors = bp.d3['Category20'][20][0::2] + bp.d3['Category20'][20][1::2] + if not _NO_BOKEH: + colors = bp.d3['Category20'][20][0::2] + bp.d3['Category20'][20][1::2] curdoc().theme = theme @@ -218,6 +224,12 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi traj_name = traj.pathname.split('.')[-1] report_filename = f'{traj.pathname}_results_report.html' report_path = str(Path(prob.get_reports_dir()) / report_filename) + if _NO_BOKEH: + with open(report_path) as f: + write("\n\n \nError: bokeh not available\n \n" + "This report requires bokeh but bokeh was not available in this python installation.\n" + "") + continue param_tables = [] phase_names = [] @@ -336,6 +348,7 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi # Save - save(report_layout, filename=report_path, title=f'trajectory results for {traj_name}') + save(report_layout, filename=report_path, title=f'trajectory results for {traj_name}', + resources=bokeh_resources.INLINE) diff --git a/dymos/visualization/timeseries_plots.py b/dymos/visualization/timeseries_plots.py index be9bf3884..5c3d680e1 100644 --- a/dymos/visualization/timeseries_plots.py +++ b/dymos/visualization/timeseries_plots.py @@ -10,7 +10,7 @@ import matplotlib.patches as mpatches import openmdao.api as om -from dymos.options import options as dymos_options +from .._options import options as dymos_options def _get_phases_node_in_problem_metadata(node, path=""): From 439502074bab3822ebcd32b3282bd7e2a3f1b902 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Fri, 24 Mar 2023 16:57:17 -0400 Subject: [PATCH 15/35] Fixed a bug in the names of the ExplicitShooting timeseries outputs --- dymos/_options.py | 2 +- .../balanced_field/doc/test_doc_balanced_field_length.py | 2 +- dymos/transcriptions/explicit_shooting/explicit_shooting.py | 4 ++-- dymos/utils/introspection.py | 1 - 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/dymos/_options.py b/dymos/_options.py index 382adc6ad..87354e66d 100644 --- a/dymos/_options.py +++ b/dymos/_options.py @@ -17,6 +17,6 @@ options.declare('notebook_mode', default=False, types=bool, desc='If True, provide notebook-enhanced plots and outputs.') -options.declare('use_timeseries_prefix', default=False, types=bool, +options.declare('use_timeseries_prefix', default=True, types=bool, desc='If True, prefix timeseries outputs with the variable type for states, times, controls,' 'and parameters.') diff --git a/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py b/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py index f29eff5b8..1d5745254 100644 --- a/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py +++ b/dymos/examples/balanced_field/doc/test_doc_balanced_field_length.py @@ -6,7 +6,7 @@ SHOW_PLOTS = True -# @use_tempdirs +@use_tempdirs class TestBalancedFieldLengthForDocs(unittest.TestCase): @require_pyoptsparse(optimizer='IPOPT') diff --git a/dymos/transcriptions/explicit_shooting/explicit_shooting.py b/dymos/transcriptions/explicit_shooting/explicit_shooting.py index b144f43e2..1ba8ed172 100644 --- a/dymos/transcriptions/explicit_shooting/explicit_shooting.py +++ b/dymos/transcriptions/explicit_shooting/explicit_shooting.py @@ -454,8 +454,8 @@ def setup_polynomial_controls(self, phase): phase.add_subsystem('polynomial_control_group', subsys=sys, promotes_inputs=['*'], promotes_outputs=['*']) - control_prefix = 'controls:' if dymos_options['use_timeseries_prefix'] else '' - control_rate_prefix = 'control_rates:' if dymos_options['use_timeseries_prefix'] else '' + control_prefix = 'polynomial_controls:' if dymos_options['use_timeseries_prefix'] else '' + control_rate_prefix = 'polynomial_control_rates:' if dymos_options['use_timeseries_prefix'] else '' for name, options in phase.polynomial_control_options.items(): diff --git a/dymos/utils/introspection.py b/dymos/utils/introspection.py index e9b2cf2a5..804b6b33a 100644 --- a/dymos/utils/introspection.py +++ b/dymos/utils/introspection.py @@ -779,7 +779,6 @@ def configure_timeseries_output_introspection(phase): for output_name, output_options in ts_opts['outputs'].items(): name = output_options['name'] - print(phase.pathname, name, output_name) if output_options['is_expr']: output_meta = phase.timeseries_ec_vars[ts_name][output_name]['meta_data'] else: From be9bc1eeabd29a2515410ec7f85e852285e194c6 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Fri, 24 Mar 2023 19:07:37 -0400 Subject: [PATCH 16/35] timeseries results report causing an error when bokeh is unavailable. --- dymos/visualization/timeseries/bokeh_timeseries_report.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dymos/visualization/timeseries/bokeh_timeseries_report.py b/dymos/visualization/timeseries/bokeh_timeseries_report.py index 03d373c4e..3a1c25adf 100644 --- a/dymos/visualization/timeseries/bokeh_timeseries_report.py +++ b/dymos/visualization/timeseries/bokeh_timeseries_report.py @@ -217,8 +217,7 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi # Colors of each phase in the plot. Start with the bright colors followed by the faded ones. if not _NO_BOKEH: colors = bp.d3['Category20'][20][0::2] + bp.d3['Category20'][20][1::2] - - curdoc().theme = theme + curdoc().theme = theme for traj in prob.model.system_iter(include_self=True, recurse=True, typ=dm.Trajectory): traj_name = traj.pathname.split('.')[-1] From 951805bc56a296e964aae863289f2ef8ba5fd43e Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Sat, 25 Mar 2023 08:43:54 -0400 Subject: [PATCH 17/35] Updated docs workflow to more reliably provide the doc build reports. Fixed a bug in ExplicitShooting when connecting the controls to the continuity comp. Removed some commented lines from introspection.py. --- .github/workflows/dymos_docs_workflow.yml | 11 +++++------ .../explicit_shooting/explicit_shooting.py | 2 +- dymos/utils/introspection.py | 16 ---------------- 3 files changed, 6 insertions(+), 23 deletions(-) diff --git a/.github/workflows/dymos_docs_workflow.yml b/.github/workflows/dymos_docs_workflow.yml index 27ebd5ac7..ba0e165b3 100644 --- a/.github/workflows/dymos_docs_workflow.yml +++ b/.github/workflows/dymos_docs_workflow.yml @@ -237,12 +237,11 @@ jobs: continue-on-error: True if: failure() && steps.build_docs.outcome == 'failure' run: | - for f in /home/runner/work/dymos/dymos/docs/dymos_book/_build/html/reports/*; do - echo "=============================================================" - echo $f - echo "=============================================================" - cat $f - done + find dymos_book/_build/html/reports/ -type f -name '*.log' \ + -exec echo "#################################################################" \; \ + -exec echo {} \; \ + -exec echo "#################################################################" \; \ + -exec cat {} \; - name: Publish docs if: | diff --git a/dymos/transcriptions/explicit_shooting/explicit_shooting.py b/dymos/transcriptions/explicit_shooting/explicit_shooting.py index 1ba8ed172..b158fe35e 100644 --- a/dymos/transcriptions/explicit_shooting/explicit_shooting.py +++ b/dymos/transcriptions/explicit_shooting/explicit_shooting.py @@ -585,7 +585,7 @@ def configure_defects(self, phase): if options['continuity'] and any_control_cnty: controls_to_enforce.add(control_name) - phase.connect(f'controls:{control_name}', + phase.connect(f'control_values:{control_name}', f'continuity_comp.controls:{control_name}', src_indices=src_idxs) if options['rate_continuity'] and any_rate_cnty: diff --git a/dymos/utils/introspection.py b/dymos/utils/introspection.py index 804b6b33a..1bc1776af 100644 --- a/dymos/utils/introspection.py +++ b/dymos/utils/introspection.py @@ -230,15 +230,6 @@ def _configure_constraint_introspection(phase): con['units'] = control_units if con['units'] is None else con['units'] con['constraint_path'] = f'timeseries.{prefix}{var}' - # elif var_type == 'input_control': - # prefix = 'controls:' if dymos_options['use_timeseries_prefix'] else '' - # control_shape = phase.control_options[var]['shape'] - # control_units = phase.control_options[var]['units'] - # - # con['shape'] = control_shape - # con['units'] = control_units if con['units'] is None else con['units'] - # con['constraint_path'] = f'timeseries.{prefix}{var}' - elif var_type in ['indep_polynomial_control', 'input_polynomial_control']: prefix = 'polynomial_controls:' if dymos_options['use_timeseries_prefix'] else '' control_shape = phase.polynomial_control_options[var]['shape'] @@ -247,13 +238,6 @@ def _configure_constraint_introspection(phase): con['units'] = control_units if con['units'] is None else con['units'] con['constraint_path'] = f'timeseries.{prefix}{var}' - # elif var_type == 'input_polynomial_control': - # control_shape = phase.polynomial_control_options[var]['shape'] - # control_units = phase.polynomial_control_options[var]['units'] - # con['shape'] = control_shape - # con['units'] = control_units if con['units'] is None else con['units'] - # con['constraint_path'] = f'timeseries.polynomial_controls:{var}' - elif var_type == 'control_rate': prefix = 'control_rates:' if dymos_options['use_timeseries_prefix'] else '' control_name = var[:-5] From 76e9eaafd743a4c47eb216b009eede843fd0253b Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Sat, 25 Mar 2023 13:16:57 -0400 Subject: [PATCH 18/35] state rates, time_phase, and control rates all added back to the recorders for grid refinement, for now. --- .github/workflows/dymos_docs_workflow.yml | 2 ++ docs/dymos_book/examples/mountain_car/mountain_car.ipynb | 2 +- dymos/phase/options.py | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dymos_docs_workflow.yml b/.github/workflows/dymos_docs_workflow.yml index ba0e165b3..0cfc6135f 100644 --- a/.github/workflows/dymos_docs_workflow.yml +++ b/.github/workflows/dymos_docs_workflow.yml @@ -237,6 +237,8 @@ jobs: continue-on-error: True if: failure() && steps.build_docs.outcome == 'failure' run: | + echo $PWD + cd docs find dymos_book/_build/html/reports/ -type f -name '*.log' \ -exec echo "#################################################################" \; \ -exec echo {} \; \ diff --git a/docs/dymos_book/examples/mountain_car/mountain_car.ipynb b/docs/dymos_book/examples/mountain_car/mountain_car.ipynb index c02e1dc33..600a779e1 100644 --- a/docs/dymos_book/examples/mountain_car/mountain_car.ipynb +++ b/docs/dymos_book/examples/mountain_car/mountain_car.ipynb @@ -467,7 +467,7 @@ "p['traj.phase0.t_duration'] = 500.0\n", "\n", "p.set_val('traj.phase0.states:x', phase.interp('x', ys=[-0.5, 0.5]))\n", - "p.set_val('traj.phase0.states:v', phase.interp('v', ys=[0, 1.0]))\n", + "p.set_val('traj.phase0.states:v', phase.interp('v', ys=[0, 0.07]))\n", "p.set_val('traj.phase0.controls:u', np.sin(phase.interp('u', ys=[0, 1.0])))\n", "\n", "#\n", diff --git a/dymos/phase/options.py b/dymos/phase/options.py index 24493aa64..f476b3fe9 100644 --- a/dymos/phase/options.py +++ b/dymos/phase/options.py @@ -756,8 +756,8 @@ def __init__(self, read_only=False): self.declare(name='include_state_rates', types=bool, default=True, desc='If True, include state rates in the timeseries outputs by default.') - self.declare(name='include_control_rates', types=bool, default=False, + self.declare(name='include_control_rates', types=bool, default=True, desc='If True, include control rates in the timeseries outputs by default.') - self.declare(name='include_t_phase', types=bool, default=False, + self.declare(name='include_t_phase', types=bool, default=True, desc='If True, include the elapsed phase time in the timeseries outputs by default.') From 1fcc7eb7010cdcc790955435d8bc24eafc5b690d Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Sat, 25 Mar 2023 13:56:41 -0400 Subject: [PATCH 19/35] Grid refinement no longer depends on the presense of time_phase in the recorded data. --- dymos/grid_refinement/error_estimation.py | 10 +++++----- dymos/phase/options.py | 6 +++--- dymos/transcriptions/transcription_base.py | 11 ++++++----- .../timeseries/bokeh_timeseries_report.py | 8 ++++---- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/dymos/grid_refinement/error_estimation.py b/dymos/grid_refinement/error_estimation.py index dd5d421da..49176cf01 100644 --- a/dymos/grid_refinement/error_estimation.py +++ b/dymos/grid_refinement/error_estimation.py @@ -148,7 +148,7 @@ def eval_ode_on_grid(phase, transcription): t_phase_name = f'{t_name}_phase' t_prev = phase.get_val(f'timeseries.{t_name}', units=phase.time_options['units']) - t_phase_prev = phase.get_val(f'timeseries.{t_phase_name}', units=phase.time_options['units']) + t_phase_prev = t_prev - t_prev[0] t_initial = np.repeat(t_prev[0, 0], repeats=transcription.grid_data.num_nodes, axis=0) t_duration = np.repeat(t_prev[-1, 0], repeats=transcription.grid_data.num_nodes, axis=0) t = np.dot(L, t_prev) @@ -183,14 +183,14 @@ def eval_ode_on_grid(phase, transcription): if targets: p_refine.set_val(f'controls:{name}', u[name]) - u_rate_prev = phase.get_val(f'timeseries.control_rates:{name}_rate') - u_rate[name] = np.dot(L, u_rate_prev) if rate_targets: + u_rate_prev = phase.get_val(f'timeseries.control_rates:{name}_rate') + u_rate[name] = np.dot(L, u_rate_prev) p_refine.set_val(f'control_rates:{name}_rate', u_rate[name]) - u_rate2_prev = phase.get_val(f'timeseries.control_rates:{name}_rate2') - u_rate2[name] = np.dot(L, u_rate2_prev) if rate2_targets: + u_rate2_prev = phase.get_val(f'timeseries.control_rates:{name}_rate2') + u_rate2[name] = np.dot(L, u_rate2_prev) p_refine.set_val(f'control_rates:{name}_rate2', u_rate2[name]) for name, options in phase.polynomial_control_options.items(): diff --git a/dymos/phase/options.py b/dymos/phase/options.py index f476b3fe9..50f8ceb06 100644 --- a/dymos/phase/options.py +++ b/dymos/phase/options.py @@ -753,11 +753,11 @@ class PhaseTimeseriesOptionsDictionary(om.OptionsDictionary): def __init__(self, read_only=False): super().__init__(read_only) - self.declare(name='include_state_rates', types=bool, default=True, + self.declare(name='include_state_rates', types=bool, default=False, desc='If True, include state rates in the timeseries outputs by default.') - self.declare(name='include_control_rates', types=bool, default=True, + self.declare(name='include_control_rates', types=bool, default=False, desc='If True, include control rates in the timeseries outputs by default.') - self.declare(name='include_t_phase', types=bool, default=True, + self.declare(name='include_t_phase', types=bool, default=False, desc='If True, include the elapsed phase time in the timeseries outputs by default.') diff --git a/dymos/transcriptions/transcription_base.py b/dymos/transcriptions/transcription_base.py index 8cab18a2c..8e8e5fce5 100644 --- a/dymos/transcriptions/transcription_base.py +++ b/dymos/transcriptions/transcription_base.py @@ -87,7 +87,8 @@ def setup_time(self, phase): for ts_name, ts_options in phase._timeseries.items(): if t_name not in ts_options['outputs']: phase.add_timeseries_output(t_name, timeseries=ts_name) - if t_phase_name not in ts_options['outputs'] and phase.timeseries_options['include_t_phase']: + if t_phase_name not in ts_options['outputs'] and \ + (phase.timeseries_options['include_t_phase'] or time_options['time_phase_targets']): phase.add_timeseries_output(t_phase_name, timeseries=ts_name) def configure_time(self, phase): @@ -179,11 +180,11 @@ def setup_controls(self, phase): phase.add_timeseries_output(name, output_name=f'{control_prefix}{name}', timeseries=ts_name) if f'{control_rate_prefix}{name}_rate' not in ts_options['outputs'] and \ - phase.timeseries_options['include_control_rates']: + (phase.timeseries_options['include_control_rates'] or options['rate_targets']): phase.add_timeseries_output(f'{name}_rate', output_name=f'{control_rate_prefix}{name}_rate', timeseries=ts_name) if f'{control_rate_prefix}{name}_rate2' not in ts_options['outputs'] and \ - phase.timeseries_options['include_control_rates']: + (phase.timeseries_options['include_control_rates'] or options['rate2_targets']): phase.add_timeseries_output(f'{name}_rate2', output_name=f'{control_rate_prefix}{name}_rate2', timeseries=ts_name) @@ -223,11 +224,11 @@ def setup_polynomial_controls(self, phase): phase.add_timeseries_output(name, output_name=f'{prefix}{name}', timeseries=ts_name) if f'polynomial_control_rates:{name}_rate' not in ts_options['outputs'] and \ - phase.timeseries_options['include_control_rates']: + (phase.timeseries_options['include_control_rates'] or options['rate_targets']): phase.add_timeseries_output(f'{name}_rate', output_name=f'{rate_prefix}{name}_rate', timeseries=ts_name) if f'polynomial_control_rates:{name}_rate2' not in ts_options['outputs'] and \ - phase.timeseries_options['include_control_rates']: + (phase.timeseries_options['include_control_rates'] or options['rate2_targets']): phase.add_timeseries_output(f'{name}_rate2', output_name=f'{rate_prefix}{name}_rate2', timeseries=ts_name) diff --git a/dymos/visualization/timeseries/bokeh_timeseries_report.py b/dymos/visualization/timeseries/bokeh_timeseries_report.py index 3a1c25adf..43e85a31e 100644 --- a/dymos/visualization/timeseries/bokeh_timeseries_report.py +++ b/dymos/visualization/timeseries/bokeh_timeseries_report.py @@ -224,10 +224,10 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi report_filename = f'{traj.pathname}_results_report.html' report_path = str(Path(prob.get_reports_dir()) / report_filename) if _NO_BOKEH: - with open(report_path) as f: - write("\n\n \nError: bokeh not available\n \n" - "This report requires bokeh but bokeh was not available in this python installation.\n" - "") + with open(report_path, 'wb') as f: + f.write("\n\n \nError: bokeh not available\n \n" + "This report requires bokeh but bokeh was not available in this python installation.\n" + "") continue param_tables = [] From dcfc18c58ed45a0c7962bf4f4a49c7ace514cbb9 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Sat, 25 Mar 2023 14:06:53 -0400 Subject: [PATCH 20/35] Do not try to make the trajectory results report if the reports directory is non existent. --- dymos/visualization/timeseries/bokeh_timeseries_report.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dymos/visualization/timeseries/bokeh_timeseries_report.py b/dymos/visualization/timeseries/bokeh_timeseries_report.py index 43e85a31e..32df2f74d 100644 --- a/dymos/visualization/timeseries/bokeh_timeseries_report.py +++ b/dymos/visualization/timeseries/bokeh_timeseries_report.py @@ -1,5 +1,6 @@ import datetime from pathlib import Path +import os.path try: from bokeh.io import output_notebook, output_file, save, show @@ -223,6 +224,8 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi traj_name = traj.pathname.split('.')[-1] report_filename = f'{traj.pathname}_results_report.html' report_path = str(Path(prob.get_reports_dir()) / report_filename) + if os.path.isdir(prob.get_reports_dir()): + om.issue_warning(f'Reports directory not available. {report_path} will not be created.') if _NO_BOKEH: with open(report_path, 'wb') as f: f.write("\n\n \nError: bokeh not available\n \n" From e6ce05aeb05a3a3361618e5cdadb5836e3b3369f Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Sat, 25 Mar 2023 14:24:44 -0400 Subject: [PATCH 21/35] properly handle missing report path --- joss/test/test_cannonball_for_joss.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/joss/test/test_cannonball_for_joss.py b/joss/test/test_cannonball_for_joss.py index df6bbc9cf..e31f03dc4 100644 --- a/joss/test/test_cannonball_for_joss.py +++ b/joss/test/test_cannonball_for_joss.py @@ -3,7 +3,7 @@ from openmdao.utils.assert_utils import assert_near_equal -@use_tempdirs +# @use_tempdirs class TestCannonballForJOSS(unittest.TestCase): @require_pyoptsparse(optimizer='SLSQP') @@ -190,7 +190,7 @@ def compute(self, inputs, outputs): # maximize range descent.add_objective('r', loc='final', ref=-1.0) - p.driver = om.pyOptSparseDriver() + p.driver = om.pyOptSparseDriver(print_results=False) p.driver.options['optimizer'] = 'SLSQP' p.driver.declare_coloring() From 022dddc4db503742f0c362e6b2079b627cb4f2f2 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Sat, 25 Mar 2023 14:34:46 -0400 Subject: [PATCH 22/35] properly handle missing report path --- dymos/visualization/timeseries/bokeh_timeseries_report.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dymos/visualization/timeseries/bokeh_timeseries_report.py b/dymos/visualization/timeseries/bokeh_timeseries_report.py index 32df2f74d..ff0e30f21 100644 --- a/dymos/visualization/timeseries/bokeh_timeseries_report.py +++ b/dymos/visualization/timeseries/bokeh_timeseries_report.py @@ -223,9 +223,11 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi for traj in prob.model.system_iter(include_self=True, recurse=True, typ=dm.Trajectory): traj_name = traj.pathname.split('.')[-1] report_filename = f'{traj.pathname}_results_report.html' - report_path = str(Path(prob.get_reports_dir()) / report_filename) - if os.path.isdir(prob.get_reports_dir()): + report_dir = Path(prob.get_reports_dir()) + report_path = report_dir / report_filename + if not os.path.isdir(report_dir): om.issue_warning(f'Reports directory not available. {report_path} will not be created.') + continue if _NO_BOKEH: with open(report_path, 'wb') as f: f.write("\n\n \nError: bokeh not available\n \n" From e3811a883f58a5225321e11457d75afd162e8a4b Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Sun, 26 Mar 2023 08:42:50 -0400 Subject: [PATCH 23/35] cleanup --- benchmark/benchmark_brachistochrone.py | 2 +- dymos/_options.py | 2 +- .../test/test_balanced_field_length.py | 1 + ...test_brachistochrone_callable_ode_class.py | 4 +- ...chrone_varying_order_control_simulation.py | 4 +- .../test/test_ex_brachistochrone.py | 2 +- .../test_ex_brachistochrone_refine_grid.py | 2 +- .../test_ex_brachistochrone_vector_states.py | 2 +- .../test/test_timeseries_units.py | 4 +- .../test/test_connect_control_to_parameter.py | 2 +- ...two_phase_cannonball_ode_output_linkage.py | 70 +----- ...test_double_integrator_timeseries_units.py | 2 + ...est_ex_two_burn_orbit_raise_bokeh_plots.py | 21 +- dymos/grid_refinement/error_estimation.py | 2 +- dymos/phase/test/test_analytic_phase.py | 2 +- dymos/phase/test/test_phase.py | 8 +- dymos/phase/test/test_timeseries.py | 19 +- dymos/run_problem.py | 13 +- dymos/test/test_run_problem.py | 43 ++-- dymos/test/test_upgrade_guide.py | 7 +- dymos/trajectory/test/test_trajectory.py | 7 +- dymos/trajectory/trajectory.py | 2 +- .../test/test_timeseries_plots.py | 59 ++++- dymos/visualization/timeseries_plots.py | 217 ++++++++---------- 24 files changed, 243 insertions(+), 254 deletions(-) diff --git a/benchmark/benchmark_brachistochrone.py b/benchmark/benchmark_brachistochrone.py index 7141b9f06..4b3c3786c 100644 --- a/benchmark/benchmark_brachistochrone.py +++ b/benchmark/benchmark_brachistochrone.py @@ -88,7 +88,7 @@ def run_asserts(self, p): v0 = p.get_val('phase0.timeseries.states:v')[0] vf = p.get_val('phase0.timeseries.states:v')[-1] - g = p.get_val('phase0.timeseries.parameters:g')[0] + g = p.get_val('phase0.parameter_vals:g')[0] thetaf = p.get_val('phase0.timeseries.controls:theta')[-1] diff --git a/dymos/_options.py b/dymos/_options.py index 87354e66d..c67955e7d 100644 --- a/dymos/_options.py +++ b/dymos/_options.py @@ -11,7 +11,7 @@ options.declare('include_check_partials', default=_icp_default, types=bool, desc='If True, include dymos components when checking partials.') -options.declare('plots', default='matplotlib', values=['matplotlib', 'bokeh'], +options.declare('plots', default='bokeh', values=['matplotlib', 'bokeh'], desc='The plot library used to generate output plots for Dymos.') options.declare('notebook_mode', default=False, types=bool, diff --git a/dymos/examples/balanced_field/test/test_balanced_field_length.py b/dymos/examples/balanced_field/test/test_balanced_field_length.py index 821453e87..c2e83d45c 100644 --- a/dymos/examples/balanced_field/test/test_balanced_field_length.py +++ b/dymos/examples/balanced_field/test/test_balanced_field_length.py @@ -18,6 +18,7 @@ def _make_problem(self): p.driver = om.pyOptSparseDriver() p.driver.options['optimizer'] = 'IPOPT' + p.driver.options['invalid_desvar_behavior'] = 'ignore' p.driver.opt_settings['print_level'] = 0 p.driver.opt_settings['derivative_test'] = 'first-order' diff --git a/dymos/examples/brachistochrone/test/test_brachistochrone_callable_ode_class.py b/dymos/examples/brachistochrone/test/test_brachistochrone_callable_ode_class.py index e8d35c6af..e60ac2627 100644 --- a/dymos/examples/brachistochrone/test/test_brachistochrone_callable_ode_class.py +++ b/dymos/examples/brachistochrone/test/test_brachistochrone_callable_ode_class.py @@ -106,7 +106,7 @@ def run_asserts(self): v0 = p.get_val('traj0.phase0.timeseries.states:v')[0] vf = p.get_val('traj0.phase0.timeseries.states:v')[-1] - g = p.get_val('traj0.phase0.timeseries.parameters:g')[0] + g = p.get_val('traj0.phase0.parameter_vals:g')[0] thetaf = p.get_val('traj0.phase0.timeseries.controls:theta')[-1] @@ -317,7 +317,7 @@ def run_asserts(self): v0 = p.get_val('traj0.phase0.timeseries.states:v')[0] vf = p.get_val('traj0.phase0.timeseries.states:v')[-1] - g = p.get_val('traj0.phase0.timeseries.parameters:g')[0] + g = p.get_val('traj0.phase0.parameter_vals:g')[0] thetaf = p.get_val('traj0.phase0.timeseries.controls:theta')[-1] diff --git a/dymos/examples/brachistochrone/test/test_brachistochrone_varying_order_control_simulation.py b/dymos/examples/brachistochrone/test/test_brachistochrone_varying_order_control_simulation.py index b92643f39..f37d69043 100644 --- a/dymos/examples/brachistochrone/test/test_brachistochrone_varying_order_control_simulation.py +++ b/dymos/examples/brachistochrone/test/test_brachistochrone_varying_order_control_simulation.py @@ -74,7 +74,7 @@ def test_gauss_lobatto(self): v0 = p.get_val('phase0.timeseries.states:v')[0] vf = p.get_val('phase0.timeseries.states:v')[-1] - g = p.get_val('phase0.timeseries.parameters:g')[0] + g = p.get_val('phase0.parameter_vals:g')[0] thetaf = exp_out.get_val('phase0.timeseries.controls:theta')[-1] @@ -151,7 +151,7 @@ def test_radau(self): v0 = p.get_val('phase0.timeseries.states:v')[0] vf = p.get_val('phase0.timeseries.states:v')[-1] - g = p.get_val('phase0.timeseries.parameters:g')[0] + g = p.get_val('phase0.parameter_vals:g')[0] thetaf = exp_out.get_val('phase0.timeseries.controls:theta')[-1] diff --git a/dymos/examples/brachistochrone/test/test_ex_brachistochrone.py b/dymos/examples/brachistochrone/test/test_ex_brachistochrone.py index 135543032..ce4a1f23f 100644 --- a/dymos/examples/brachistochrone/test/test_ex_brachistochrone.py +++ b/dymos/examples/brachistochrone/test/test_ex_brachistochrone.py @@ -32,7 +32,7 @@ def run_asserts(self, p): v0 = p.get_val('traj0.phase0.timeseries.states:v')[0] vf = p.get_val('traj0.phase0.timeseries.states:v')[-1] - g = p.get_val('traj0.phase0.timeseries.parameters:g')[0] + g = p.get_val('traj0.phase0.parameter_vals:g')[0] thetaf = p.get_val('traj0.phase0.timeseries.controls:theta')[-1] diff --git a/dymos/examples/brachistochrone/test/test_ex_brachistochrone_refine_grid.py b/dymos/examples/brachistochrone/test/test_ex_brachistochrone_refine_grid.py index 367b164b6..ff37ea1d8 100644 --- a/dymos/examples/brachistochrone/test/test_ex_brachistochrone_refine_grid.py +++ b/dymos/examples/brachistochrone/test/test_ex_brachistochrone_refine_grid.py @@ -85,7 +85,7 @@ def run_asserts(self, p): v0 = p.get_val('traj0.phase0.timeseries.states:v')[0] vf = p.get_val('traj0.phase0.timeseries.states:v')[-1] - g = p.get_val('traj0.phase0.timeseries.parameters:g')[0] + g = p.get_val('traj0.phase0.parameter_vals:g')[0] thetaf = p.get_val('traj0.phase0.timeseries.controls:theta')[-1] diff --git a/dymos/examples/brachistochrone/test/test_ex_brachistochrone_vector_states.py b/dymos/examples/brachistochrone/test/test_ex_brachistochrone_vector_states.py index bff4dc0ab..43a832771 100644 --- a/dymos/examples/brachistochrone/test/test_ex_brachistochrone_vector_states.py +++ b/dymos/examples/brachistochrone/test/test_ex_brachistochrone_vector_states.py @@ -26,7 +26,7 @@ def assert_results(self, p): v0 = p.get_val('traj0.phase0.timeseries.states:v')[0, 0] vf = p.get_val('traj0.phase0.timeseries.states:v')[-1, 0] - g = p.get_val('traj0.phase0.timeseries.parameters:g') + g = p.get_val('traj0.phase0.parameter_vals:g') thetaf = p.get_val('traj0.phase0.timeseries.controls:theta')[-1, 0] diff --git a/dymos/examples/brachistochrone/test/test_timeseries_units.py b/dymos/examples/brachistochrone/test/test_timeseries_units.py index f5ff1660f..17b37e61c 100644 --- a/dymos/examples/brachistochrone/test/test_timeseries_units.py +++ b/dymos/examples/brachistochrone/test/test_timeseries_units.py @@ -65,7 +65,7 @@ def ode(num_nodes): continuity=True, rate_continuity=True, units='deg', lower=0.01, upper=179.9) - phase.add_parameter('g', units='m/s**2', static_target=True) + phase.add_parameter('g', units='m/s**2', static_target=True, include_timeseries=True) phase.add_boundary_constraint('x', loc='final', equals=10) phase.add_boundary_constraint('y', loc='final', equals=5) @@ -75,6 +75,8 @@ def ode(num_nodes): phase.add_timeseries_output('xdot', units='degF') phase.add_timeseries_output('ydot', units='degC') + phase.timeseries_options['include_state_rates'] = True + p.setup(check=['unconnected_inputs'], force_alloc_complex=force_alloc_complex) p['traj0.phase0.t_initial'] = 0.0 diff --git a/dymos/examples/cannonball/test/test_connect_control_to_parameter.py b/dymos/examples/cannonball/test/test_connect_control_to_parameter.py index c9a255d90..56bbd80e8 100644 --- a/dymos/examples/cannonball/test/test_connect_control_to_parameter.py +++ b/dymos/examples/cannonball/test/test_connect_control_to_parameter.py @@ -206,7 +206,7 @@ def test_connect_control_to_parameter(self): assert_near_equal(p.get_val('traj.descent.states:r')[-1], 3183.25, tolerance=1.0E-2) assert_near_equal(p.get_val('traj.ascent.timeseries.controls:CD')[-1], - p.get_val('traj.descent.timeseries.parameters:CD')[0]) + p.get_val('traj.descent.parameter_vals:CD')[0]) if __name__ == '__main__': # pragma: no cover diff --git a/dymos/examples/cannonball/test/test_two_phase_cannonball_ode_output_linkage.py b/dymos/examples/cannonball/test/test_two_phase_cannonball_ode_output_linkage.py index 62f6283aa..0e69bd3d9 100644 --- a/dymos/examples/cannonball/test/test_two_phase_cannonball_ode_output_linkage.py +++ b/dymos/examples/cannonball/test/test_two_phase_cannonball_ode_output_linkage.py @@ -130,8 +130,6 @@ def test_two_phase_cannonball_ode_output_linkage(self): assert_near_equal(p.get_val('traj.descent.states:r')[-1], 3183.25, tolerance=1.0E-2) - exp_out = traj.simulate() - print('optimal radius: {0:6.4f} m '.format(p.get_val('radius', units='m')[0])) print('cannonball mass: {0:6.4f} kg '.format(p.get_val('size_comp.mass', @@ -141,73 +139,7 @@ def test_two_phase_cannonball_ode_output_linkage(self): print('maximum range: {0:6.4f} ' 'm '.format(p.get_val('traj.descent.timeseries.states:r')[-1, 0])) - fig, axes = plt.subplots(nrows=1, ncols=1, figsize=(10, 6)) - - time_imp = {'ascent': p.get_val('traj.ascent.timeseries.time'), - 'descent': p.get_val('traj.descent.timeseries.time')} - - time_exp = {'ascent': exp_out.get_val('traj.ascent.timeseries.time'), - 'descent': exp_out.get_val('traj.descent.timeseries.time')} - - r_imp = {'ascent': p.get_val('traj.ascent.timeseries.states:r'), - 'descent': p.get_val('traj.descent.timeseries.states:r')} - - r_exp = {'ascent': exp_out.get_val('traj.ascent.timeseries.states:r'), - 'descent': exp_out.get_val('traj.descent.timeseries.states:r')} - - h_imp = {'ascent': p.get_val('traj.ascent.timeseries.states:h'), - 'descent': p.get_val('traj.descent.timeseries.states:h')} - - h_exp = {'ascent': exp_out.get_val('traj.ascent.timeseries.states:h'), - 'descent': exp_out.get_val('traj.descent.timeseries.states:h')} - - axes.plot(r_imp['ascent'], h_imp['ascent'], 'bo') - - axes.plot(r_imp['descent'], h_imp['descent'], 'ro') - - axes.plot(r_exp['ascent'], h_exp['ascent'], 'b--') - - axes.plot(r_exp['descent'], h_exp['descent'], 'r--') - - axes.set_xlabel('range (m)') - axes.set_ylabel('altitude (m)') - - fig, axes = plt.subplots(nrows=4, ncols=1, figsize=(10, 6)) - states = ['r', 'h', 'v', 'gam'] - for i, state in enumerate(states): - x_imp = {'ascent': p.get_val('traj.ascent.timeseries.states:{0}'.format(state)), - 'descent': p.get_val('traj.descent.timeseries.states:{0}'.format(state))} - - x_exp = {'ascent': exp_out.get_val('traj.ascent.timeseries.states:{0}'.format(state)), - 'descent': exp_out.get_val('traj.descent.timeseries.states:{0}'.format(state))} - - axes[i].set_ylabel(state) - - axes[i].plot(time_imp['ascent'], x_imp['ascent'], 'bo') - axes[i].plot(time_imp['descent'], x_imp['descent'], 'ro') - axes[i].plot(time_exp['ascent'], x_exp['ascent'], 'b--') - axes[i].plot(time_exp['descent'], x_exp['descent'], 'r--') - - params = ['CD', 'mass', 'S'] - fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(12, 6)) - for i, param in enumerate(params): - p_imp = { - 'ascent': p.get_val('traj.ascent.timeseries.parameters:{0}'.format(param)), - 'descent': p.get_val('traj.descent.timeseries.parameters:{0}'.format(param))} - - p_exp = {'ascent': exp_out.get_val('traj.ascent.timeseries.' - 'parameters:{0}'.format(param)), - 'descent': exp_out.get_val('traj.descent.timeseries.' - 'parameters:{0}'.format(param))} - - axes[i].set_ylabel(param) - - axes[i].plot(time_imp['ascent'], p_imp['ascent'], 'bo') - axes[i].plot(time_imp['descent'], p_imp['descent'], 'ro') - axes[i].plot(time_exp['ascent'], p_exp['ascent'], 'b--') - axes[i].plot(time_exp['descent'], p_exp['descent'], 'r--') - - plt.show() + assert_near_equal(p.get_val('traj.linkages.ascent:ke_final|descent:ke_initial'), 0.0) @require_pyoptsparse(optimizer='SLSQP') def test_traj_param_target_none(self): diff --git a/dymos/examples/double_integrator/test/test_double_integrator_timeseries_units.py b/dymos/examples/double_integrator/test/test_double_integrator_timeseries_units.py index 2af080ce4..06b2b7fbe 100644 --- a/dymos/examples/double_integrator/test/test_double_integrator_timeseries_units.py +++ b/dymos/examples/double_integrator/test/test_double_integrator_timeseries_units.py @@ -34,6 +34,8 @@ def double_integrator_direct_collocation(transcription=dm.GaussLobatto, compress phase.set_simulate_options(rtol=1.0E-9, atol=1.0E-9) + phase.timeseries_options['include_state_rates'] = True + # Maximize distance travelled in one second. phase.add_objective('x', loc='final', scaler=-1) diff --git a/dymos/examples/finite_burn_orbit_raise/test/test_ex_two_burn_orbit_raise_bokeh_plots.py b/dymos/examples/finite_burn_orbit_raise/test/test_ex_two_burn_orbit_raise_bokeh_plots.py index 747488e98..2c18c0724 100644 --- a/dymos/examples/finite_burn_orbit_raise/test/test_ex_two_burn_orbit_raise_bokeh_plots.py +++ b/dymos/examples/finite_burn_orbit_raise/test/test_ex_two_burn_orbit_raise_bokeh_plots.py @@ -191,7 +191,7 @@ def two_burn_orbit_raise_problem(transcription='gauss-lobatto', optimizer='SLSQP return p -@use_tempdirs +# @use_tempdirs class TestExampleTwoBurnOrbitRaise(unittest.TestCase): def tearDown(self): @@ -205,8 +205,8 @@ def test_bokeh_plots(self): p = two_burn_orbit_raise_problem(transcription='gauss-lobatto', transcription_order=3, compressed=False, optimizer='SLSQP', show_output=False) - plot_dir = pathlib.Path(_get_reports_dir(p)).joinpath('plots') - self.assertSetEqual({'plots.html'}, set(os.listdir(plot_dir))) + html_file = pathlib.Path(_get_reports_dir(p)) / 'traj_results_report.html' + self.assertTrue(html_file.exists(), msg=f'{html_file} does not exist!') def test_mpl_plots(self): dm.options['plots'] = 'matplotlib' @@ -214,16 +214,13 @@ def test_mpl_plots(self): p = two_burn_orbit_raise_problem(transcription='gauss-lobatto', transcription_order=3, compressed=False, optimizer='SLSQP', show_output=False) - expected_files = {'control_rates_u1_rate.png', 'state_rates_r.png', 'states_deltav.png', - 'states_r.png', 'state_rates_accel.png', 'state_rates_deltav.png', - 'states_accel.png', 'controls_u1.png', 'states_vr.png', 'pos_x.png', - 'states_vt.png', 'pos_y.png', 'parameters_u1.png', 'states_theta.png', - 'control_rates_u1_rate2.png', 'state_rates_vt.png', 'time_phase.png', - 'parameters_c.png', 'state_rates_theta.png', 'state_rates_vr.png', 'dt_dstau.png'} + expected_files = ('states_deltav.png', 'states_r.png', 'states_accel.png', + 'controls_u1.png', 'states_vr.png', 'pos_x.png', + 'states_vt.png', 'pos_y.png', 'states_theta.png') - html_files = {str(pathlib.Path(f).with_suffix('.html')) for f in expected_files} - plot_dir = pathlib.Path(_get_reports_dir(p)).joinpath('plots') - self.assertSetEqual(expected_files.union(html_files), set(os.listdir(plot_dir))) + for file in expected_files: + plotfile = pathlib.Path(_get_reports_dir(p)).joinpath('plots') / file + self.assertTrue(plotfile.exists(), msg=f'{plotfile} does not exist!') if __name__ == '__main__': # pragma: no cover diff --git a/dymos/grid_refinement/error_estimation.py b/dymos/grid_refinement/error_estimation.py index 49176cf01..fe207a696 100644 --- a/dymos/grid_refinement/error_estimation.py +++ b/dymos/grid_refinement/error_estimation.py @@ -217,7 +217,7 @@ def eval_ode_on_grid(phase, transcription): for name, options in phase.parameter_options.items(): targets = get_targets(ode, name, options['targets']) # The value of the parameter at one node - param[name] = phase.get_val(f'timeseries.parameters:{name}', units=options['units'])[0, ...] + param[name] = phase.get_val(f'parameter_vals:{name}', units=options['units'])[0, ...] if targets: p_refine.set_val(f'parameters:{name}', param[name], units=options['units']) diff --git a/dymos/phase/test/test_analytic_phase.py b/dymos/phase/test/test_analytic_phase.py index 4c99e8959..eb781bb0c 100644 --- a/dymos/phase/test/test_analytic_phase.py +++ b/dymos/phase/test/test_analytic_phase.py @@ -297,7 +297,7 @@ def test_timeseries_expr(self): p.run_model() y = p.get_val('traj.phase.timeseries.states:y', units='unitless') - y0 = p.get_val('traj.phase.timeseries.parameters:y0', units='unitless') + y0 = p.get_val('traj.phase.parameter_vals:y0', units='unitless') expected_z = y0 + y**2 z = p.get_val('traj.phase.timeseries.z') diff --git a/dymos/phase/test/test_phase.py b/dymos/phase/test/test_phase.py index 441b82b0e..6c1f22450 100644 --- a/dymos/phase/test/test_phase.py +++ b/dymos/phase/test/test_phase.py @@ -613,9 +613,9 @@ def test_parameter_initial_boundary_constraint(self): assert_near_equal(p.get_val('phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-4) - assert_near_equal(p.get_val('phase0.timeseries.parameters:g')[0], 9.80665, + assert_near_equal(p.get_val('phase0.parameter_vals:g')[0], 9.80665, tolerance=1.0E-6) - assert_near_equal(p.get_val('phase0.timeseries.parameters:g')[-1], 9.80665, + assert_near_equal(p.get_val('phase0.parameter_vals:g')[-1], 9.80665, tolerance=1.0E-6) def test_parameter_final_boundary_constraint(self): @@ -681,9 +681,9 @@ def test_parameter_final_boundary_constraint(self): assert_near_equal(p.get_val('phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-4) - assert_near_equal(p.get_val('phase0.timeseries.parameters:g')[0], 9.80665, + assert_near_equal(p.get_val('phase0.parameter_vals:g')[0], 9.80665, tolerance=1.0E-6) - assert_near_equal(p.get_val('phase0.timeseries.parameters:g')[-1], 9.80665, + assert_near_equal(p.get_val('phase0.parameter_vals:g')[-1], 9.80665, tolerance=1.0E-6) def test_parameter_path_constraint(self): diff --git a/dymos/phase/test/test_timeseries.py b/dymos/phase/test/test_timeseries.py index 3e3a8017b..cc8bb3d54 100644 --- a/dymos/phase/test/test_timeseries.py +++ b/dymos/phase/test/test_timeseries.py @@ -45,9 +45,9 @@ def test_timeseries_gl(self, test_smaller_timeseries=False): units='deg', lower=0.01, upper=179.9, ref=1, ref0=0) if test_smaller_timeseries: - phase.add_parameter('g', opt=True, units='m/s**2', val=9.80665, include_timeseries=False) - else: phase.add_parameter('g', opt=True, units='m/s**2', val=9.80665) + else: + phase.add_parameter('g', opt=True, units='m/s**2', val=9.80665, include_timeseries=True) # Minimize time at the end of the phase phase.add_objective('time_phase', loc='final', scaler=10) @@ -94,7 +94,7 @@ def test_timeseries_gl(self, test_smaller_timeseries=False): with self.assertRaises(KeyError): p.get_val(f'phase0.timeseries.parameters:{dp}') else: - assert_near_equal(p.get_val(f'phase0.parameters:{dp}')[0], + assert_near_equal(p.get_val(f'phase0.parameter_vals:{dp}')[0], p.get_val(f'phase0.timeseries.parameters:{dp}')[i]) # call simulate to test SolveIVP transcription @@ -109,6 +109,7 @@ def test_timeseries_gl_smaller_timeseries(self): self.test_timeseries_gl(test_smaller_timeseries=True) def test_timeseries_radau(self, test_smaller_timeseries=False): + p = om.Problem(model=om.Group()) p.driver = om.ScipyOptimizeDriver() @@ -117,6 +118,8 @@ def test_timeseries_radau(self, test_smaller_timeseries=False): phase = dm.Phase(ode_class=BrachistochroneODE, transcription=dm.Radau(num_segments=8, order=3, compressed=True)) + phase.timeseries_options['include_state_rates'] = True + p.model.add_subsystem('phase0', phase) phase.set_time_options(fix_initial=True, duration_bounds=(.5, 10)) @@ -131,9 +134,9 @@ def test_timeseries_radau(self, test_smaller_timeseries=False): units='deg', lower=0.01, upper=179.9, ref=1, ref0=0) if test_smaller_timeseries: - phase.add_parameter('g', opt=True, units='m/s**2', val=9.80665, include_timeseries=False) - else: phase.add_parameter('g', opt=True, units='m/s**2', val=9.80665) + else: + phase.add_parameter('g', opt=True, units='m/s**2', val=9.80665, include_timeseries=True) # Minimize time at the end of the phase phase.add_objective('time_phase', loc='final', scaler=10) @@ -247,8 +250,8 @@ def test_timeseries_explicit_shooting(self, test_smaller_timeseries=False): assert_near_equal(np.atleast_2d(p.get_val('phase0.t')).T, p.get_val('phase0.timeseries.time')) - assert_near_equal(np.atleast_2d(p.get_val('phase0.t_phase')).T, - p.get_val('phase0.timeseries.time_phase')) + with self.assertRaises(KeyError): + p.get_val('phase0.timeseries.time_phase') for state in ('x', 'y', 'v'): assert_near_equal(p.get_val(f'phase0.integrator.states_out:{state}'), @@ -775,4 +778,4 @@ def test_timeseries_expr_gl(self): if __name__ == '__main__': # pragma: no cover - unittest.main() + unittest.main() \ No newline at end of file diff --git a/dymos/run_problem.py b/dymos/run_problem.py index 6fb4a3e06..3026a7d9f 100755 --- a/dymos/run_problem.py +++ b/dymos/run_problem.py @@ -2,6 +2,7 @@ import openmdao.api as om from openmdao.recorders.case import Case +from ._options import options as dymos_options from dymos.trajectory.trajectory import Trajectory from dymos.load_case import load_case from dymos.visualization.timeseries_plots import timeseries_plots @@ -112,8 +113,14 @@ def run_problem(problem, refine_method='hp', refine_iteration_limit=0, run_drive subsys.simulate(record_file=simulation_record_file, case_prefix=case_prefix, **_simulate_kwargs) if make_plots: - from dymos.visualization.timeseries.bokeh_timeseries_report import make_timeseries_report - make_timeseries_report(prob=problem, solution_record_file=solution_record_file, - simulation_record_file=simulation_record_file) + if dymos_options['plots'] == 'bokeh': + from dymos.visualization.timeseries.bokeh_timeseries_report import make_timeseries_report + make_timeseries_report(prob=problem, solution_record_file=solution_record_file, + simulation_record_file=simulation_record_file) + else: + _sim_record_file = None if not simulate else simulation_record_file + _plot_kwargs = plot_kwargs if plot_kwargs is not None else {} + timeseries_plots(solution_record_file, simulation_record_file=_sim_record_file, + plot_dir=plot_dir, problem=problem, **_plot_kwargs) return failed diff --git a/dymos/test/test_run_problem.py b/dymos/test/test_run_problem.py index c1bf07910..f9d07b063 100755 --- a/dymos/test/test_run_problem.py +++ b/dymos/test/test_run_problem.py @@ -39,7 +39,6 @@ def test_run_HS_problem_radau_hp_refine(self): p.driver.opt_settings['Major optimality tolerance'] = 1.0E-6 elif optimizer == 'IPOPT': p.driver.opt_settings['hessian_approximation'] = 'limited-memory' - # p.driver.opt_settings['nlp_scaling_method'] = 'user-scaling' p.driver.opt_settings['print_level'] = 0 p.driver.opt_settings['max_iter'] = 200 p.driver.opt_settings['linear_solver'] = 'mumps' @@ -393,7 +392,6 @@ def test_run_brachistochrone_problem_refine_case_driver_case_prefix(self): self.assertTrue(case.startswith('hp_') and 'pyOptSparse_SLSQP|' in case, msg=f'Unexpected case: {case}') @require_pyoptsparse(optimizer='SLSQP') - @unittest.skip(reason='Skipped until the OpenMDAO issue of coloring changing the case_prefix is resolved.') def test_run_brachistochrone_problem_refine_case_prefix(self): p = om.Problem(model=om.Group()) p.driver = om.pyOptSparseDriver() @@ -661,6 +659,9 @@ def setUp(self): phase0.set_refine_options(refine=True) + phase0.timeseries_options['include_state_rates'] = True + phase0.timeseries_options['include_t_phase'] = True + p.model.linear_solver = om.DirectSolver() p.setup(check=True) @@ -676,14 +677,18 @@ def setUp(self): self.p = p def test_run_brachistochrone_problem_make_plots(self): + plots_cache = dm.options['plots'] + dm.options['plots'] = 'matplotlib' dm.run_problem(self.p, make_plots=True) plot_dir = pathlib.Path(_get_reports_dir(self.p)).joinpath('plots') for varname in ['time_phase', 'states:x', 'state_rates:x', 'states:y', 'state_rates:y', 'states:v', 'state_rates:v', 'controls:theta', 'control_rates:theta_rate', - 'control_rates:theta_rate2', 'parameters:g']: - self.assertTrue(plot_dir.joinpath(f'{varname.replace(":","_")}.png').exists()) + 'control_rates:theta_rate2']: + self.assertTrue(plot_dir.joinpath(f'{varname.replace(":","_")}.png').exists(), + msg=str(plot_dir.joinpath(f'{varname.replace(":","_")}.png')) + ' does not exist.') + dm.options['plots'] = plots_cache def test_run_brachistochrone_problem_make_plots_set_plot_dir(self): dm.run_problem(self.p, make_plots=True, plot_dir="test_plot_dir") @@ -692,8 +697,9 @@ def test_run_brachistochrone_problem_make_plots_set_plot_dir(self): for varname in ['time_phase', 'states:x', 'state_rates:x', 'states:y', 'state_rates:y', 'states:v', 'state_rates:v', 'controls:theta', 'control_rates:theta_rate', - 'control_rates:theta_rate2', 'parameters:g']: - self.assertTrue(plot_dir.joinpath('test_plot_dir', f'{varname.replace(":","_")}.png').exists()) + 'control_rates:theta_rate2']: + plotfile = plot_dir.joinpath('test_plot_dir', f'{varname.replace(":","_")}.png') + self.assertTrue(plotfile.exists(), msg = str(plotfile) + ' does not exist.') def test_run_brachistochrone_problem_do_not_make_plots(self): dm.run_problem(self.p, make_plots=False) @@ -703,7 +709,8 @@ def test_run_brachistochrone_problem_do_not_make_plots(self): 'state_rates:y', 'states:v', 'state_rates:v', 'controls:theta', 'control_rates:theta_rate', 'control_rates:theta_rate2', 'parameters:g']: - self.assertFalse(plot_dir.joinpath(f'{varname.replace(":","_")}.png').exists()) + plotfile = plot_dir.joinpath(f'{varname.replace(":","_")}.png') + self.assertFalse(plotfile.exists(), msg=f'Unexpectedly found plot file {plotfile}') def test_run_brachistochrone_problem_set_simulation_record_file(self): simulation_record_file = 'simulation_record_file.db' @@ -718,25 +725,35 @@ def test_run_brachistochrone_problem_set_solution_record_file(self): self.assertTrue(os.path.exists(solution_record_file)) def test_run_brachistochrone_problem_plot_simulation(self): + plots_cache = dm.options['plots'] + dm.options['plots'] = 'matplotlib' + dm.run_problem(self.p, make_plots=True, simulate=True) plot_dir = pathlib.Path(_get_reports_dir(self.p)).joinpath('plots') for varname in ['time_phase', 'states:x', 'state_rates:x', 'states:y', 'state_rates:y', 'states:v', 'state_rates:v', 'controls:theta', 'control_rates:theta_rate', - 'control_rates:theta_rate2', 'parameters:g']: - self.assertTrue(plot_dir.joinpath(f'{varname.replace(":","_")}.png').exists()) + 'control_rates:theta_rate2']: + plotfile = plot_dir.joinpath(f'{varname.replace(":","_")}.png') + self.assertTrue(plotfile.exists(), msg=f'plot file {plotfile} does not exist!') + dm.options['plots'] = plots_cache def test_run_brachistochrone_problem_plot_no_simulation_record_file_given(self): + plots_cache = dm.options['plots'] + dm.options['plots'] = 'matplotlib' + dm.run_problem(self.p, make_plots=True, simulate=True) plot_dir = pathlib.Path(_get_reports_dir(self.p)).joinpath('plots') for varname in ['time_phase', 'states:x', 'state_rates:x', 'states:y', 'state_rates:y', 'states:v', 'state_rates:v', 'controls:theta', 'control_rates:theta_rate', - 'control_rates:theta_rate2', 'parameters:g']: - self.assertTrue(plot_dir.joinpath(f'{varname.replace(":","_")}.png').exists()) + 'control_rates:theta_rate2']: + plotfile = plot_dir.joinpath(f'{varname.replace(":", "_")}.png') + self.assertTrue(plotfile.exists(), msg=f'plot file {plotfile} does not exist!') + dm.options['plots'] = plots_cache @use_tempdirs class TestSimulateArrayParam(unittest.TestCase): @@ -813,8 +830,8 @@ def test_simulate_array_param(self): sol_results = om.CaseReader('dymos_solution.db').get_case('final') sim_results = om.CaseReader('dymos_solution.db').get_case('final') - sol = sol_results.get_val('traj.phase0.timeseries.parameters:array') - sim = sim_results.get_val('traj.phase0.timeseries.parameters:array') + sol = sol_results.get_val('traj.phase0.parameter_vals:array') + sim = sim_results.get_val('traj.phase0.parameter_vals:array') assert_near_equal(sol - sim, np.zeros_like(sol)) diff --git a/dymos/test/test_upgrade_guide.py b/dymos/test/test_upgrade_guide.py index 81923054a..1609b9a55 100644 --- a/dymos/test/test_upgrade_guide.py +++ b/dymos/test/test_upgrade_guide.py @@ -108,10 +108,9 @@ def test_parameters(self): assert_near_equal(p.get_val('traj.phase0.timeseries.states:vy')[-1], 0, 1e-4) # upgrade_doc: begin parameter_timeseries - thrust = p.get_val('traj.phase0.timeseries.parameters:thrust') + thrust = p.get_val('traj.phase0.parameter_vals:thrust') # upgrade_doc: end parameter_timeseries - nn = phase.options['transcription'].grid_data.num_nodes - assert_near_equal(thrust, 2.1E6 * np.ones([nn, 1])) + assert_near_equal(thrust.ravel(), 2.1E6) def test_parameter_no_timeseries(self): import openmdao.api as om @@ -752,7 +751,7 @@ def run_asserts(self): v0 = p.get_val('traj0.phase0.timeseries.states:v')[0] vf = p.get_val('traj0.phase0.timeseries.states:v')[-1] - g = p.get_val('traj0.phase0.timeseries.parameters:g')[0] + g = p.get_val('traj0.phase0.parameter_vals:g')[0] thetaf = p.get_val('traj0.phase0.timeseries.controls:theta')[-1] diff --git a/dymos/trajectory/test/test_trajectory.py b/dymos/trajectory/test/test_trajectory.py index a871f3644..8bc2f3bfd 100644 --- a/dymos/trajectory/test/test_trajectory.py +++ b/dymos/trajectory/test/test_trajectory.py @@ -412,8 +412,8 @@ def test_linked_parameters(self): p.run_model() - burn1_c_final = p.get_val('burn1.timeseries.parameters:c')[-1, ...] - burn2_c_initial = p.get_val('burn2.timeseries.parameters:c')[0, ...] + burn1_c_final = p.get_val('burn1.parameter_vals:c')[-1, ...] + burn2_c_initial = p.get_val('burn2.parameter_vals:c')[0, ...] c_linkage_error = p.get_val('linkages.burn1:c_final|burn2:c_initial') assert_near_equal(c_linkage_error, burn1_c_final - burn2_c_initial) @@ -627,6 +627,9 @@ def test_linked_control_rate(self): self.traj.link_phases(phases=['burn1', 'burn2'], vars=['accel']) self.traj.link_phases(phases=['burn1', 'burn2'], vars=['u1_rate']) + for phs in burn1, coast, burn2: + phs.timeseries_options['include_control_rates'] = True + # Finish Problem Setup p.model.linear_solver = om.DirectSolver() diff --git a/dymos/trajectory/trajectory.py b/dymos/trajectory/trajectory.py index 75fecd821..96d67cbfe 100644 --- a/dymos/trajectory/trajectory.py +++ b/dymos/trajectory/trajectory.py @@ -556,7 +556,7 @@ def _update_linkage_options_configure(self, linkage_options): shapes[i] = phases[i].control_options[vars[i]]['shape'] elif classes[i] in {'control_rate', 'control_rate2'}: prefix = 'control_rates:' if dymos_options['use_timeseries_prefix'] else '' - sources[i] = f'timeseries.{prefix}:{vars[i]}' + sources[i] = f'timeseries.{prefix}{vars[i]}' control_name = vars[i][:-5] if classes[i] == 'control_rate' else vars[i][:-6] units[i] = phases[i].control_options[control_name]['units'] deriv = 1 if classes[i].endswith('rate') else 2 diff --git a/dymos/visualization/test/test_timeseries_plots.py b/dymos/visualization/test/test_timeseries_plots.py index a05f0513b..80c75ca1d 100644 --- a/dymos/visualization/test/test_timeseries_plots.py +++ b/dymos/visualization/test/test_timeseries_plots.py @@ -80,6 +80,9 @@ def setUp(self): self.p = p def test_brachistochrone_timeseries_plots(self): + temp = dm.options['plots'] + dm.options['plots'] = 'matplotlib' + dm.run_problem(self.p, make_plots=False) timeseries_plots('dymos_solution.db', problem=self.p) @@ -92,7 +95,12 @@ def test_brachistochrone_timeseries_plots(self): self.assertTrue(plot_dir.joinpath('control_rates_theta_rate.png').exists()) self.assertTrue(plot_dir.joinpath('control_rates_theta_rate2.png').exists()) + dm.options['plots'] = temp + def test_brachistochrone_timeseries_plots_solution_only_set_solution_record_file(self): + temp = dm.options['plots'] + dm.options['plots'] = 'matplotlib' + # records to the default file 'dymos_simulation.db' dm.run_problem(self.p, make_plots=False, solution_record_file='solution_record_file.db') @@ -106,6 +114,8 @@ def test_brachistochrone_timeseries_plots_solution_only_set_solution_record_file self.assertTrue(plot_dir.joinpath('control_rates_theta_rate.png').exists()) self.assertTrue(plot_dir.joinpath('control_rates_theta_rate2.png').exists()) + dm.options['plots'] = temp + def test_brachistochrone_timeseries_plots_solution_and_simulation(self): dm.run_problem(self.p, simulate=True, make_plots=False, simulation_record_file='simulation_record_file.db') @@ -113,6 +123,10 @@ def test_brachistochrone_timeseries_plots_solution_and_simulation(self): timeseries_plots('dymos_solution.db', simulation_record_file='simulation_record_file.db', problem=self.p) def test_brachistochrone_timeseries_plots_set_plot_dir(self): + + temp = dm.options['plots'] + dm.options['plots'] = 'matplotlib' + dm.run_problem(self.p, make_plots=False) plot_dir = pathlib.Path(_get_reports_dir(self.p)).joinpath("test_plot_dir").resolve() @@ -125,12 +139,16 @@ def test_brachistochrone_timeseries_plots_set_plot_dir(self): self.assertTrue(plot_dir.joinpath('control_rates_theta_rate.png').exists()) self.assertTrue(plot_dir.joinpath('control_rates_theta_rate2.png').exists()) + dm.options['plots'] = temp + @use_tempdirs class TestTimeSeriesPlotsMultiPhase(unittest.TestCase): @require_pyoptsparse(optimizer='IPOPT') def test_trajectory_linked_phases_make_plot(self): + temp = dm.options['plots'] + dm.options['plots'] = 'matplotlib' self.traj = dm.Trajectory() p = self.p = om.Problem(model=self.traj) @@ -221,6 +239,10 @@ def test_trajectory_linked_phases_make_plot(self): # Finish Problem Setup p.model.linear_solver = om.DirectSolver() + for phase_name, phase in self.traj._phases.items(): + phase.timeseries_options['include_state_rates'] = True + phase.timeseries_options['include_t_phase'] = True + p.setup(check=True) # Set Initial Guesses @@ -271,12 +293,17 @@ def test_trajectory_linked_phases_make_plot(self): 'state_rates:theta', 'states:vr', 'state_rates:vr', 'states:vt', 'state_rates:vt', 'states:accel', 'state_rates:accel', 'states:deltav', 'state_rates:deltav', - 'controls:u1', 'control_rates:u1_rate', 'control_rates:u1_rate2', - 'parameters:c']: - self.assertTrue(plot_dir.joinpath(varname.replace(":", "_") + '.png').exists()) + 'controls:u1', 'control_rates:u1_rate', 'control_rates:u1_rate2']: + plotfile = plot_dir.joinpath(varname.replace(":", "_") + '.png') + self.assertTrue(plotfile.exists(), msg=f'{plotfile} does not exist!') + + dm.options['plots'] = temp def test_overlapping_phases_make_plot(self): + _temp = dm.options['plots'] + dm.options['plots'] = 'matplotlib' + prob = om.Problem() opt = prob.driver = om.ScipyOptimizeDriver() @@ -335,6 +362,10 @@ def test_overlapping_phases_make_plot(self): prob.model.options['assembled_jac_type'] = 'csc' prob.model.linear_solver = om.DirectSolver(assemble_jac=True) + for phase_name, phase in traj._phases.items(): + phase.timeseries_options['include_t_phase'] = True + phase.timeseries_options['include_state_rates'] = True + prob.setup() prob['traj.phase0.t_initial'] = 0 @@ -361,12 +392,16 @@ def test_overlapping_phases_make_plot(self): self.assertTrue(plot_dir.joinpath('states_state_of_charge.png').exists()) self.assertTrue(plot_dir.joinpath('state_rates_state_of_charge.png').exists()) + dm.options['plots'] = _temp + @require_pyoptsparse(optimizer='IPOPT') def test_trajectory_linked_phases_make_plot_missing_data(self): """ Test that plots are still generated even if the phases don't share the exact same variables in the timeseries. """ + temp = dm.options['plots'] + dm.options['plots'] = 'matplotlib' self.traj = dm.Trajectory() p = self.p = om.Problem(model=self.traj) @@ -396,7 +431,7 @@ def test_trajectory_linked_phases_make_plot_missing_data(self): rate_source='deltav_dot', units='DU/TU') burn1.add_control('u1', targets=['u1'], rate_continuity=True, rate2_continuity=True, units='deg', scaler=0.01, lower=-30, upper=30) - burn1.add_parameter('c', opt=False, val=1.5, targets=['c'], units='DU/TU') + burn1.add_parameter('c', opt=False, val=1.5, targets=['c'], units='DU/TU', include_timeseries=True) # Second Phase (Coast) @@ -419,8 +454,8 @@ def test_trajectory_linked_phases_make_plot_missing_data(self): rate_source='at_dot', targets=['accel'], units='DU/TU**2') coast.add_state('deltav', fix_initial=False, fix_final=False, rate_source='deltav_dot', units='DU/TU') - coast.add_parameter('u1', targets=['u1'], opt=False, val=0.0, units='deg') - coast.add_parameter('c', opt=False, val=1.5, targets=['c'], units='DU/TU') + coast.add_parameter('u1', targets=['u1'], opt=False, val=0.0, units='deg', include_timeseries=True) + coast.add_parameter('c', opt=False, val=1.5, targets=['c'], units='DU/TU', include_timeseries=True) # Third Phase (burn) @@ -445,7 +480,7 @@ def test_trajectory_linked_phases_make_plot_missing_data(self): rate_source='deltav_dot', units='DU/TU') burn2.add_control('u1', targets=['u1'], rate_continuity=True, rate2_continuity=True, units='deg', scaler=0.01, lower=-30, upper=30) - burn2.add_parameter('c', opt=False, val=1.5, targets=['c'], units='DU/TU') + burn2.add_parameter('c', opt=False, val=1.5, targets=['c'], units='DU/TU', include_timeseries=True) burn2.add_objective('deltav', loc='final') @@ -457,6 +492,11 @@ def test_trajectory_linked_phases_make_plot_missing_data(self): # Finish Problem Setup p.model.linear_solver = om.DirectSolver() + for phase_name, phase in self.traj._phases.items(): + phase.timeseries_options['include_t_phase'] = True + phase.timeseries_options['include_state_rates'] = True + phase.timeseries_options['include_control_rates'] = True + p.setup(check=True) # Set Initial Guesses @@ -507,7 +547,10 @@ def test_trajectory_linked_phases_make_plot_missing_data(self): 'state_rates:accel', 'states:deltav', 'state_rates:deltav', 'controls:u1', 'control_rates:u1_rate', 'control_rates:u1_rate2', 'parameters:c', 'parameters:u1']: - self.assertTrue(plot_dir.joinpath(varname.replace(":", "_") + '.png').exists()) + self.assertTrue(plot_dir.joinpath(varname.replace(":", "_") + '.png').exists(), + msg=plot_dir.joinpath(varname.replace(":", "_") + '.png does not exist!')) + + dm.options['plots'] = temp if __name__ == '__main__': # pragma: no cover diff --git a/dymos/visualization/timeseries_plots.py b/dymos/visualization/timeseries_plots.py index 1101f2c69..6aac87991 100644 --- a/dymos/visualization/timeseries_plots.py +++ b/dymos/visualization/timeseries_plots.py @@ -151,7 +151,7 @@ def _bokeh_timeseries_plots(time_units, var_units, phase_names, phases_node_path bg_fill_color='#282828', grid_line_color='#666666', open_browser=False): from bokeh.io import output_notebook, output_file, save, show from bokeh.layouts import gridplot, column - from bokeh.models import Legend, ColumnDataSource + from bokeh.models import Legend from bokeh.plotting import figure import bokeh.palettes as bp @@ -161,7 +161,7 @@ def _bokeh_timeseries_plots(time_units, var_units, phase_names, phases_node_path output_file(os.path.join(plot_dir_path, 'plots.html')) # Prune the edges from the color map - phase_color_map = bp.d3['Category20b'] + cmap = bp.turbo(len(phase_names) + 2)[1:-1] figures = [] colors = {} sol_plots = {} @@ -172,124 +172,107 @@ def _bokeh_timeseries_plots(time_units, var_units, phase_names, phases_node_path min_time = 1.0E21 max_time = -1.0E21 - # for iphase, phase_name in enumerate(phase_names): - # if phases_node_path: - # time_name = f'{phases_node_path}.{phase_name}.timeseries.time' - # else: - # time_name = f'{phase_name}.timeseries.time' - # # min_time = min(min_time, np.min(last_solution_case.outputs[time_name])) - # # max_time = max(max_time, np.max(last_solution_case.outputs[time_name])) - # colors[phase_name] = phase_color_map[iphase + 3] + for iphase, phase_name in enumerate(phase_names): + if phases_node_path: + time_name = f'{phases_node_path}.{phase_name}.timeseries.time' + else: + time_name = f'{phase_name}.timeseries.time' + min_time = min(min_time, np.min(last_solution_case.outputs[time_name])) + max_time = max(max_time, np.max(last_solution_case.outputs[time_name])) + colors[phase_name] = cmap[iphase] - print(var_units) + for var_name, var_unit in var_units.items(): + # Get the labels + time_label = f'time ({time_units[var_name]})' + var_label = f'{var_name} ({var_unit})' + title = f'timeseries.{var_name}' - # Build the ColumnDataSource - data = {} + # add labels, title, and legend + padding = 0.05 * (max_time - min_time) + fig = figure(title=title, background_fill_color=bg_fill_color, + x_range=(min_time - padding, max_time + padding), + width=180, height=180) + fig.xaxis.axis_label = time_label + fig.yaxis.axis_label = var_label + fig.xgrid.grid_line_color = grid_line_color + fig.ygrid.grid_line_color = grid_line_color - last_solution_case.list_outputs() + # Plot each phase + for iphase, phase_name in enumerate(phase_names): + sol_color = cmap[iphase] + sim_color = cmap[iphase] - for var_name, units in var_units.items(): - if last_solution_case: - data[f'sol:{var_name}'] = last_solution_case.get_val(var_name, units=units) - if last_simulation_case: - data[f'sim:{var_name}'] = last_simulation_case.get_val(var_name, units=units) - - data_source = ColumnDataSource(data=data) - - # for var_name, var_unit in var_units.items(): - # # Get the labels - # time_label = f'time ({time_units[var_name]})' - # var_label = f'{var_name} ({var_unit})' - # title = f'timeseries.{var_name}' - # - # - # - # # add labels, title, and legend - # padding = 0.05 * (max_time - min_time) - # fig = figure(title=title, background_fill_color=bg_fill_color, - # x_range=(min_time - padding, max_time + padding), - # width=180, height=180) - # fig.xaxis.axis_label = time_label - # fig.yaxis.axis_label = var_label - # fig.xgrid.grid_line_color = grid_line_color - # fig.ygrid.grid_line_color = grid_line_color - # - # # Plot each phase - # for iphase, phase_name in enumerate(phase_names): - # sol_color = phase_color_map[iphase] - # sim_color = phase_color_map[iphase] - # - # if phases_node_path: - # var_name_full = f'{phases_node_path}.{phase_name}.timeseries.{var_name}' - # time_name = f'{phases_node_path}.{phase_name}.timeseries.time' - # else: - # var_name_full = f'{phase_name}.timeseries.{var_name}' - # time_name = f'{phase_name}.timeseries.time' - # - # # Get values - # if var_name_full not in last_solution_case.outputs: - # continue - # - # var_val = last_solution_case.outputs[var_name_full] - # time_val = last_solution_case.outputs[time_name] - # - # for idxs, i in np.ndenumerate(np.zeros(var_val.shape[1:])): - # var_val_i = var_val[:, idxs].ravel() - # sol_plots[phase_name] = fig.circle(time_val.ravel(), var_val_i, size=5, - # color=sol_color, name='sol:' + phase_name) - # - # # get simulation values, if plotting simulation - # if last_simulation_case: - # # if the phases_node_path is empty, need to pre-pend names with "sim_traj." - # # as that is pre-pended in Trajectory.simulate code - # sim_prefix = '' if phases_node_path else 'sim_traj.' - # var_val_simulate = last_simulation_case.outputs[sim_prefix + var_name_full] - # time_val_simulate = last_simulation_case.outputs[sim_prefix + time_name] - # for idxs, i in np.ndenumerate(np.zeros(var_val_simulate.shape[1:])): - # var_val_i = var_val_simulate[:, idxs].ravel() - # sim_plots[phase_name] = fig.line(time_val_simulate.ravel(), var_val_i, - # line_dash='solid', line_width=0.5, color=sim_color, - # name='sim:' + phase_name) - # figures.append(fig) - # - # # Implement a single legend for all figures using the example here: - # # https://stackoverflow.com/a/56825812/754536 - # - # # ## Use a dummy figure for the LEGEND - # dum_fig = figure(outline_line_alpha=0, toolbar_location=None, - # background_fill_color=bg_fill_color, width=250, max_width=250) - # - # # set the components of the figure invisible - # for fig_component in [dum_fig.grid, dum_fig.ygrid, dum_fig.xaxis, dum_fig.yaxis]: - # fig_component.visible = False - # - # # The glyphs referred by the legend need to be present in the figure that holds the legend, - # # so we must add them to the figure renderers. - # sol_legend_items = [(phase_name + ' solution', [dum_fig.circle([0], [0], - # size=5, - # color=colors[phase_name], - # tags=['sol:' + phase_name])]) for phase_name in phase_names] - # sim_legend_items = [(phase_name + ' simulation', [dum_fig.line([0], [0], - # line_dash='solid', - # line_width=0.5, - # color=colors[phase_name], - # tags=['sim:' + phase_name])]) for phase_name in phase_names] - # legend_items = [j for i in zip(sol_legend_items, sim_legend_items) for j in i] - # - # # # set the figure range outside of the range of all glyphs - # dum_fig.x_range.end = 1005 - # dum_fig.x_range.start = 1000 - # - # legend = Legend(click_policy='hide', location='top_left', border_line_alpha=0, items=legend_items, - # background_fill_alpha=0.0, label_text_color='white', label_width=120, spacing=10) - # - # dum_fig.add_layout(legend, place='center') - # - # gd = gridplot(figures, ncols=num_cols, sizing_mode='scale_both') - # - # plots = gridplot([[gd, column(dum_fig, sizing_mode='stretch_height')]], - # toolbar_location=None, - # sizing_mode='scale_both') + if phases_node_path: + var_name_full = f'{phases_node_path}.{phase_name}.timeseries.{var_name}' + time_name = f'{phases_node_path}.{phase_name}.timeseries.time' + else: + var_name_full = f'{phase_name}.timeseries.{var_name}' + time_name = f'{phase_name}.timeseries.time' + + # Get values + if var_name_full not in last_solution_case.outputs: + continue + + var_val = last_solution_case.outputs[var_name_full] + time_val = last_solution_case.outputs[time_name] + + for idxs, i in np.ndenumerate(np.zeros(var_val.shape[1:])): + var_val_i = var_val[:, idxs].ravel() + sol_plots[phase_name] = fig.circle(time_val.ravel(), var_val_i, size=5, + color=sol_color, name='sol:' + phase_name) + + # get simulation values, if plotting simulation + if last_simulation_case: + # if the phases_node_path is empty, need to pre-pend names with "sim_traj." + # as that is pre-pended in Trajectory.simulate code + sim_prefix = '' if phases_node_path else 'sim_traj.' + var_val_simulate = last_simulation_case.outputs[sim_prefix + var_name_full] + time_val_simulate = last_simulation_case.outputs[sim_prefix + time_name] + for idxs, i in np.ndenumerate(np.zeros(var_val_simulate.shape[1:])): + var_val_i = var_val_simulate[:, idxs].ravel() + sim_plots[phase_name] = fig.line(time_val_simulate.ravel(), var_val_i, + line_dash='solid', line_width=0.5, color=sim_color, + name='sim:' + phase_name) + figures.append(fig) + + # Implement a single legend for all figures using the example here: + # https://stackoverflow.com/a/56825812/754536 + + # ## Use a dummy figure for the LEGEND + dum_fig = figure(outline_line_alpha=0, toolbar_location=None, + background_fill_color=bg_fill_color, width=250, max_width=250) + + # set the components of the figure invisible + for fig_component in [dum_fig.grid, dum_fig.ygrid, dum_fig.xaxis, dum_fig.yaxis]: + fig_component.visible = False + + # The glyphs referred by the legend need to be present in the figure that holds the legend, + # so we must add them to the figure renderers. + sol_legend_items = [(phase_name + ' solution', [dum_fig.circle([0], [0], + size=5, + color=colors[phase_name], + tags=['sol:' + phase_name])]) for phase_name in phase_names] + sim_legend_items = [(phase_name + ' simulation', [dum_fig.line([0], [0], + line_dash='solid', + line_width=0.5, + color=colors[phase_name], + tags=['sim:' + phase_name])]) for phase_name in phase_names] + legend_items = [j for i in zip(sol_legend_items, sim_legend_items) for j in i] + + # # set the figure range outside of the range of all glyphs + dum_fig.x_range.end = 1005 + dum_fig.x_range.start = 1000 + + legend = Legend(click_policy='hide', location='top_left', border_line_alpha=0, items=legend_items, + background_fill_alpha=0.0, label_text_color='white', label_width=120, spacing=10) + + dum_fig.add_layout(legend, place='center') + + gd = gridplot(figures, ncols=num_cols, sizing_mode='scale_both') + + plots = gridplot([[gd, column(dum_fig, sizing_mode='stretch_height')]], + toolbar_location=None, + sizing_mode='scale_both') if dymos_options['notebook_mode'] or open_browser: show(plots) @@ -460,4 +443,4 @@ def timeseries_plots(solution_recorder_filename, simulation_record_file=None, pl with open(htmlpath, 'w', encoding='utf-8') as f: f.write(image2html(fpath.name)) else: - raise ValueError(f'Unknown plotting option: {dymos_options["plots"]}') + raise ValueError(f'Unknown plotting option: {dymos_options["plots"]}') \ No newline at end of file From c2e8932029ba9ac60ee8809063ae7e55a5484020 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Sun, 26 Mar 2023 19:47:48 -0400 Subject: [PATCH 24/35] more cleanup --- ...st_brachistochrone_control_rate_targets.py | 39 +------------------ .../test/test_simulate_units.py | 4 +- .../doc/test_doc_two_phase_cannonball.py | 17 -------- .../test_two_burn_orbit_raise_linkages.py | 4 +- dymos/phase/test/test_phase.py | 4 +- dymos/phase/test/test_time_targets.py | 2 + dymos/phase/test/test_timeseries.py | 2 +- dymos/test/test_run_problem.py | 13 ++++--- .../explicit_shooting/ode_evaluation_group.py | 2 +- .../test/test_explicit_shooting.py | 2 +- .../test/test_ode_integration_comp.py | 2 +- dymos/transcriptions/transcription_base.py | 7 ++-- .../test/test_timeseries_plots.py | 13 +++++++ .../timeseries/bokeh_timeseries_report.py | 11 +++--- .../timeseries/timeseries_report.py | 27 ------------- dymos/visualization/timeseries_plots.py | 2 +- 16 files changed, 44 insertions(+), 107 deletions(-) delete mode 100644 dymos/visualization/timeseries/timeseries_report.py diff --git a/dymos/examples/brachistochrone/test/test_brachistochrone_control_rate_targets.py b/dymos/examples/brachistochrone/test/test_brachistochrone_control_rate_targets.py index 86d294bc1..a31c8e557 100644 --- a/dymos/examples/brachistochrone/test/test_brachistochrone_control_rate_targets.py +++ b/dymos/examples/brachistochrone/test/test_brachistochrone_control_rate_targets.py @@ -927,48 +927,11 @@ def test_brachistochrone_polynomial_control_rate_targets_gauss_lobatto(self): p['phase0.polynomial_controls:theta'] = [0, 10, 40, 60, 80, 100] # Solve for the optimal trajectory - p.run_driver() + dm.run_problem(p, simulate=True) # Test the results assert_near_equal(p.get_val('phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-3) - # Generate the explicitly simulated trajectory - exp_out = phase.simulate() - - fig, ax = plt.subplots() - fig.suptitle('Brachistochrone Solution') - - x_imp = p.get_val('phase0.timeseries.states:x') - y_imp = p.get_val('phase0.timeseries.states:y') - - x_exp = exp_out.get_val('phase0.timeseries.states:x') - y_exp = exp_out.get_val('phase0.timeseries.states:y') - - ax.plot(x_imp, y_imp, 'ro', label='solution') - ax.plot(x_exp, y_exp, 'b-', label='simulated') - - ax.set_xlabel('x (m)') - ax.set_ylabel('y (m)') - ax.grid(True) - ax.legend(loc='upper right') - - fig, ax = plt.subplots() - - t_imp = p.get_val('phase0.timeseries.time') - theta_imp = p.get_val('phase0.timeseries.polynomial_control_rates:theta_rate') - t_exp = exp_out.get_val('phase0.timeseries.time') - theta_exp = exp_out.get_val('phase0.timeseries.polynomial_control_rates:theta_rate') - - ax.plot(t_imp, theta_imp, 'ro', label='solution') - ax.plot(t_exp, theta_exp, 'b-', label='simulated') - - ax.set_xlabel('time (s)') - ax.set_ylabel(r'$\theta$ (deg)') - ax.grid(True) - ax.legend(loc='upper right') - - plt.show() - if __name__ == '__main__': # pragma: no cover unittest.main() diff --git a/dymos/examples/brachistochrone/test/test_simulate_units.py b/dymos/examples/brachistochrone/test/test_simulate_units.py index 78a513d01..b998e46cf 100644 --- a/dymos/examples/brachistochrone/test/test_simulate_units.py +++ b/dymos/examples/brachistochrone/test/test_simulate_units.py @@ -92,8 +92,8 @@ def test_brachistochrone_simulate_units(self): sol_case = om.CaseReader('dymos_solution.db').get_case('final') sim_case = om.CaseReader('dymos_simulation.db').get_case('final') - assert_near_equal(sim_case.get_val('traj.phase0.timeseries.parameters:g', units='m/s**2')[0], - sol_case.get_val('traj.phase0.timeseries.parameters:g', units='m/s**2')[0]) + assert_near_equal(sim_case.get_val('traj.phase0.parameter_vals:g', units='m/s**2')[0], + sol_case.get_val('traj.phase0.parameter_vals:g', units='m/s**2')[0]) assert_near_equal(sol_case.get_val('traj.phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-4) assert_near_equal(sim_case.get_val('traj.phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-4) diff --git a/dymos/examples/cannonball/doc/test_doc_two_phase_cannonball.py b/dymos/examples/cannonball/doc/test_doc_two_phase_cannonball.py index 450e7f016..c883b37a4 100644 --- a/dymos/examples/cannonball/doc/test_doc_two_phase_cannonball.py +++ b/dymos/examples/cannonball/doc/test_doc_two_phase_cannonball.py @@ -324,23 +324,6 @@ def compute(self, inputs, outputs): axes[i].plot(time_exp['ascent'], x_exp['ascent'], 'b--') axes[i].plot(time_exp['descent'], x_exp['descent'], 'r--') - params = ['m', 'S'] - fig, axes = plt.subplots(nrows=6, ncols=1, figsize=(12, 6)) - for i, param in enumerate(params): - p_imp = { - 'ascent': p.get_val(f'traj.ascent.timeseries.parameters:{param}'), - 'descent': p.get_val(f'traj.descent.timeseries.parameters:{param}')} - - p_exp = {'ascent': exp_out.get_val(f'traj.ascent.timeseries.parameters:{param}'), - 'descent': exp_out.get_val(f'traj.descent.timeseries.parameters:{param}')} - - axes[i].set_ylabel(param) - - axes[i].plot(time_imp['ascent'], p_imp['ascent'], 'bo') - axes[i].plot(time_imp['descent'], p_imp['descent'], 'ro') - axes[i].plot(time_exp['ascent'], p_exp['ascent'], 'b--') - axes[i].plot(time_exp['descent'], p_exp['descent'], 'r--') - plt.show() diff --git a/dymos/examples/finite_burn_orbit_raise/test/test_two_burn_orbit_raise_linkages.py b/dymos/examples/finite_burn_orbit_raise/test/test_two_burn_orbit_raise_linkages.py index a1d56633c..f73613289 100644 --- a/dymos/examples/finite_burn_orbit_raise/test/test_two_burn_orbit_raise_linkages.py +++ b/dymos/examples/finite_burn_orbit_raise/test/test_two_burn_orbit_raise_linkages.py @@ -625,8 +625,8 @@ def test_two_burn_orbit_raise_link_control_to_param(self): tolerance=0.05) burn1_u1_final = p.get_val('traj.burn1.timeseries.controls:u1')[-1, ...] - coast_u1_initial = p.get_val('traj.coast.timeseries.parameters:u1')[0, ...] - coast_u1_final = p.get_val('traj.coast.timeseries.parameters:u1')[-1, ...] + coast_u1_initial = p.get_val('traj.coast.parameter_vals:u1')[0, ...] + coast_u1_final = p.get_val('traj.coast.parameter_vals:u1')[-1, ...] burn2_u1_initial = p.get_val('traj.burn2.timeseries.controls:u1')[0, ...] assert_near_equal(burn1_u1_final - coast_u1_initial, 0.0, 1e-12) diff --git a/dymos/phase/test/test_phase.py b/dymos/phase/test/test_phase.py index 6c1f22450..bbe26b286 100644 --- a/dymos/phase/test/test_phase.py +++ b/dymos/phase/test/test_phase.py @@ -748,9 +748,9 @@ def test_parameter_path_constraint(self): assert_near_equal(p.get_val('phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-4) - assert_near_equal(p.get_val('phase0.timeseries.parameters:g')[0], 9.80665, + assert_near_equal(p.get_val('phase0.parameter_vals:g')[0], 9.80665, tolerance=1.0E-6) - assert_near_equal(p.get_val('phase0.timeseries.parameters:g')[-1], 9.80665, + assert_near_equal(p.get_val('phase0.parameter_vals:g')[-1], 9.80665, tolerance=1.0E-6) diff --git a/dymos/phase/test/test_time_targets.py b/dymos/phase/test/test_time_targets.py index 1a4054558..8ce1bf053 100644 --- a/dymos/phase/test/test_time_targets.py +++ b/dymos/phase/test/test_time_targets.py @@ -128,6 +128,8 @@ def _make_problem(self, transcription, num_seg, transcription_order=3, input_ini phase.add_boundary_constraint('x', loc='final', equals=10) phase.add_boundary_constraint('y', loc='final', equals=5) + phase.timeseries_options['include_t_phase'] = True + # Minimize time at the end of the phase phase.add_objective(time_name, loc='final', scaler=10) diff --git a/dymos/phase/test/test_timeseries.py b/dymos/phase/test/test_timeseries.py index cc8bb3d54..288162ed4 100644 --- a/dymos/phase/test/test_timeseries.py +++ b/dymos/phase/test/test_timeseries.py @@ -778,4 +778,4 @@ def test_timeseries_expr_gl(self): if __name__ == '__main__': # pragma: no cover - unittest.main() \ No newline at end of file + unittest.main() diff --git a/dymos/test/test_run_problem.py b/dymos/test/test_run_problem.py index f9d07b063..f889bae0d 100755 --- a/dymos/test/test_run_problem.py +++ b/dymos/test/test_run_problem.py @@ -691,15 +691,17 @@ def test_run_brachistochrone_problem_make_plots(self): dm.options['plots'] = plots_cache def test_run_brachistochrone_problem_make_plots_set_plot_dir(self): + _cache = dm.options['plots'] + dm.options['plots'] = 'matplotlib' + dm.run_problem(self.p, make_plots=True, plot_dir="test_plot_dir") plot_dir = pathlib.Path(_get_reports_dir(self.p)) - for varname in ['time_phase', 'states:x', 'state_rates:x', 'states:y', - 'state_rates:y', 'states:v', - 'state_rates:v', 'controls:theta', 'control_rates:theta_rate', - 'control_rates:theta_rate2']: + for varname in ['states:x', 'states:y', 'states:v', 'controls:theta']: plotfile = plot_dir.joinpath('test_plot_dir', f'{varname.replace(":","_")}.png') - self.assertTrue(plotfile.exists(), msg = str(plotfile) + ' does not exist.') + self.assertTrue(plotfile.exists(), msg=str(plotfile) + ' does not exist.') + + dm.options['plots'] = _cache def test_run_brachistochrone_problem_do_not_make_plots(self): dm.run_problem(self.p, make_plots=False) @@ -755,6 +757,7 @@ def test_run_brachistochrone_problem_plot_no_simulation_record_file_given(self): dm.options['plots'] = plots_cache + @use_tempdirs class TestSimulateArrayParam(unittest.TestCase): diff --git a/dymos/transcriptions/explicit_shooting/ode_evaluation_group.py b/dymos/transcriptions/explicit_shooting/ode_evaluation_group.py index 4a0e0e411..1387f0411 100644 --- a/dymos/transcriptions/explicit_shooting/ode_evaluation_group.py +++ b/dymos/transcriptions/explicit_shooting/ode_evaluation_group.py @@ -215,7 +215,7 @@ def _configure_params(self): self.add_design_var(var_name) if options['static_target']: - src_idxs = om.slicer[0, ...] + src_idxs = None else: src_rows = np.zeros(vec_size, dtype=int) src_idxs = om.slicer[src_rows, ...] diff --git a/dymos/transcriptions/explicit_shooting/test/test_explicit_shooting.py b/dymos/transcriptions/explicit_shooting/test/test_explicit_shooting.py index 61104b0eb..46f475ff1 100644 --- a/dymos/transcriptions/explicit_shooting/test/test_explicit_shooting.py +++ b/dymos/transcriptions/explicit_shooting/test/test_explicit_shooting.py @@ -5,7 +5,7 @@ import openmdao.api as om import dymos as dm -import dymos.options as dymos_options +from dymos import options as dymos_options from openmdao.utils.assert_utils import assert_check_partials, assert_near_equal from openmdao.utils.testing_utils import use_tempdirs diff --git a/dymos/transcriptions/explicit_shooting/test/test_ode_integration_comp.py b/dymos/transcriptions/explicit_shooting/test/test_ode_integration_comp.py index c1f93dc60..5c960fed6 100644 --- a/dymos/transcriptions/explicit_shooting/test/test_ode_integration_comp.py +++ b/dymos/transcriptions/explicit_shooting/test/test_ode_integration_comp.py @@ -3,7 +3,7 @@ import numpy as np import openmdao.api as om import dymos as dm -import dymos.options as dymos_options +from dymos import options as dymos_options from openmdao.utils.assert_utils import assert_check_partials, assert_near_equal from dymos.examples.brachistochrone.brachistochrone_ode import BrachistochroneODE diff --git a/dymos/transcriptions/transcription_base.py b/dymos/transcriptions/transcription_base.py index 8e8e5fce5..15c5093e5 100644 --- a/dymos/transcriptions/transcription_base.py +++ b/dymos/transcriptions/transcription_base.py @@ -220,15 +220,16 @@ def setup_polynomial_controls(self, phase): for name, options in phase.polynomial_control_options.items(): for ts_name, ts_options in phase._timeseries.items(): - if f'polynomial_controls:{name}' not in ts_options['outputs']: + if f'{prefix}{name}' not in ts_options['outputs']: phase.add_timeseries_output(name, output_name=f'{prefix}{name}', timeseries=ts_name) - if f'polynomial_control_rates:{name}_rate' not in ts_options['outputs'] and \ + if f'{rate_prefix}{name}_rate' not in ts_options['outputs'] and \ (phase.timeseries_options['include_control_rates'] or options['rate_targets']): phase.add_timeseries_output(f'{name}_rate', output_name=f'{rate_prefix}{name}_rate', timeseries=ts_name) - if f'polynomial_control_rates:{name}_rate2' not in ts_options['outputs'] and \ + if f'{rate_prefix}{name}_rate2' not in ts_options['outputs'] and \ (phase.timeseries_options['include_control_rates'] or options['rate2_targets']): + print(name, rate_prefix, ts_name) phase.add_timeseries_output(f'{name}_rate2', output_name=f'{rate_prefix}{name}_rate2', timeseries=ts_name) diff --git a/dymos/visualization/test/test_timeseries_plots.py b/dymos/visualization/test/test_timeseries_plots.py index 80c75ca1d..4c68b5a0c 100644 --- a/dymos/visualization/test/test_timeseries_plots.py +++ b/dymos/visualization/test/test_timeseries_plots.py @@ -117,11 +117,24 @@ def test_brachistochrone_timeseries_plots_solution_only_set_solution_record_file dm.options['plots'] = temp def test_brachistochrone_timeseries_plots_solution_and_simulation(self): + temp = dm.options['plots'] + dm.options['plots'] = 'matplotlib' + dm.run_problem(self.p, simulate=True, make_plots=False, simulation_record_file='simulation_record_file.db') timeseries_plots('dymos_solution.db', simulation_record_file='simulation_record_file.db', problem=self.p) + plot_dir = pathlib.Path(_get_reports_dir(self.p)).joinpath("plots").resolve() + self.assertTrue(plot_dir.joinpath('states_x.png').exists()) + self.assertTrue(plot_dir.joinpath('states_y.png').exists()) + self.assertTrue(plot_dir.joinpath('states_v.png').exists()) + self.assertTrue(plot_dir.joinpath('controls_theta.png').exists()) + self.assertTrue(plot_dir.joinpath('control_rates_theta_rate.png').exists()) + self.assertTrue(plot_dir.joinpath('control_rates_theta_rate2.png').exists()) + + dm.options['plots'] = temp + def test_brachistochrone_timeseries_plots_set_plot_dir(self): temp = dm.options['plots'] diff --git a/dymos/visualization/timeseries/bokeh_timeseries_report.py b/dymos/visualization/timeseries/bokeh_timeseries_report.py index ff0e30f21..0bb0207c5 100644 --- a/dymos/visualization/timeseries/bokeh_timeseries_report.py +++ b/dymos/visualization/timeseries/bokeh_timeseries_report.py @@ -27,7 +27,7 @@ break; } } - return ((tags.includes('sol') && kinds_to_show.includes(0)) || + return ((tags.includes('sol') && kinds_to_show.includes(0)) || (tags.includes('sim') && kinds_to_show.includes(1))) && phases_to_show.includes(renderer_phase); } @@ -74,7 +74,7 @@ break; } } - return ((tags.includes('sol') && kinds_to_show.includes(0)) || + return ((tags.includes('sol') && kinds_to_show.includes(0)) || (tags.includes('sim') && kinds_to_show.includes(1))) && phases_to_show.includes(renderer_phase); } @@ -157,7 +157,8 @@ def _load_data_sources(prob, solution_record_file=None, simulation_record_file=N phase_sim_data = data_dict[traj_name]['sim_data_by_phase'][phase_name] = {} ts_units_dict = data_dict[traj_name]['timeseries_units'] - param_outputs = {op: meta for op, meta in outputs.items() if op.startswith(f'{phase.pathname}.param_comp.parameter_vals')} + param_outputs = {op: meta for op, meta in outputs.items() + if op.startswith(f'{phase.pathname}.param_comp.parameter_vals')} param_case = sol_case if sol_case else sim_case for output_name in sorted(param_outputs.keys(), key=str.casefold): @@ -260,7 +261,7 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi for var_name in sorted(ts_units_dict.keys(), key=str.casefold): fig_kwargs = {'x_range': x_range} if x_range is not None else {} - tool_tips = [(f'{x_name}', '$x'), (f'{var_name}', '$y')] + tool_tips = [(f'{x_name}', '$x'), (f'{var_name}', '$y')] fig = figure(tools='pan,box_zoom,xwheel_zoom,hover,undo,reset,save', tooltips=tool_tips, @@ -354,5 +355,3 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi save(report_layout, filename=report_path, title=f'trajectory results for {traj_name}', resources=bokeh_resources.INLINE) - - diff --git a/dymos/visualization/timeseries/timeseries_report.py b/dymos/visualization/timeseries/timeseries_report.py deleted file mode 100644 index 91d1b25ee..000000000 --- a/dymos/visualization/timeseries/timeseries_report.py +++ /dev/null @@ -1,27 +0,0 @@ -import dymos as dm -from collections import OrderedDict -import inspect -import re -from pathlib import Path -from openmdao.visualization.htmlpp import HtmlPreprocessor -import openmdao.utils.reports_system as rptsys - -_default_timeseries_report_title = 'Dymos Timeseries Report' -_default_timeseries_report_filename = 'timeseries_report.html' - -def _run_timeseries_report(prob): - """ Function invoked by the reports system """ - - # Find all Trajectory objects in the Problem. Usually, there's only one - for traj in prob.model.system_iter(include_self=True, recurse=True, typ=dm.Trajectory): - report_filename = f'{traj.pathname}_{_default_timeseries_report_filename}' - report_path = str(Path(prob.get_reports_dir()) / report_filename) - create_timeseries_report(traj, report_path) - - -# def _timeseries_report_register(): -# rptsys.register_report('dymos.timeseries', _run_timeseries_report, _default_timeseries_report_title, -# 'prob', 'run_driver', 'post') -# rptsys.register_report('dymos.timeseries', _run_timeseries_report, _default_timeseries_report_title, -# 'prob', 'run_model', 'post') -# rptsys._default_reports.append('dymos.timeseries') diff --git a/dymos/visualization/timeseries_plots.py b/dymos/visualization/timeseries_plots.py index 6aac87991..51d268b42 100644 --- a/dymos/visualization/timeseries_plots.py +++ b/dymos/visualization/timeseries_plots.py @@ -443,4 +443,4 @@ def timeseries_plots(solution_recorder_filename, simulation_record_file=None, pl with open(htmlpath, 'w', encoding='utf-8') as f: f.write(image2html(fpath.name)) else: - raise ValueError(f'Unknown plotting option: {dymos_options["plots"]}') \ No newline at end of file + raise ValueError(f'Unknown plotting option: {dymos_options["plots"]}') From 6768ab2d98a5cb6230a679ddf04ece41547925ba Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Mon, 27 Mar 2023 11:01:03 -0400 Subject: [PATCH 25/35] more cleanup --- .../test/test_balanced_field_length.py | 3 +- .../test/test_path_constraints.py | 37 ++++++++++++------- .../test/test_error_estimation.py | 2 + dymos/phase/phase.py | 2 +- setup.py | 1 + 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/dymos/examples/balanced_field/test/test_balanced_field_length.py b/dymos/examples/balanced_field/test/test_balanced_field_length.py index c2e83d45c..19c1aa873 100644 --- a/dymos/examples/balanced_field/test/test_balanced_field_length.py +++ b/dymos/examples/balanced_field/test/test_balanced_field_length.py @@ -10,7 +10,7 @@ from dymos.examples.balanced_field.balanced_field_ode import BalancedFieldODEComp -# @use_tempdirs +@use_tempdirs class TestBalancedFieldLengthRestart(unittest.TestCase): def _make_problem(self): @@ -218,6 +218,7 @@ def _make_problem(self): return p + @require_pyoptsparse(optimizer='IPOPT') def test_make_plots(self): p = self._make_problem() dm.run_problem(p, run_driver=True, simulate=True, make_plots=True) diff --git a/dymos/examples/brachistochrone/test/test_path_constraints.py b/dymos/examples/brachistochrone/test/test_path_constraints.py index 53fcc731e..59429725b 100644 --- a/dymos/examples/brachistochrone/test/test_path_constraints.py +++ b/dymos/examples/brachistochrone/test/test_path_constraints.py @@ -1,5 +1,6 @@ import unittest +import numpy as np from openmdao.utils.testing_utils import use_tempdirs, require_pyoptsparse @@ -99,14 +100,16 @@ def test_control_rate2_path_constraint_gl(self): p['phase0.controls:theta'] = phase.interp('theta', [5, 100.5]) # Solve for the optimal trajectory - p.run_driver() + failed = p.run_driver() + self.assertFalse(failed, msg='optimization failed') # Test the results - assert_near_equal(p.get_val('phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-3) + rate_path = 'control_rates:theta_rate2' if dm.options['use_timeseries_prefix'] else 'theta_rate2' + self.assertGreaterEqual(np.min(p.get_val(f'phase0.timeseries.{rate_path}')), -200.000001) + self.assertLessEqual(np.max(p.get_val(f'phase0.timeseries.{rate_path}')), 200.000001) def test_control_rate_path_constraint_radau(self): import openmdao.api as om - from openmdao.utils.assert_utils import assert_near_equal import dymos as dm from dymos.examples.brachistochrone.brachistochrone_ode import BrachistochroneODE @@ -148,10 +151,13 @@ def test_control_rate_path_constraint_radau(self): p['phase0.controls:theta'] = phase.interp('theta', ys=[0.9, 101.5]) # Solve for the optimal trajectory - p.run_driver() + failed = p.run_driver() + self.assertFalse(failed, msg='optimization failed') # Test the results - assert_near_equal(p.get_val('phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-3) + rate_path = 'control_rates:theta_rate' if dm.options['use_timeseries_prefix'] else 'theta_rate' + self.assertGreaterEqual(np.min(p.get_val(f'phase0.timeseries.{rate_path}')), -0.000001) + self.assertLessEqual(np.max(p.get_val(f'phase0.timeseries.{rate_path}')), 100.000001) def test_control_rate2_path_constraint_radau(self): import openmdao.api as om @@ -199,15 +205,17 @@ def test_control_rate2_path_constraint_radau(self): p['phase0.controls:theta'] = phase.interp('theta', [5, 100.5]) # Solve for the optimal trajectory - p.run_driver() + failed = p.run_driver() + self.assertFalse(failed, msg='optimization failed') # Test the results - assert_near_equal(p.get_val('phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-3) + rate_path = 'control_rates:theta_rate2' if dm.options['use_timeseries_prefix'] else 'theta_rate2' + self.assertGreaterEqual(np.min(p.get_val(f'phase0.timeseries.{rate_path}')), -200.000001) + self.assertLessEqual(np.max(p.get_val(f'phase0.timeseries.{rate_path}')), 200.000001) @require_pyoptsparse(optimizer='IPOPT') def test_state_path_constraint_radau(self): import openmdao.api as om - from openmdao.utils.assert_utils import assert_near_equal import dymos as dm from dymos.examples.brachistochrone.brachistochrone_ode import BrachistochroneODE @@ -253,15 +261,16 @@ def test_state_path_constraint_radau(self): p['traj0.phase0.controls:theta'] = phase.interp('theta', ys=[0.9, 101.5]) # Solve for the optimal trajectory - p.run_driver() + failed = p.run_driver() + self.assertFalse(failed, msg='optimization failed') # Test the results - assert_near_equal(p.get_val('traj0.phase0.timeseries.time')[-1], 1.8029, tolerance=1.0E-3) + state_path = 'states:y' if dm.options['use_timeseries_prefix'] else 'x' + self.assertGreaterEqual(np.min(p.get_val(f'traj0.phase0.timeseries.{state_path}')), 4.999999) @require_pyoptsparse(optimizer='IPOPT') def test_state_path_constraint_gl(self): import openmdao.api as om - from openmdao.utils.assert_utils import assert_near_equal import dymos as dm from dymos.examples.brachistochrone.brachistochrone_ode import BrachistochroneODE @@ -307,10 +316,12 @@ def test_state_path_constraint_gl(self): p['traj0.phase0.controls:theta'] = phase.interp('theta', ys=[0.9, 101.5]) # Solve for the optimal trajectory - p.run_driver() + failed = p.run_driver() + self.assertFalse(failed, msg='optimization failed') # Test the results - assert_near_equal(p.get_val('traj0.phase0.timeseries.time')[-1], 1.8029, tolerance=1.0E-3) + state_path = 'states:y' if dm.options['use_timeseries_prefix'] else 'x' + self.assertGreaterEqual(np.min(p.get_val(f'traj0.phase0.timeseries.{state_path}')), 4.999999) if __name__ == '__main__': # pragma: no cover diff --git a/dymos/grid_refinement/test/test_error_estimation.py b/dymos/grid_refinement/test/test_error_estimation.py index e44f89b91..59cb33f50 100644 --- a/dymos/grid_refinement/test/test_error_estimation.py +++ b/dymos/grid_refinement/test/test_error_estimation.py @@ -54,6 +54,8 @@ def _run_brachistochrone(self, transcription_class=dm.Radau, control_type='contr # Minimize time at the end of the phase phase.add_objective('time_phase', loc='final', scaler=10) + phase.timeseries_options['include_state_rates'] = True + p.setup() p['traj0.phase0.t_initial'] = 0.0 diff --git a/dymos/phase/phase.py b/dymos/phase/phase.py index 96f173433..f6274656a 100644 --- a/dymos/phase/phase.py +++ b/dymos/phase/phase.py @@ -2025,7 +2025,7 @@ def interpolate(self, xs=None, ys=None, nodes='all', kind='linear', axis=0): res = res.T return res - def interp(self, name=None, ys=None, xs=None, nodes=None, kind='linear', axis=0): + def interp(self, name=None, ys=None, xs=None, nodes=None, kind='linear', axis=0, respect_bounds=True): """ Interpolate values onto the given subset of nodes in the phase. diff --git a/setup.py b/setup.py index 63a29e450..a3be646ab 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,7 @@ optional_dependencies = { 'docs': [ 'matplotlib', + 'bokeh', 'jupyter', 'jupyter-book==0.14', 'nbconvert', From 169c3a1f5188ad497d5b7364be5e184ff748ed54 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Mon, 27 Mar 2023 14:27:01 -0400 Subject: [PATCH 26/35] more cleanup --- .../examples/vanderpol/vanderpol.ipynb | 8 ---- .../test/test_trajectory_parameters.py | 47 ++++++++++++------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/docs/dymos_book/examples/vanderpol/vanderpol.ipynb b/docs/dymos_book/examples/vanderpol/vanderpol.ipynb index e611beb53..ae3a985df 100644 --- a/docs/dymos_book/examples/vanderpol/vanderpol.ipynb +++ b/docs/dymos_book/examples/vanderpol/vanderpol.ipynb @@ -420,14 +420,6 @@ " 'traj.phase0.timeseries.controls:u',\n", " 'time (s)',\n", " 'control u'),\n", - " ('traj.phase0.timeseries.time',\n", - " 'traj.phase0.timeseries.state_rates:x1',\n", - " 'time (s)',\n", - " '$\\dot{x}_1$ (V)'),\n", - " ('traj.phase0.timeseries.time',\n", - " 'traj.phase0.timeseries.state_rates:x0',\n", - " 'time (s)',\n", - " '$\\dot{x}_0$ (V/s)'),\n", " ],\n", " title='Van Der Pol Simulation',\n", " p_sol=sol, p_sim=sim);\n", diff --git a/dymos/trajectory/test/test_trajectory_parameters.py b/dymos/trajectory/test/test_trajectory_parameters.py index ef7aaf97a..6d9084d9b 100644 --- a/dymos/trajectory/test/test_trajectory_parameters.py +++ b/dymos/trajectory/test/test_trajectory_parameters.py @@ -158,7 +158,7 @@ def make_traj(transcription='gauss-lobatto', transcription_order=3, compressed=F def two_burn_orbit_raise_problem(transcription='gauss-lobatto', optimizer='SLSQP', r_target=3.0, - transcription_order=3, compressed=False, + transcription_order=3, compressed=False, run_driver=True, simulate=True, show_output=True, connected=False, param_mode='param_sequence'): p = om.Problem(model=om.Group()) @@ -234,7 +234,7 @@ def two_burn_orbit_raise_problem(transcription='gauss-lobatto', optimizer='SLSQP p.set_val('orbit_transfer.burn2.controls:u1', val=burn2.interp('u1', [0, 0])) - dm.run_problem(p, simulate=True) + dm.run_problem(p, run_driver=run_driver, simulate=simulate) return p @@ -250,12 +250,17 @@ def test_param_explicit_connections_to_sequence(self): automatically be added. """ p = two_burn_orbit_raise_problem(transcription='gauss-lobatto', transcription_order=3, - compressed=False, optimizer='IPOPT', - show_output=False, param_mode='param_sequence') + compressed=False, optimizer='IPOPT', run_driver=False, + simulate=False, show_output=False, param_mode='param_sequence') - if p.model.orbit_transfer.phases.burn2 in p.model.orbit_transfer.phases._subsystems_myproc: - assert_near_equal(p.get_val('orbit_transfer.burn2.states:deltav')[-1], 0.3995, - tolerance=2.0E-3) + traj_c = p.get_val('orbit_transfer.parameter_vals:c') + burn1_c = p.get_val('orbit_transfer.burn1.parameter_vals:c') + coast_c = p.get_val('orbit_transfer.coast.parameter_vals:c') + burn2_c = p.get_val('orbit_transfer.burn2.parameter_vals:c') + + assert_near_equal(burn1_c, traj_c) + assert_near_equal(coast_c, traj_c) + assert_near_equal(burn2_c, traj_c) @require_pyoptsparse(optimizer='IPOPT') def test_param_explicit_connections_to_sequence_missing_phase(self): @@ -264,13 +269,18 @@ def test_param_explicit_connections_to_sequence_missing_phase(self): that we attempt to connect to an existing input variable in that phase of the same name. """ p = two_burn_orbit_raise_problem(transcription='gauss-lobatto', transcription_order=3, - compressed=False, optimizer='IPOPT', + compressed=False, optimizer='IPOPT', run_driver=False, simulate=False, show_output=False, param_mode='param_sequence_missing_phase') - if p.model.orbit_transfer.phases.burn2 in p.model.orbit_transfer.phases._subsystems_myproc: - assert_near_equal(p.get_val('orbit_transfer.burn2.states:deltav')[-1], 0.3995, - tolerance=2.0E-3) + traj_c = p.get_val('orbit_transfer.parameter_vals:c') + burn1_c = p.get_val('orbit_transfer.burn1.parameter_vals:c') + coast_c = p.get_val('orbit_transfer.coast.parameter_vals:c') + burn2_c = p.get_val('orbit_transfer.burn2.parameter_vals:c') + + assert_near_equal(burn1_c, traj_c) + assert_near_equal(coast_c, traj_c) + assert_near_equal(burn2_c, traj_c) @require_pyoptsparse(optimizer='IPOPT') def test_param_no_targets(self): @@ -279,13 +289,18 @@ def test_param_no_targets(self): that we attempt to connect to an existing input variable in that phase of the same name. """ p = two_burn_orbit_raise_problem(transcription='gauss-lobatto', transcription_order=3, - compressed=False, optimizer='IPOPT', - show_output=False, + compressed=False, optimizer='IPOPT', run_driver=False, + show_output=False, simulate=False, param_mode='param_no_targets') - if p.model.orbit_transfer.phases.burn2 in p.model.orbit_transfer.phases._subsystems_myproc: - assert_near_equal(p.get_val('orbit_transfer.burn2.states:deltav')[-1], 0.3995, - tolerance=2.0E-3) + traj_c = p.get_val('orbit_transfer.parameter_vals:c') + burn1_c = p.get_val('orbit_transfer.burn1.parameter_vals:c') + coast_c = p.get_val('orbit_transfer.coast.parameter_vals:c') + burn2_c = p.get_val('orbit_transfer.burn2.parameter_vals:c') + + assert_near_equal(burn1_c, traj_c) + assert_near_equal(coast_c, traj_c) + assert_near_equal(burn2_c, traj_c) @use_tempdirs From b64f8ec6879de0a3f7d966044b459e91992152db Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Mon, 27 Mar 2023 15:07:14 -0400 Subject: [PATCH 27/35] fixed an issue with writing the trajectory results report when bokeh is not available. --- dymos/utils/testing_utils.py | 1 + dymos/visualization/timeseries/bokeh_timeseries_report.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/dymos/utils/testing_utils.py b/dymos/utils/testing_utils.py index e952e309f..2d2aec442 100644 --- a/dymos/utils/testing_utils.py +++ b/dymos/utils/testing_utils.py @@ -1,3 +1,4 @@ +import builtins import io import os diff --git a/dymos/visualization/timeseries/bokeh_timeseries_report.py b/dymos/visualization/timeseries/bokeh_timeseries_report.py index 0bb0207c5..1bd18f682 100644 --- a/dymos/visualization/timeseries/bokeh_timeseries_report.py +++ b/dymos/visualization/timeseries/bokeh_timeseries_report.py @@ -233,7 +233,7 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi with open(report_path, 'wb') as f: f.write("\n\n \nError: bokeh not available\n \n" "This report requires bokeh but bokeh was not available in this python installation.\n" - "") + "".encode()) continue param_tables = [] From 0b40dbd80676ac8e8733c47c0f4c795964475819 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Mon, 27 Mar 2023 15:21:57 -0400 Subject: [PATCH 28/35] docstring linting --- dymos/phase/phase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dymos/phase/phase.py b/dymos/phase/phase.py index f6274656a..96f173433 100644 --- a/dymos/phase/phase.py +++ b/dymos/phase/phase.py @@ -2025,7 +2025,7 @@ def interpolate(self, xs=None, ys=None, nodes='all', kind='linear', axis=0): res = res.T return res - def interp(self, name=None, ys=None, xs=None, nodes=None, kind='linear', axis=0, respect_bounds=True): + def interp(self, name=None, ys=None, xs=None, nodes=None, kind='linear', axis=0): """ Interpolate values onto the given subset of nodes in the phase. From 1b38472412d8ae7d5726e9b43330827a1612dd9a Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Tue, 28 Mar 2023 09:25:34 -0400 Subject: [PATCH 29/35] Skip a few regridding tests where the interpolation of the results were throwing design variables out of bounds. --- .../balanced_field/test/test_balanced_field_length.py | 5 +++++ setup.py | 1 + 2 files changed, 6 insertions(+) diff --git a/dymos/examples/balanced_field/test/test_balanced_field_length.py b/dymos/examples/balanced_field/test/test_balanced_field_length.py index 19c1aa873..870c98a67 100644 --- a/dymos/examples/balanced_field/test/test_balanced_field_length.py +++ b/dymos/examples/balanced_field/test/test_balanced_field_length.py @@ -1,7 +1,9 @@ +from packaging.version import Version import unittest import numpy as np +import openmdao import openmdao.api as om from openmdao.utils.testing_utils import use_tempdirs, require_pyoptsparse from openmdao.utils.assert_utils import assert_near_equal @@ -219,11 +221,13 @@ def _make_problem(self): return p @require_pyoptsparse(optimizer='IPOPT') + @unittest.skipUnless(Version(openmdao.__version__) > Version("3.23")) def test_make_plots(self): p = self._make_problem() dm.run_problem(p, run_driver=True, simulate=True, make_plots=True) @require_pyoptsparse(optimizer='IPOPT') + @unittest.skipUnless(Version(openmdao.__version__) > Version("3.23")) def test_restart_from_sol(self): p = self._make_problem() dm.run_problem(p, run_driver=True, simulate=False) @@ -244,6 +248,7 @@ def test_restart_from_sol(self): assert_near_equal(sim_results.get_val('traj.rto.timeseries.states:r')[-1], 2016, tolerance=0.01) @require_pyoptsparse(optimizer='IPOPT') + @unittest.skipUnless(Version(openmdao.__version__) > Version("3.23")) def test_restart_from_sim(self): p = self._make_problem() dm.run_problem(p, run_driver=True, simulate=True) diff --git a/setup.py b/setup.py index a3be646ab..f977ab96d 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ 'ipython' ], 'test': [ + 'packaging', 'pycodestyle', 'testflo>=1.3.6', 'matplotlib', From 141740f42b831fd90cd9f9134f62ecc00775669c Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Fri, 31 Mar 2023 21:01:45 -0400 Subject: [PATCH 30/35] Fixed skip tests for tests that require invalid_desvar_behavior --- .../balanced_field/test/test_balanced_field_length.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/dymos/examples/balanced_field/test/test_balanced_field_length.py b/dymos/examples/balanced_field/test/test_balanced_field_length.py index 870c98a67..7e4765521 100644 --- a/dymos/examples/balanced_field/test/test_balanced_field_length.py +++ b/dymos/examples/balanced_field/test/test_balanced_field_length.py @@ -221,13 +221,15 @@ def _make_problem(self): return p @require_pyoptsparse(optimizer='IPOPT') - @unittest.skipUnless(Version(openmdao.__version__) > Version("3.23")) + @unittest.skipUnless(Version(openmdao.__version__) > Version("3.23"), + reason='Test requires OpenMDAO 3.23.0 or later.') def test_make_plots(self): p = self._make_problem() dm.run_problem(p, run_driver=True, simulate=True, make_plots=True) @require_pyoptsparse(optimizer='IPOPT') - @unittest.skipUnless(Version(openmdao.__version__) > Version("3.23")) + @unittest.skipUnless(Version(openmdao.__version__) > Version("3.23"), + reason='Test requires OpenMDAO 3.23.0 or later.') def test_restart_from_sol(self): p = self._make_problem() dm.run_problem(p, run_driver=True, simulate=False) @@ -248,7 +250,8 @@ def test_restart_from_sol(self): assert_near_equal(sim_results.get_val('traj.rto.timeseries.states:r')[-1], 2016, tolerance=0.01) @require_pyoptsparse(optimizer='IPOPT') - @unittest.skipUnless(Version(openmdao.__version__) > Version("3.23")) + @unittest.skipUnless(Version(openmdao.__version__) > Version("3.23"), + reason='Test requires OpenMDAO 3.23.0 or later.') def test_restart_from_sim(self): p = self._make_problem() dm.run_problem(p, run_driver=True, simulate=True) From 7f70cb58f95dd13e2b35180fe32d1491249606d8 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Thu, 6 Apr 2023 16:16:46 -0400 Subject: [PATCH 31/35] parameter names now correctly split on colons. timeseries plot units should now plot on the "largest" unit found in the data. --- .../test/test_balanced_field_length.py | 2 +- dymos/phase/options.py | 6 ++-- dymos/transcriptions/transcription_base.py | 1 - .../timeseries/bokeh_timeseries_report.py | 29 +++++++++++++++++-- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/dymos/examples/balanced_field/test/test_balanced_field_length.py b/dymos/examples/balanced_field/test/test_balanced_field_length.py index 7e4765521..cbf4f2bd8 100644 --- a/dymos/examples/balanced_field/test/test_balanced_field_length.py +++ b/dymos/examples/balanced_field/test/test_balanced_field_length.py @@ -12,7 +12,7 @@ from dymos.examples.balanced_field.balanced_field_ode import BalancedFieldODEComp -@use_tempdirs +# @use_tempdirs class TestBalancedFieldLengthRestart(unittest.TestCase): def _make_problem(self): diff --git a/dymos/phase/options.py b/dymos/phase/options.py index 50f8ceb06..f476b3fe9 100644 --- a/dymos/phase/options.py +++ b/dymos/phase/options.py @@ -753,11 +753,11 @@ class PhaseTimeseriesOptionsDictionary(om.OptionsDictionary): def __init__(self, read_only=False): super().__init__(read_only) - self.declare(name='include_state_rates', types=bool, default=False, + self.declare(name='include_state_rates', types=bool, default=True, desc='If True, include state rates in the timeseries outputs by default.') - self.declare(name='include_control_rates', types=bool, default=False, + self.declare(name='include_control_rates', types=bool, default=True, desc='If True, include control rates in the timeseries outputs by default.') - self.declare(name='include_t_phase', types=bool, default=False, + self.declare(name='include_t_phase', types=bool, default=True, desc='If True, include the elapsed phase time in the timeseries outputs by default.') diff --git a/dymos/transcriptions/transcription_base.py b/dymos/transcriptions/transcription_base.py index 15c5093e5..360b090d0 100644 --- a/dymos/transcriptions/transcription_base.py +++ b/dymos/transcriptions/transcription_base.py @@ -229,7 +229,6 @@ def setup_polynomial_controls(self, phase): timeseries=ts_name) if f'{rate_prefix}{name}_rate2' not in ts_options['outputs'] and \ (phase.timeseries_options['include_control_rates'] or options['rate2_targets']): - print(name, rate_prefix, ts_name) phase.add_timeseries_output(f'{name}_rate2', output_name=f'{rate_prefix}{name}_rate2', timeseries=ts_name) diff --git a/dymos/visualization/timeseries/bokeh_timeseries_report.py b/dymos/visualization/timeseries/bokeh_timeseries_report.py index 1bd18f682..c5f80b766 100644 --- a/dymos/visualization/timeseries/bokeh_timeseries_report.py +++ b/dymos/visualization/timeseries/bokeh_timeseries_report.py @@ -15,6 +15,7 @@ _NO_BOKEH = True import openmdao.api as om +from openmdao.utils.units import conversion_to_base_units import dymos as dm @@ -174,18 +175,40 @@ def _load_data_sources(prob, solution_record_file=None, simulation_record_file=N ts_outputs = {op: meta for op, meta in outputs.items() if op.startswith(f'{phase.pathname}.timeseries')} + # Find the "largest" unit used for any timeseries output across all phases for output_name in sorted(ts_outputs.keys(), key=str.casefold): meta = ts_outputs[output_name] prom_name = abs2prom_map['output'][output_name] var_name = prom_name.split('.')[-1] - if meta['units'] not in ts_units_dict: + if var_name not in ts_units_dict: ts_units_dict[var_name] = meta['units'] + else: + _, new_conv_factor = conversion_to_base_units(meta['units']) + _, old_conv_factor = conversion_to_base_units(ts_units_dict[var_name]) + if new_conv_factor < old_conv_factor: + ts_units_dict[var_name] = meta['units'] + + # Now a second pass through the phases since we know the units of each timeseries variable output. + for phase in traj.system_iter(include_self=True, recurse=True, typ=dm.Phase): + phase_name = phase.pathname.split('.')[-1] + + # data_dict[traj_name]['param_data_by_phase'][phase_name] = {'param': [], 'val': [], 'units': []} + phase_sol_data = data_dict[traj_name]['sol_data_by_phase'][phase_name] = {} + phase_sim_data = data_dict[traj_name]['sim_data_by_phase'][phase_name] = {} + ts_units_dict = data_dict[traj_name]['timeseries_units'] + + ts_outputs = {op: meta for op, meta in outputs.items() if op.startswith(f'{phase.pathname}.timeseries')} + + for output_name in sorted(ts_outputs.keys(), key=str.casefold): + meta = ts_outputs[output_name] + prom_name = abs2prom_map['output'][output_name] + var_name = prom_name.split('.')[-1] if sol_case: - phase_sol_data[var_name] = sol_case.get_val(prom_name, units=meta['units']) + phase_sol_data[var_name] = sol_case.get_val(prom_name, units=ts_units_dict[var_name]) if sim_case: - phase_sim_data[var_name] = sim_case.get_val(prom_name, units=meta['units']) + phase_sim_data[var_name] = sim_case.get_val(prom_name, units=ts_units_dict[var_name]) return data_dict From de262e2dc81a041d74d92bd582acc38bbaf339db Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Thu, 6 Apr 2023 16:47:45 -0400 Subject: [PATCH 32/35] fixed for parameter table formatting --- dymos/visualization/timeseries/bokeh_timeseries_report.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dymos/visualization/timeseries/bokeh_timeseries_report.py b/dymos/visualization/timeseries/bokeh_timeseries_report.py index c5f80b766..7297ec0e3 100644 --- a/dymos/visualization/timeseries/bokeh_timeseries_report.py +++ b/dymos/visualization/timeseries/bokeh_timeseries_report.py @@ -167,7 +167,7 @@ def _load_data_sources(prob, solution_record_file=None, simulation_record_file=N param_dict = data_dict[traj_name]['param_data_by_phase'][phase_name] prom_name = abs2prom_map['output'][output_name] - param_name = prom_name.split(':')[-1] + param_name = output_name.replace(f'{phase.pathname}.param_comp.parameter_vals:', '', 1) param_dict['param'].append(param_name) param_dict['units'].append(meta['units']) @@ -273,7 +273,8 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi TableColumn(field='val', title='Value'), TableColumn(field='units', title='Units'), ] - param_tables.append(DataTable(source=source, columns=columns, index_position=None, sizing_mode='stretch_width')) + param_tables.append(DataTable(source=source, columns=columns, index_position=None, + sizing_mode='stretch_both')) # Plot the timeseries ts_units_dict = source_data[traj_name]['timeseries_units'] From 464557f7f4a72b34e6a739d1a582dddaf7fe9191 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Fri, 7 Apr 2023 08:30:45 -0400 Subject: [PATCH 33/35] cleanup --- .../balanced_field/test/test_balanced_field_length.py | 2 +- .../test/test_ex_two_burn_orbit_raise_bokeh_plots.py | 2 +- dymos/visualization/timeseries/bokeh_timeseries_report.py | 6 +++--- joss/test/test_cannonball_for_joss.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dymos/examples/balanced_field/test/test_balanced_field_length.py b/dymos/examples/balanced_field/test/test_balanced_field_length.py index cbf4f2bd8..7e4765521 100644 --- a/dymos/examples/balanced_field/test/test_balanced_field_length.py +++ b/dymos/examples/balanced_field/test/test_balanced_field_length.py @@ -12,7 +12,7 @@ from dymos.examples.balanced_field.balanced_field_ode import BalancedFieldODEComp -# @use_tempdirs +@use_tempdirs class TestBalancedFieldLengthRestart(unittest.TestCase): def _make_problem(self): diff --git a/dymos/examples/finite_burn_orbit_raise/test/test_ex_two_burn_orbit_raise_bokeh_plots.py b/dymos/examples/finite_burn_orbit_raise/test/test_ex_two_burn_orbit_raise_bokeh_plots.py index 2c18c0724..384724335 100644 --- a/dymos/examples/finite_burn_orbit_raise/test/test_ex_two_burn_orbit_raise_bokeh_plots.py +++ b/dymos/examples/finite_burn_orbit_raise/test/test_ex_two_burn_orbit_raise_bokeh_plots.py @@ -191,7 +191,7 @@ def two_burn_orbit_raise_problem(transcription='gauss-lobatto', optimizer='SLSQP return p -# @use_tempdirs +@use_tempdirs class TestExampleTwoBurnOrbitRaise(unittest.TestCase): def tearDown(self): diff --git a/dymos/visualization/timeseries/bokeh_timeseries_report.py b/dymos/visualization/timeseries/bokeh_timeseries_report.py index 7297ec0e3..175f2b6ca 100644 --- a/dymos/visualization/timeseries/bokeh_timeseries_report.py +++ b/dymos/visualization/timeseries/bokeh_timeseries_report.py @@ -189,11 +189,11 @@ def _load_data_sources(prob, solution_record_file=None, simulation_record_file=N if new_conv_factor < old_conv_factor: ts_units_dict[var_name] = meta['units'] - # Now a second pass through the phases since we know the units of each timeseries variable output. + # Now a second pass through the phases since we know the units in which to plot + # each timeseries variable output. for phase in traj.system_iter(include_self=True, recurse=True, typ=dm.Phase): phase_name = phase.pathname.split('.')[-1] - # data_dict[traj_name]['param_data_by_phase'][phase_name] = {'param': [], 'val': [], 'units': []} phase_sol_data = data_dict[traj_name]['sol_data_by_phase'][phase_name] = {} phase_sim_data = data_dict[traj_name]['sim_data_by_phase'][phase_name] = {} ts_units_dict = data_dict[traj_name]['timeseries_units'] @@ -274,7 +274,7 @@ def make_timeseries_report(prob, solution_record_file=None, simulation_record_fi TableColumn(field='units', title='Units'), ] param_tables.append(DataTable(source=source, columns=columns, index_position=None, - sizing_mode='stretch_both')) + height=30*len(source.data['param']), sizing_mode='stretch_both')) # Plot the timeseries ts_units_dict = source_data[traj_name]['timeseries_units'] diff --git a/joss/test/test_cannonball_for_joss.py b/joss/test/test_cannonball_for_joss.py index e31f03dc4..7669318cf 100644 --- a/joss/test/test_cannonball_for_joss.py +++ b/joss/test/test_cannonball_for_joss.py @@ -3,7 +3,7 @@ from openmdao.utils.assert_utils import assert_near_equal -# @use_tempdirs +@use_tempdirs class TestCannonballForJOSS(unittest.TestCase): @require_pyoptsparse(optimizer='SLSQP') From bc93852b537482748c34749883ba36107cfdfa1d Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Fri, 7 Apr 2023 10:52:20 -0400 Subject: [PATCH 34/35] Failed due to some potential numerical noise. --- dymos/phase/test/test_timeseries.py | 3 --- .../components/test/test_control_endpoint_defect_comp.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/dymos/phase/test/test_timeseries.py b/dymos/phase/test/test_timeseries.py index c0221bb56..ad6b0b8a3 100644 --- a/dymos/phase/test/test_timeseries.py +++ b/dymos/phase/test/test_timeseries.py @@ -250,9 +250,6 @@ def test_timeseries_explicit_shooting(self, test_smaller_timeseries=False): assert_near_equal(np.atleast_2d(p.get_val('phase0.t')).T, p.get_val('phase0.timeseries.time')) - with self.assertRaises(KeyError): - p.get_val('phase0.timeseries.time_phase') - for state in ('x', 'y', 'v'): assert_near_equal(p.get_val(f'phase0.integrator.states_out:{state}'), p.get_val(f'phase0.timeseries.states:{state}')) diff --git a/dymos/transcriptions/pseudospectral/components/test/test_control_endpoint_defect_comp.py b/dymos/transcriptions/pseudospectral/components/test/test_control_endpoint_defect_comp.py index 03e486d86..87db3c314 100644 --- a/dymos/transcriptions/pseudospectral/components/test/test_control_endpoint_defect_comp.py +++ b/dymos/transcriptions/pseudospectral/components/test/test_control_endpoint_defect_comp.py @@ -59,7 +59,7 @@ def test_results(self): u_given = self.p['controls:u'][-1] assert_near_equal(np.ravel(self.p['endpoint_defect_comp.control_endpoint_defects:u']), np.ravel(u_given - u_interp), - tolerance=1.0E-12) + tolerance=1.0E-9) def test_partials(self): cpd = self.p.check_partials(compact_print=False, method='cs') From 36d5692c6b948f19bcb54b2cec464cd87f1cad04 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Mon, 17 Apr 2023 08:15:52 -0400 Subject: [PATCH 35/35] typo from review --- dymos/phase/options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dymos/phase/options.py b/dymos/phase/options.py index f476b3fe9..2fc316b87 100644 --- a/dymos/phase/options.py +++ b/dymos/phase/options.py @@ -743,7 +743,7 @@ def __init__(self, read_only=False): class PhaseTimeseriesOptionsDictionary(om.OptionsDictionary): """ - An OptionsDictionary for phase options related to timeseries.. + An OptionsDictionary for phase options related to timeseries. Parameters ----------