Multi-stage optimisation

Note

This section describes how to run multi-stage optimisations with SpineOpt using the stage class - not to be confused with the rolling horizon optimisation technique described in Temporal Framework, nor the Benders decomposition algorithm described in Decomposition.

Warning

This feature is experimental. It may change in future versions without notice.

By default, SpineOpt is solved as a 'single-stage' optimisation problem. However you can add additional stages to the optimisation by creating stage objects in your DB.

To motivate this discussion, say you want to model a storage over a year with hourly resolution. The model is large, so you would like to solve it using a rolling horizon of, say, one day - so it solves quickly (see roll_forward and the Temporal Framework section). But this wouldn't capture the long-term value of your storage!

To remediate this, you can introduce an additional 'stage' that solves the entire year at once with a lower temporal resolution (say, one day instead of one hour), and then fixes the storage level at certain points for your higher-resolution rolling horizon model. Both models, the year-long model at daily resolution and the rolling horizon model at hourly resolution, will solve faster than the year-long model at hourly resolution - hopefully much faster - leading to a good compromise between speed and accuracy.

So how do you do that? You use a stage.

The stage class

In SpineOpt, a stage is an additional optimisation model that fixes certain outputs for another set of models declared as their children.

The children of a stage are defined via stage__child_stage relationships (with the parent stage in the first dimension). If a stage has no stage__child_stage relationships as a parent, then it is assumed to have only one children: the model itself.

The outputs that a stage fixes for its children are defined via stage__output__node, stage__output__unit and/or stage__output__connection relationships. For example, if you want to fix node_state for a node, then you would create a stage__output__node between the stage, the node_state output and the node.

By default, the output is fixed at the end of each child's rolling window. However, you can fix it at other points in time by specifying the output_resolution parameter as a duration (or array of durations) relative to the start of the child's rolling window. For example, if you specify an output_resolution of 1 day, then the output will be fixed at one day after the child's window start. If you specify something like [1 day, 2 days], then it will be fixed at one day after the window start, and then at two days after that (i.e., three days after the window start).

The optimisation model that a stage solves is given by the stage_scenario parameter value, which must be a scenario in your DB.

And that's basically it!

Example

In case of the year-long storage model with hourly resolution, here is how you would do it.

First, the basic setup:

  1. Create your model.
  2. Create a temporal_block called flat.
  3. Create the rest of your model (the storage node, etc.)
  4. Create a model__default_temporal_block between your model and the flat temporal_block (to keep things simple, but of course you can use node__temporal_block, etc., as needed).
  5. Create a scenario called e.g. Base_scenario including only the Base alternative.
  6. For the Base alternative:
    1. Specify model_start and model_end for your model to cover the year of interest.
    2. Specify roll_forward for your model as 1 day.
    3. Specify resolution for your temporal_block as 1 hour.

With the above, if you run the Base_scenario SpineOpt will run an hourly-resolution year-long rolling horizon model solving one day at a time, that would probably finish in reasonable time but wouldn't capture the long-term value of your storage.

Next, the 'stage' stuff:

  1. Create a stage called lt_storage.
  2. (Don't create any stage__child_stage relationsips - the only child is the model - plus you don't have/need other stages).
  3. Create a stage__output__node between your stage, the node_state output and your storage node.
  4. Create an alternative called lt_storage_alt.
  5. Create a scenario called lt_storage_scen with lt_storage_alt in the higher rank and the Base alternative in the lower rank.
  6. For the lt_storage_alt:
    1. Specify roll_forward for your model as nothing - so the model doesn't roll - the entire year is solved at once.
    2. Specify resolution for the flat temporal_block as 1 day.
    3. (Don't specify output_resolution so the output is fixed at the end of the model's rolling window.)
  7. For the Base alternative, specify stage_scenario for the lt_storage stage as lt_storage_scen.

Now, if you run the Base_scenario SpineOpt will run a two-stage model:

  • First, a daily-resolution year-long model that will capture the long-term value of your storage.
  • Next, an hourly-resolution year-long rolling horizon model solving one day at a time, where the node_state of your storage node will be fixed at the end of each day to the optimal LT trajectory computed in the previous stage.