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

Removed redundant implementations of timeseries output components. #1005

Merged
merged 3 commits into from
Oct 17, 2023
Merged
Changes from all commits
Commits
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
11 changes: 5 additions & 6 deletions dymos/transcriptions/analytic/analytic.py
Original file line number Diff line number Diff line change
@@ -6,8 +6,7 @@
get_source_metadata, configure_analytic_states_discovery
from ...utils.indexing import get_src_indices_by_row
from ..grid_data import GridData
from .analytic_timeseries_output_comp import AnalyticTimeseriesOutputComp
from ..common import TimeComp, TimeseriesOutputGroup
from ..common import TimeComp, TimeseriesOutputGroup, TimeseriesOutputComp
from ..._options import options as dymos_options


@@ -315,10 +314,10 @@ def setup_timeseries_outputs(self, phase):
else:
ogd = options['transcription'].grid_data

timeseries_comp = AnalyticTimeseriesOutputComp(input_grid_data=gd,
output_grid_data=ogd,
output_subset=options['subset'],
time_units=phase.time_options['units'])
timeseries_comp = TimeseriesOutputComp(input_grid_data=gd,
output_grid_data=ogd,
output_subset=options['subset'],
time_units=phase.time_options['units'])

timeseries_group = TimeseriesOutputGroup(has_expr=has_expr, timeseries_output_comp=timeseries_comp)
phase.add_subsystem(name, subsys=timeseries_group)
224 changes: 0 additions & 224 deletions dymos/transcriptions/analytic/analytic_timeseries_output_comp.py

This file was deleted.

1 change: 1 addition & 0 deletions dymos/transcriptions/common/__init__.py
Original file line number Diff line number Diff line change
@@ -4,3 +4,4 @@
from .polynomial_control_group import PolynomialControlGroup
from .time_comp import TimeComp
from .timeseries_group import TimeseriesOutputGroup
from .timeseries_output_comp import TimeseriesOutputComp
4 changes: 2 additions & 2 deletions dymos/transcriptions/common/timeseries_group.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import openmdao.api as om

from .timeseries_output_comp import TimeseriesOutputCompBase
from .timeseries_output_comp import TimeseriesOutputComp


class TimeseriesOutputGroup(om.Group):
@@ -19,7 +19,7 @@ def initialize(self):
"""
Declare component options.
"""
self.options.declare('timeseries_output_comp', types=TimeseriesOutputCompBase, recordable=False,
self.options.declare('timeseries_output_comp', types=TimeseriesOutputComp, recordable=False,
desc='Timeseries component specific to the transcription of the optimal control problem.')

self.options.declare('has_expr', types=bool,
213 changes: 204 additions & 9 deletions dymos/transcriptions/common/timeseries_output_comp.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import numpy as np
import openmdao.api as om
from openmdao.utils.units import unit_conversion
from scipy import sparse as sp

from ...transcriptions.grid_data import GridData
from ..._options import options as dymos_options
from ...utils.lagrange import lagrange_matrices


class TimeseriesOutputCompBase(om.ExplicitComponent):
class TimeseriesOutputComp(om.ExplicitComponent):
"""
Class definition of the TimeseriesOutputCompBase.
TimeseriesOutputComp collects variable values from the phase and provides them in chronological
order as outputs. Some phase types don't internally have access to a contiguous array of all
values of a given variable in the phase. For instance, the GaussLobatto pseudospectral has
separate arrays of variable values at discretization and collocation nodes. These values
need to be interleaved to provide a time series. Pseudospectral techniques provide timeseries
data at 'all' nodes, while ExplicitPhase provides values at the step boundaries.
Class definition of the TimeseriesOutputComp.
Parameters
----------
@@ -22,6 +19,7 @@ class TimeseriesOutputCompBase(om.ExplicitComponent):
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)

self._no_check_partials = not dymos_options['include_check_partials']

# _vars keeps track of the name of each output and maps to its metadata;
@@ -44,6 +42,9 @@ def __init__(self, **kwargs):
self._units = {}
self._conversion_factors = {}

# Flag to set if no multiplication by the interpolation matrix is necessary
self._no_interp = False

def initialize(self):
"""
Declare component options.
@@ -62,3 +63,197 @@ def initialize(self):
types=str,
default='all',
desc='Name of the node subset at which outputs are desired.')

self.options.declare('time_units', default=None, allow_none=True, types=str,
desc='Units of time')

def setup(self):
"""
Define the independent variables as output variables.
"""
igd = self.options['input_grid_data']
ogd = self.options['output_grid_data']
output_subset = self.options['output_subset']

if ogd is None:
ogd = igd

if ogd == igd and output_subset == 'all':
self._no_interp = True

self.input_num_nodes = igd.num_nodes
self.output_num_nodes = ogd.subset_num_nodes[output_subset]

# Build the interpolation matrix which maps from the input grid to the output grid.
# Rather than a single phase-wide interpolating polynomial, map each segment.
# To do this, find the nodes in the output grid which fall in each segment of the input
# grid. Then build a Lagrange interpolating polynomial for that segment
L_blocks = []
D_blocks = []
output_nodes_ptau = ogd.node_ptau[ogd.subset_node_indices[output_subset]]

for iseg in range(igd.num_segments):
i1, i2 = igd.segment_indices[iseg]
iptau_segi = igd.node_ptau[i1:i2]
istau_segi = igd.node_stau[i1:i2]

# The indices of the output grid that fall within this segment of the input grid
if ogd is igd and output_subset == 'all':
optau_segi = iptau_segi
else:
ptau_hi = igd.segment_ends[iseg+1]
if iseg < igd.num_segments - 1:
optau_segi = output_nodes_ptau[output_nodes_ptau <= ptau_hi]
else:
optau_segi = output_nodes_ptau

# Remove the captured nodes so we don't accidentally include them again
output_nodes_ptau = output_nodes_ptau[len(optau_segi):]

# # Now get the output nodes which fall in iseg in iseg's segment tau space.
ostau_segi = 2.0 * (optau_segi - iptau_segi[0]) / (iptau_segi[-1] - iptau_segi[0]) - 1

# Create the interpolation matrix and add it to the blocks
L, D = lagrange_matrices(istau_segi, ostau_segi)
L_blocks.append(L)
D_blocks.append(D)

self.interpolation_matrix = sp.block_diag(L_blocks, format='csr')
self.differentiation_matrix = sp.block_diag(D_blocks, format='csr')

self.add_input('dt_dstau', shape=(self.input_num_nodes,), units=self.options['time_units'])

def _add_output_configure(self, name, units, shape, desc='', src=None, rate=False):
"""
Add a single timeseries output.
Can be called by parent groups in configure.
Parameters
----------
name : str
name of the variable in this component's namespace.
shape : int or tuple or list or None
Shape of this variable, only required if val is not an array.
Default is None.
units : str or None
Units in which the output variables will be provided to the component during execution.
Default is None, which means it has no units.
desc : str
description of the timeseries output variable.
src : str
The src path of the variables input, used to prevent redundant inputs.
rate : bool
If True, timeseries output is a rate.
Returns
-------
bool
True if a new input was added for the output, or False if it reuses an existing input.
"""
input_num_nodes = self.input_num_nodes
output_num_nodes = self.output_num_nodes
added_source = False

if name in self._vars:
return False

if src in self._sources:
# If we're already pulling the source into this timeseries, use that as the
# input for this output.
input_name = self._sources[src]
input_units = self._units[input_name]
else:
input_name = f'input_values:{name}'
self.add_input(input_name,
shape=(input_num_nodes,) + shape,
units=units, desc=desc)
self._sources[src] = input_name
input_units = self._units[input_name] = units
added_source = True

output_name = name
self.add_output(output_name, shape=(output_num_nodes,) + shape, units=units, desc=desc)

self._vars[name] = (input_name, output_name, shape, rate)

size = np.prod(shape)

if rate:
mat = self.differentiation_matrix
else:
mat = self.interpolation_matrix

# Preallocate lists to hold data for jac
jac_data = []
jac_indices = []
jac_indptr = [0]

# Iterate over the dense dimension 'size'
for s in range(size):
# Extend the data and indices using the CSR attributes of mat
jac_data.extend(mat.data)
jac_indices.extend(mat.indices + s * input_num_nodes)

# For every non-zero row in mat, update jac's indptr
new_indptr = mat.indptr[1:] + s * len(mat.data)
jac_indptr.extend(new_indptr)

# Correct the last entry of jac_indptr
jac_indptr[-1] = len(jac_data)

# Construct the sparse jac matrix in CSR format
jac = sp.csr_matrix((jac_data, jac_indices, jac_indptr),
shape=(output_num_nodes * size, input_num_nodes * size))

# Now, if you want to get the row and column indices of the non-zero entries in the jac matrix:
jac_rows, jac_cols = jac.nonzero()

# There's a chance that the input for this output was pulled from another variable with
# different units, so account for that with a conversion.
val = np.squeeze(np.array(jac[jac_rows, jac_cols]))
if input_units is None or units is None:
self.declare_partials(of=output_name, wrt=input_name,
rows=jac_rows, cols=jac_cols, val=val)
else:
scale, offset = unit_conversion(input_units, units)
self._conversion_factors[output_name] = scale, offset

self.declare_partials(of=output_name, wrt=input_name,
rows=jac_rows, cols=jac_cols,
val=scale * val)

return added_source

def compute(self, inputs, outputs):
"""
Compute component outputs.
Parameters
----------
inputs : `Vector`
`Vector` containing inputs.
outputs : `Vector`
`Vector` containing outputs.
"""
# convert dt_dstau to a column vector
dt_dstau = inputs['dt_dstau'][:, np.newaxis]

for (input_name, output_name, _, is_rate) in self._vars.values():
if self._no_interp:
interp_vals = inputs[input_name]
else:
inp = inputs[input_name]
if len(inp.shape) > 2:
# Dot product always performs the sum product over axis 2.
inp = inp.swapaxes(0, 1)
interp_vals = self.interpolation_matrix.dot(inp)

if is_rate:
interp_vals = self.differentiation_matrix.dot(interp_vals) / dt_dstau

if output_name in self._conversion_factors:
scale, offset = self._conversion_factors[output_name]
outputs[output_name] = scale * (interp_vals + offset)
else:
outputs[output_name] = interp_vals
10 changes: 5 additions & 5 deletions dymos/transcriptions/explicit_shooting/explicit_shooting.py
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@
import openmdao.api as om
from openmdao.utils.om_warnings import warn_deprecation

from ..pseudospectral.components import PseudospectralTimeseriesOutputComp
from ..common.timeseries_output_comp import TimeseriesOutputComp
from .explicit_shooting_continuity_comp import ExplicitShootingContinuityComp
from ..transcription_base import TranscriptionBase
from ..grid_data import GaussLobattoGrid, RadauGrid, UniformGrid
@@ -622,10 +622,10 @@ def setup_timeseries_outputs(self, phase):
has_expr = True
break

timeseries_comp = PseudospectralTimeseriesOutputComp(input_grid_data=self._output_grid_data,
output_grid_data=self._output_grid_data,
output_subset=options['subset'],
time_units=phase.time_options['units'])
timeseries_comp = TimeseriesOutputComp(input_grid_data=self._output_grid_data,
output_grid_data=self._output_grid_data,
output_subset=options['subset'],
time_units=phase.time_options['units'])
timeseries_group = TimeseriesOutputGroup(has_expr=has_expr, timeseries_output_comp=timeseries_comp)
phase.add_subsystem(name, subsys=timeseries_group)

This file was deleted.

24 changes: 23 additions & 1 deletion dymos/transcriptions/grid_data.py
Original file line number Diff line number Diff line change
@@ -253,7 +253,6 @@ class GridData(object):
Dict keyed by the map name that provides a mapping for src_indices to
and from "compressed" form.
"""

def __init__(self, num_segments, transcription, transcription_order=None,
segment_ends=None, compressed=False, num_steps_per_segment=1):
if segment_ends is None:
@@ -401,6 +400,29 @@ def __init__(self, num_segments, transcription, transcription_order=None,
self.input_maps['dynamic_control_input_to_disc'] = make_subset_map(control_input_idxs,
control_disc_idxs)

def __eq__(self, other):
"""
Compare this GridData with an object with other and return True if they are equivalent.
Parameters
----------
other : GridData
Returns
-------
bool
True if other is equivalent to self, otherwise False.
"""
if isinstance(other, GridData):
return self.transcription == other.transcription and \
self.num_segments == other.num_segments and \
np.all(self.segment_ends == other.segment_ends) and \
self.compressed == other.compressed and \
np.all(self.transcription_order == other.transcription_order) and \
np.all(self.num_steps_per_segment == other.num_steps_per_segment)
else:
return False

def is_aligned_with(self, other, tol=1.0E-12):
"""
Check that the segment distribution in GridData object `other` matches that of this GridData object.
2 changes: 1 addition & 1 deletion dymos/transcriptions/pseudospectral/components/__init__.py
Original file line number Diff line number Diff line change
@@ -3,4 +3,4 @@
from .state_independents import StateIndependentsComp
from .control_endpoint_defect_comp import ControlEndpointDefectComp
from .gauss_lobatto_interleave_comp import GaussLobattoInterleaveComp
from .pseudospectral_timeseries_output_comp import PseudospectralTimeseriesOutputComp
from ...common.timeseries_output_comp import TimeseriesOutputComp

This file was deleted.

12 changes: 6 additions & 6 deletions dymos/transcriptions/pseudospectral/pseudospectral_base.py
Original file line number Diff line number Diff line change
@@ -3,8 +3,8 @@
import openmdao.api as om
from ..transcription_base import TranscriptionBase
from ..common import TimeComp, TimeseriesOutputGroup
from .components import StateIndependentsComp, StateInterpComp, CollocationComp, \
PseudospectralTimeseriesOutputComp
from .components import StateIndependentsComp, StateInterpComp, CollocationComp
from ..common.timeseries_output_comp import TimeseriesOutputComp
from ...utils.misc import CoerceDesvar, get_rate_units, reshape_val
from ...utils.introspection import get_promoted_vars, get_source_metadata, configure_duration_balance_introspection
from ...utils.constants import INF_BOUND
@@ -540,10 +540,10 @@ def setup_timeseries_outputs(self, phase):
else:
ogd = options['transcription'].grid_data

timeseries_comp = PseudospectralTimeseriesOutputComp(input_grid_data=gd,
output_grid_data=ogd,
output_subset=options['subset'],
time_units=phase.time_options['units'])
timeseries_comp = TimeseriesOutputComp(input_grid_data=gd,
output_grid_data=ogd,
output_subset=options['subset'],
time_units=phase.time_options['units'])
timeseries_group = TimeseriesOutputGroup(has_expr=has_expr, timeseries_output_comp=timeseries_comp)
phase.add_subsystem(name, subsys=timeseries_group)

Original file line number Diff line number Diff line change
@@ -3,12 +3,12 @@
import scipy.sparse as sp

from openmdao.utils.units import unit_conversion
from dymos.transcriptions.common.timeseries_output_comp import TimeseriesOutputCompBase

from ....utils.lagrange import lagrange_matrices
from ...common import TimeseriesOutputComp


class SolveIVPTimeseriesOutputComp(TimeseriesOutputCompBase):
class SolveIVPTimeseriesOutputComp(TimeseriesOutputComp):
"""
Class definition for SolveIVPTimeseriesOutputComp.
@@ -21,16 +21,13 @@ def initialize(self):
"""
Declare component options.
"""
super(SolveIVPTimeseriesOutputComp, self).initialize()
super().initialize()

self.options.declare('output_nodes_per_seg', default=None, types=(int,), allow_none=True,
desc='If None, results are provided at the all nodes within each'
'segment. If an int (n) then results are provided at n '
'equally distributed points in time within each segment.')

self.options.declare('time_units', default=None, allow_none=True, types=str,
desc='Units of time')

def setup(self):
"""
Define the independent variables as output variables.