Overview

What does this package contain?

  1. Data structures for values and time series data associated with time-indexed variables (or parameters, or named expressions). Examples are setpoint values associated with a subset of state variables or time series data from a simulation

  2. Utilities for loading and extracting this data into and from variables in a model

  3. Utilities for constructing components from this data (expressions, constraints, and objectives) that are useful for dynamic optimization

What is the goal of this package?

This package was written to help developers of Pyomo-based dynamic optimization case studies, especially rolling horizon dynamic optimization case studies, write scripts that are small, legible, and maintainable. It does this by providing utilities for mundane data-management and model construction tasks, allowing the developer to focus on their application.

Why is this package useful?

First, it is not normally easy to extract “flattened” time series data, in which all indexing structure other than time-indexing has been flattened to yield a set of one-dimensional arrays, from a Pyomo model. This is an extremely convenient data structure to have for plotting, analysis, initialization, and manipulation of dynamic models. If all variables are indexed by time and only time, this data is relatively easy to obtain. The first issue comes up when dealing with components that are indexed by time in addition to some other set(s). For example:

>>> import pyomo.environ as pyo

>>> m = pyo.ConcreteModel()
>>> m.time = pyo.Set(initialize=[0, 1, 2])
>>> m.comp = pyo.Set(initialize=["A", "B"])
>>> m.var = pyo.Var(m.time, m.comp, initialize=1.0)

>>> t0 = m.time.first()
>>> data = {
...    m.var[t0, j].name: [m.var[i, j].value for i in m.time]
...    for j in m.comp
... }
>>> data
{'var[0,A]': [1.0, 1.0, 1.0], 'var[0,B]': [1.0, 1.0, 1.0]}

To generate data in this form, we need to (a) know that our variable is indexed by time and m.comp and (b) arbitrarily select a time index t0 to generate a unique key for each time series. This gets more difficult when blocks and time-indexed blocks are used as well. The first difficulty can be alleviated using flatten_dae_components from pyomo.dae.flatten:

>>> import pyomo.environ as pyo
>>> from pyomo.dae.flatten import flatten_dae_components

>>> m = pyo.ConcreteModel()
>>> m.time = pyo.Set(initialize=[0, 1, 2])
>>> m.comp = pyo.Set(initialize=["A", "B"])
>>> m.var = pyo.Var(m.time, m.comp, initialize=1.0)

>>> t0 = m.time.first()
>>> scalar_vars, dae_vars = flatten_dae_components(m, m.time, pyo.Var)
>>> data = {var[t0].name: list(var[:].value) for var in dae_vars}
>>> data
{'var[0,A]': [1.0, 1.0, 1.0], 'var[0,B]': [1.0, 1.0, 1.0]}

Addressing the arbitrary t0 index requires us to ask what key we would like to use to identify each time series in our data structure. The key should uniquely correspond to a component, or “sub-component” that is indexed only by time. A slice, e.g. m.var[:, "A"] seems natural. However, Pyomo provides a better data structure that can be constructed from a component, slice, or string, called ComponentUID. Being constructable from a string is important as we may want to store or serialize this data in a form that is agnostic of any particular ConcreteModel object. We can now generate our data structure as:

>>> data = {
...    pyo.ComponentUID(var.referent): list(var[:].value)
...    for var in dae_vars
... }
>>> data
{var[*,A]: [1.0, 1.0, 1.0], var[*,B]: [1.0, 1.0, 1.0]}

This is the structure of the underlying dictionary in the TimeSeriesData class provided by this package. We can generate this data using this package as:

>>> import pyomo.environ as pyo
>>> from pyomo.contrib.mpc import DynamicModelInterface

>>> m = pyo.ConcreteModel()
>>> m.time = pyo.Set(initialize=[0, 1, 2])
>>> m.comp = pyo.Set(initialize=["A", "B"])
>>> m.var = pyo.Var(m.time, m.comp, initialize=1.0)

>>> # Construct a helper class for interfacing model with data
>>> helper = DynamicModelInterface(m, m.time)

>>> # Generates a TimeSeriesData object
>>> series_data = helper.get_data_at_time()

>>> # Get the underlying dictionary
>>> data = series_data.get_data()
>>> data
{var[*,A]: [1.0, 1.0, 1.0], var[*,B]: [1.0, 1.0, 1.0]}

The first value proposition of this package is that DynamicModelInterface and TimeSeriesData provide wrappers to ease loading and extraction of data via flatten_dae_components and ComponentUID.

The second difficulty addressed by this package is that of extracting and loading data between (potentially) different models. For instance, in model predictive control, we often want to extract data from a particular time point in a plant model and load it into a controller model as initial conditions. This can be done as follows:

>>> import pyomo.environ as pyo
>>> from pyomo.contrib.mpc import DynamicModelInterface

>>> m1 = pyo.ConcreteModel()
>>> m1.time = pyo.Set(initialize=[0, 1, 2])
>>> m1.comp = pyo.Set(initialize=["A", "B"])
>>> m1.var = pyo.Var(m1.time, m1.comp, initialize=1.0)

>>> m2 = pyo.ConcreteModel()
>>> m2.time = pyo.Set(initialize=[0, 1, 2])
>>> m2.comp = pyo.Set(initialize=["A", "B"])
>>> m2.var = pyo.Var(m2.time, m2.comp, initialize=2.0)

>>> # Construct helper objects
>>> m1_helper = DynamicModelInterface(m1, m1.time)
>>> m2_helper = DynamicModelInterface(m2, m2.time)

>>> # Extract data from final time point of m2
>>> tf = m2.time.last()
>>> tf_data = m2_helper.get_data_at_time(tf)

>>> # Load data into initial time point of m1
>>> t0 = m1.time.first()
>>> m1_helper.load_data(tf_data, time_points=t0)

>>> # Get TimeSeriesData object
>>> series_data = m1_helper.get_data_at_time()
>>> # Get underlying dictionary
>>> series_data.get_data()
{var[*,A]: [2.0, 1.0, 1.0], var[*,B]: [2.0, 1.0, 1.0]}

Note

Here we rely on the fact that our variable has the same name in both models.

Finally, this package provides methods for constructing components like tracking cost expressions and piecewise-constant constraints from the provided data structures. For example, the following code constructs a tracking cost expression.

>>> import pyomo.environ as pyo
>>> from pyomo.contrib.mpc import DynamicModelInterface

>>> m = pyo.ConcreteModel()
>>> m.time = pyo.Set(initialize=[0, 1, 2])
>>> m.comp = pyo.Set(initialize=["A", "B"])
>>> m.var = pyo.Var(m.time, m.comp, initialize=1.0)

>>> # Construct helper object
>>> helper = DynamicModelInterface(m, m.time)

>>> # Construct data structure for setpoints
>>> setpoint = {m.var[:, "A"]: 0.5, m.var[:, "B"]: 2.0}
>>> var_set, tr_cost = helper.get_penalty_from_target(setpoint)
>>> m.setpoint_idx = var_set
>>> m.tracking_cost = tr_cost
>>> m.tracking_cost.pprint()
tracking_cost : Size=6, Index=setpoint_idx*time
    Key    : Expression
    (0, 0) : (var[0,A] - 0.5)**2
    (0, 1) : (var[1,A] - 0.5)**2
    (0, 2) : (var[2,A] - 0.5)**2
    (1, 0) : (var[0,B] - 2.0)**2
    (1, 1) : (var[1,B] - 2.0)**2
    (1, 2) : (var[2,B] - 2.0)**2

These methods will hopefully allow developers to declutter dynamic optimization scripts and pay more attention to the application of the optimization problem rather than the setup of the optimization problem.

Who develops and maintains this package?

This package was developed by Robert Parker while a PhD student in Larry Biegler’s group at CMU, with guidance from Bethany Nicholson and John Siirola.