Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

trajectory results report now automatically generated by run_problem #918

Merged
merged 37 commits into from
Apr 17, 2023
Merged
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
638263a
start up updated timeseries report.
robfalck Mar 6, 2023
43de3ea
dymos now puts a report in the reports directory for each phase that …
robfalck Mar 15, 2023
f0c639d
Moved parameter tables to a different tab for each phase.
robfalck Mar 16, 2023
20983ee
user can now toggle view of solution/simulation
robfalck Mar 17, 2023
ab82d22
Moved phase.timeseries_options['use_prefix'] to global dymos option […
robfalck Mar 17, 2023
5ae4e43
timeseries outputs now stored with full name as key rather than outpu…
robfalck Mar 17, 2023
d32eeae
one more fix for the unfound timeseries issue
robfalck Mar 17, 2023
c111892
phase selection in the trajectory results report
robfalck Mar 17, 2023
453d1aa
cleanup of traj_results_report
robfalck Mar 18, 2023
f419f99
handle case where either solution or simulation data is not present
robfalck Mar 18, 2023
6b8518d
handle case of missing simulation or solution data
robfalck Mar 18, 2023
cacfd89
explicit shooting fix
robfalck Mar 20, 2023
8c644d4
adding some doc strings
robfalck Mar 22, 2023
fdd92a6
implemented a new bokeh based trajectory results report
robfalck Mar 24, 2023
e9157dd
merge out
robfalck Mar 24, 2023
4395020
Fixed a bug in the names of the ExplicitShooting timeseries outputs
robfalck Mar 24, 2023
be9bc1e
timeseries results report causing an error when bokeh is unavailable.
robfalck Mar 24, 2023
951805b
Updated docs workflow to more reliably provide the doc build reports.
robfalck Mar 25, 2023
76e9eaa
state rates, time_phase, and control rates all added back to the reco…
robfalck Mar 25, 2023
1fcc7eb
Grid refinement no longer depends on the presense of time_phase in th…
robfalck Mar 25, 2023
dcfc18c
Do not try to make the trajectory results report if the reports direc…
robfalck Mar 25, 2023
e6ce05a
properly handle missing report path
robfalck Mar 25, 2023
022dddc
properly handle missing report path
robfalck Mar 25, 2023
e3811a8
cleanup
robfalck Mar 26, 2023
c2e8932
more cleanup
robfalck Mar 26, 2023
6768ab2
more cleanup
robfalck Mar 27, 2023
169c3a1
more cleanup
robfalck Mar 27, 2023
b64f8ec
fixed an issue with writing the trajectory results report when bokeh …
robfalck Mar 27, 2023
0b40dbd
docstring linting
robfalck Mar 27, 2023
1b38472
Skip a few regridding tests where the interpolation of the results we…
robfalck Mar 28, 2023
141740f
Fixed skip tests for tests that require invalid_desvar_behavior
robfalck Apr 1, 2023
48b77bb
Merge branch 'master' of https://github.com/OpenMDAO/dymos into times…
robfalck Apr 1, 2023
7f70cb5
parameter names now correctly split on colons.
robfalck Apr 6, 2023
de262e2
fixed for parameter table formatting
robfalck Apr 6, 2023
464557f
cleanup
robfalck Apr 7, 2023
bc93852
Failed due to some potential numerical noise.
robfalck Apr 7, 2023
36d5692
typo from review
robfalck Apr 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
cleanup
robfalck committed Mar 26, 2023
commit e3811a883f58a5225321e11457d75afd162e8a4b
2 changes: 1 addition & 1 deletion benchmark/benchmark_brachistochrone.py
Original file line number Diff line number Diff line change
@@ -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]

2 changes: 1 addition & 1 deletion dymos/_options.py
Original file line number Diff line number Diff line change
@@ -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,
Original file line number Diff line number Diff line change
@@ -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'

Original file line number Diff line number Diff line change
@@ -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]

Original file line number Diff line number Diff line change
@@ -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]

Original file line number Diff line number Diff line change
@@ -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]

Original file line number Diff line number Diff line change
@@ -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]

Original file line number Diff line number Diff line change
@@ -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]

4 changes: 3 additions & 1 deletion dymos/examples/brachistochrone/test/test_timeseries_units.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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):
Original file line number Diff line number Diff line change
@@ -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)

Original file line number Diff line number Diff line change
@@ -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,25 +205,22 @@ 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'

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
2 changes: 1 addition & 1 deletion dymos/grid_refinement/error_estimation.py
Original file line number Diff line number Diff line change
@@ -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'])

2 changes: 1 addition & 1 deletion dymos/phase/test/test_analytic_phase.py
Original file line number Diff line number Diff line change
@@ -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')
8 changes: 4 additions & 4 deletions dymos/phase/test/test_phase.py
Original file line number Diff line number Diff line change
@@ -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):
19 changes: 11 additions & 8 deletions dymos/phase/test/test_timeseries.py
Original file line number Diff line number Diff line change
@@ -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()
13 changes: 10 additions & 3 deletions dymos/run_problem.py
Original file line number Diff line number Diff line change
@@ -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
43 changes: 30 additions & 13 deletions dymos/test/test_run_problem.py
Original file line number Diff line number Diff line change
@@ -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))

7 changes: 3 additions & 4 deletions dymos/test/test_upgrade_guide.py
Original file line number Diff line number Diff line change
@@ -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]

7 changes: 5 additions & 2 deletions dymos/trajectory/test/test_trajectory.py
Original file line number Diff line number Diff line change
@@ -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()

2 changes: 1 addition & 1 deletion dymos/trajectory/trajectory.py
Original file line number Diff line number Diff line change
@@ -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
59 changes: 51 additions & 8 deletions dymos/visualization/test/test_timeseries_plots.py
Original file line number Diff line number Diff line change
@@ -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,13 +114,19 @@ 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')

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
217 changes: 100 additions & 117 deletions dymos/visualization/timeseries_plots.py
Original file line number Diff line number Diff line change
@@ -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"]}')