Working with Time Series Data

In this tutorial, we will manually add, retrieve, and inspect time-series data in different formats, including identifying which components in a power System have time series data. Along the way, we will also use workarounds for missing forecast data and reuse identical time series profiles to avoid unnecessary memory usage.

Example Data and Setup

We will make an example System with a wind generator and two loads, and add the time series needed to model, for example, the impacts of wind forecast uncertainty.

Here is the available data:

For the wind generator, we have the historical point (deterministic) forecasts of power output. The forecasts were generated every 30 minutes with a 5-minute resolution and 1-hour horizon. We also have measurements what actually happened at 5-minute resolution over the 2 hours.

For the loads, note that the forecast data is missing. We only have the historical measurements of total load for the system, which is normalized to the system's peak load.

Load the PowerSystems, Dates, and TimeSeries packages to get started:

julia> using PowerSystems
julia> using Dates
julia> using TimeSeries

As usual, we need to define a power System that holds all our data. Let's define a simple system with a bus, a wind generator, and two loads:

julia> system = System(100.0); # 100 MVA base power
julia> bus1 = ACBus(; number = 1, name = "bus1", bustype = ACBusTypes.REF, angle = 0.0, magnitude = 1.0, voltage_limits = (min = 0.9, max = 1.05), base_voltage = 230.0, );
julia> wind1 = RenewableDispatch(; name = "wind1", available = true, bus = bus1, active_power = 0.0, # Per-unitized by device base_power reactive_power = 0.0, # Per-unitized by device base_power rating = 1.0, # 10 MW per-unitized by device base_power prime_mover_type = PrimeMovers.WT, reactive_power_limits = (min = 0.0, max = 0.0), # per-unitized by device base_power power_factor = 1.0, operation_cost = RenewableGenerationCost(nothing), base_power = 10.0, # MVA );
julia> load1 = PowerLoad(; name = "load1", available = true, bus = bus1, active_power = 0.0, # Per-unitized by device base_power reactive_power = 0.0, # Per-unitized by device base_power base_power = 10.0, # MVA max_active_power = 1.0, # 10 MW per-unitized by device base_power max_reactive_power = 0.0, );
julia> load2 = PowerLoad(; name = "load2", available = true, bus = bus1, active_power = 0.0, # Per-unitized by device base_power reactive_power = 0.0, # Per-unitized by device base_power base_power = 30.0, # MVA max_active_power = 1.0, # 10 MW per-unitized by device base_power max_reactive_power = 0.0, );
julia> add_components!(system, [bus1, wind1, load1, load2])

Recall that we can also set the System's unit base to natural units (MW) to make it easier to inspect results:

julia> set_units_base_system!(system, "NATURAL_UNITS")[ Info: Unit System changed to UnitSystem.NATURAL_UNITS = 2

Before we get started, print wind1 to see its data:

julia> wind1RenewableDispatch: wind1:
   name: wind1
   available: true
   bus: ACBus: bus1
   active_power: 0.0
   reactive_power: 0.0
   rating: 10.0
   prime_mover_type: PrimeMovers.WT = 22
   reactive_power_limits: (min = 0.0, max = 0.0)
   power_factor: 1.0
   operation_cost: RenewableGenerationCost composed of variable: CostCurve{LinearCurve}, curtailment_cost: CostCurve{LinearCurve}
   base_power: 10.0
   services: 0-element Vector{Service}
   dynamic_injector: nothing
   ext: Dict{String, Any}()
   InfrastructureSystems.SystemUnitsSettings:
      base_value: 100.0
      unit_system: UnitSystem.NATURAL_UNITS = 2
   has_supplemental_attributes: false
   has_time_series: false

See the has_time_series field at the bottom is false.

Recall that we also can see a summary of the system by printing it:

julia> systemSystem
┌───────────────────┬───────────────┐
│ Property          │ Value         │
├───────────────────┼───────────────┤
│ Name              │               │
│ Description       │               │
│ System Units Base │ NATURAL_UNITS │
│ Base Power        │ 100.0         │
│ Base Frequency    │ 60.0          │
│ Num Components    │ 4             │
└───────────────────┴───────────────┘

Static Components
┌───────────────────┬───────┐
│ Type              │ Count │
├───────────────────┼───────┤
│ ACBus             │ 1     │
│ PowerLoad         │ 2     │
│ RenewableDispatch │ 1     │
└───────────────────┴───────┘

Observe that there is no mention of time series data in the system yet.

Add and Retrieve a Single Time Series

Let's start by defining and attaching the wind measurements shown in the data above. This is a single time series profile, so we will use a SingleTimeSeries.

First, define a TimeSeries.TimeArray of input data, using the 5-minute resolution to define the time-stamps in the example data:

julia> wind_values = [6.0, 7, 7, 6, 7, 9, 9, 9, 8, 8, 7, 6, 5, 5, 5, 5, 5, 6, 6, 6, 7, 6, 7, 7];
julia> resolution = Dates.Minute(5);
julia> timestamps = range(DateTime("2020-01-01T08:00:00"); step = resolution, length = 24);
julia> wind_timearray = TimeArray(timestamps, wind_values);

Now, use the input data to define a Single Time Series in PowerSystems:

julia> wind_time_series = SingleTimeSeries(;
           name = "max_active_power",
           data = wind_timearray,
       );

Note that we've chosen the name max_active_power, which is the default time series profile name when using PowerSimulations.jl for simulations.

So far, this time series has been defined, but not attached to our System in any way. Now, attach it to wind1 using add_time_series!:

julia> add_time_series!(system, wind1, wind_time_series);

Let's double-check this worked by calling show_time_series:

julia> show_time_series(wind1)┌──────────────────┬──────────────────┬─────────────────────┬─────────────────────┬────────┬─────────────────────┐
│ time_series_type │             name │   initial_timestamp │          resolution │ length │            features │
│           String │           String │      Dates.DateTime │   Dates.Millisecond │  Int64 │   Dict{String, Any} │
├──────────────────┼──────────────────┼─────────────────────┼─────────────────────┼────────┼─────────────────────┤
│ SingleTimeSeries │ max_active_power │ 2020-01-01T08:00:00 │ 300000 milliseconds │     24 │ Dict{String, Any}() │
└──────────────────┴──────────────────┴─────────────────────┴─────────────────────┴────────┴─────────────────────┘

Now wind1 has the first time-series data set. Recall that you can also print wind1 and check the has_time_series field like we did above.

Finally, let's retrieve and inspect the new timeseries, using get_time_series_array:

julia> get_time_series_array(SingleTimeSeries, wind1, "max_active_power")24×1 TimeSeries.TimeArray{Float64, 1, Dates.DateTime, Vector{Float64}} 2020-01-01T08:00:00 to 2020-01-01T09:55:00
┌─────────────────────┬─────┐
│                     │ A   │
├─────────────────────┼─────┤
│ 2020-01-01T08:00:00 │ 6.0 │
│ 2020-01-01T08:05:00 │ 7.0 │
│ 2020-01-01T08:10:00 │ 7.0 │
│ 2020-01-01T08:15:00 │ 6.0 │
│ 2020-01-01T08:20:00 │ 7.0 │
│ 2020-01-01T08:25:00 │ 9.0 │
│ 2020-01-01T08:30:00 │ 9.0 │
│ 2020-01-01T08:35:00 │ 9.0 │
│          ⋮          │  ⋮  │
│ 2020-01-01T09:25:00 │ 6.0 │
│ 2020-01-01T09:30:00 │ 6.0 │
│ 2020-01-01T09:35:00 │ 6.0 │
│ 2020-01-01T09:40:00 │ 7.0 │
│ 2020-01-01T09:45:00 │ 6.0 │
│ 2020-01-01T09:50:00 │ 7.0 │
│ 2020-01-01T09:55:00 │ 7.0 │
└─────────────────────┴─────┘
               9 rows omitted

Verify this matches your expectation based on the input data.

Add and Retrieve a Forecast

Next, let's add the wind power forecasts. We will use a Deterministic format for the point forecasts.

Because we have forecasts with at different initial times, the input data must be a dictionary where the keys are the initial times and the values are vectors or TimeSeries.TimeArrays of the forecast data. Set up the example input data:

julia> wind_forecast_data = Dict(
           DateTime("2020-01-01T08:00:00") => [5.0, 6, 7, 7, 7, 8, 9, 10, 10, 9, 7, 5],
           DateTime("2020-01-01T08:30:00") => [9.0, 9, 9, 9, 8, 7, 6, 5, 4, 5, 4, 4],
           DateTime("2020-01-01T09:00:00") => [6.0, 6, 5, 5, 4, 5, 6, 7, 7, 7, 6, 6],
       );

Define the Deterministic forecast and attach it to wind1:

julia> wind_forecast = Deterministic("max_active_power", wind_forecast_data, resolution);
julia> add_time_series!(system, wind1, wind_forecast);

Let's call show_time_series once again:

julia> show_time_series(wind1)┌──────────────────┬──────────────────┬─────────────────────┬──────────────┬──────────────┬──────────────────────┬───────┬─────────────────────┐
│ time_series_type │             name │   initial_timestamp │   resolution │      horizon │             interval │ count │            features │
│           String │           String │      Dates.DateTime │ Dates.Minute │ Dates.Minute │    Dates.Millisecond │ Int64 │   Dict{String, Any} │
├──────────────────┼──────────────────┼─────────────────────┼──────────────┼──────────────┼──────────────────────┼───────┼─────────────────────┤
│    Deterministic │ max_active_power │ 2020-01-01T08:00:00 │    5 minutes │   60 minutes │ 1800000 milliseconds │     3 │ Dict{String, Any}() │
└──────────────────┴──────────────────┴─────────────────────┴──────────────┴──────────────┴──────────────────────┴───────┴─────────────────────┘
┌──────────────────┬──────────────────┬─────────────────────┬─────────────────────┬────────┬─────────────────────┐
│ time_series_type │             name │   initial_timestamp │          resolution │ length │            features │
│           String │           String │      Dates.DateTime │   Dates.Millisecond │  Int64 │   Dict{String, Any} │
├──────────────────┼──────────────────┼─────────────────────┼─────────────────────┼────────┼─────────────────────┤
│ SingleTimeSeries │ max_active_power │ 2020-01-01T08:00:00 │ 300000 milliseconds │     24 │ Dict{String, Any}() │
└──────────────────┴──────────────────┴─────────────────────┴─────────────────────┴────────┴─────────────────────┘

Notice that we now have two types of time series listed – the single time series and the forecasts.

Finally, let's retrieve the forecast data to double check it was added properly, specifying the initial time to get the 2nd forecast window starting at 8:30:

julia> get_time_series_array(
           Deterministic,
           wind1,
           "max_active_power";
           start_time = DateTime("2020-01-01T08:30:00"),
       )12×1 TimeSeries.TimeArray{Float64, 1, Dates.DateTime, SubArray{Float64, 1, Vector{Float64}, Tuple{UnitRange{Int64}}, true}} 2020-01-01T08:30:00 to 2020-01-01T09:25:00
┌─────────────────────┬─────┐
│                     │ A   │
├─────────────────────┼─────┤
│ 2020-01-01T08:30:00 │ 9.0 │
│ 2020-01-01T08:35:00 │ 9.0 │
│ 2020-01-01T08:40:00 │ 9.0 │
│ 2020-01-01T08:45:00 │ 9.0 │
│ 2020-01-01T08:50:00 │ 8.0 │
│ 2020-01-01T08:55:00 │ 7.0 │
│ 2020-01-01T09:00:00 │ 6.0 │
│ 2020-01-01T09:05:00 │ 5.0 │
│ 2020-01-01T09:10:00 │ 4.0 │
│ 2020-01-01T09:15:00 │ 5.0 │
│ 2020-01-01T09:20:00 │ 4.0 │
│ 2020-01-01T09:25:00 │ 4.0 │
└─────────────────────┴─────┘

Add A Time Series Using Scaling Factors

Let's add the load time series. Recall that this data is normalized to the peak system power, so we'll use it to scale both of our loads. We call normalized time series data scaling factors.

First, let's create our input data TimeSeries.TimeArray with the example data and the same time stamps we used in the wind time series:

julia> load_values = [0.3, 0.3, 0.3, 0.3, 0.4, 0.4, 0.4, 0.4, 0.5, 0.5, 0.6, 0.6,
           0.7, 0.8, 0.8, 0.8, 0.8, 0.8, 0.9, 0.8, 0.8, 0.8, 0.8, 0.8];
julia> load_timearray = TimeArray(timestamps, load_values);

Again, define a SingleTimeSeries, but this time use the scaling_factor_multiplierparameter to scale this time series from normalized values to power values:

julia> load_time_series = SingleTimeSeries(;
           name = "max_active_power",
           data = load_timearray,
           scaling_factor_multiplier = get_max_active_power,
       );

Notice that we assigned the get_max_active_power function to scale the time series, rather than a value, making the time series reusable for multiple components or multiple fields in a component.

Now, add the scaling factor time series to both loads to save memory and avoid data duplication:

julia> add_time_series!(system, [load1, load2], load_time_series);

Let's take a look at load1, including printing its parameters...

julia> load1PowerLoad: load1:
   name: load1
   available: true
   bus: ACBus: bus1
   active_power: 0.0
   reactive_power: 0.0
   base_power: 10.0
   max_active_power: 10.0
   max_reactive_power: 0.0
   services: 0-element Vector{Service}
   dynamic_injector: nothing
   ext: Dict{String, Any}()
   InfrastructureSystems.SystemUnitsSettings:
      base_value: 100.0
      unit_system: UnitSystem.NATURAL_UNITS = 2
   has_supplemental_attributes: false
   has_time_series: true

...as well as its time series:

julia> show_time_series(load1)┌──────────────────┬──────────────────┬─────────────────────┬─────────────────────┬────────┬─────────────────────┐
│ time_series_type │             name │   initial_timestamp │          resolution │ length │            features │
│           String │           String │      Dates.DateTime │   Dates.Millisecond │  Int64 │   Dict{String, Any} │
├──────────────────┼──────────────────┼─────────────────────┼─────────────────────┼────────┼─────────────────────┤
│ SingleTimeSeries │ max_active_power │ 2020-01-01T08:00:00 │ 300000 milliseconds │     24 │ Dict{String, Any}() │
└──────────────────┴──────────────────┴─────────────────────┴─────────────────────┴────────┴─────────────────────┘
Important

Notice that each load now has two references to max_active_power. This is intentional. There is the parameter, max_active_power, which is the maximum demand of each load at any time (10 MW). There is also max_active_power the time series, which is the time varying demand over the 2-hour window, calculated using the scaling factors and the max_active_power parameter.

This means that if we change the max_active_power parameter, the time series will also change when we retrieve it! This is also true when we apply the same scaling factors to multiple components or parameters.

Let's check the impact that these two max_active_power data sources have on the times series data when we retrieve it. Get the max_active_power time series for load1:

julia> get_time_series_array(SingleTimeSeries, load1, "max_active_power") # in MW24×1 TimeSeries.TimeArray{Float64, 1, Dates.DateTime, Vector{Float64}} 2020-01-01T08:00:00 to 2020-01-01T09:55:00
┌─────────────────────┬─────┐
│                     │ A   │
├─────────────────────┼─────┤
│ 2020-01-01T08:00:00 │ 3.0 │
│ 2020-01-01T08:05:00 │ 3.0 │
│ 2020-01-01T08:10:00 │ 3.0 │
│ 2020-01-01T08:15:00 │ 3.0 │
│ 2020-01-01T08:20:00 │ 4.0 │
│ 2020-01-01T08:25:00 │ 4.0 │
│ 2020-01-01T08:30:00 │ 4.0 │
│ 2020-01-01T08:35:00 │ 4.0 │
│          ⋮          │  ⋮  │
│ 2020-01-01T09:25:00 │ 8.0 │
│ 2020-01-01T09:30:00 │ 9.0 │
│ 2020-01-01T09:35:00 │ 8.0 │
│ 2020-01-01T09:40:00 │ 8.0 │
│ 2020-01-01T09:45:00 │ 8.0 │
│ 2020-01-01T09:50:00 │ 8.0 │
│ 2020-01-01T09:55:00 │ 8.0 │
└─────────────────────┴─────┘
               9 rows omitted

See that the normalized values have been scaled up by 10 MW.

Now let's at load2. First check its max_active_power parameter:

julia> get_max_active_power(load2)30.0

This has a higher peak maximum demand of 30 MW.

Next, retrieve it's max_active_power time series:

julia> get_time_series_array(SingleTimeSeries, load2, "max_active_power") # in MW24×1 TimeSeries.TimeArray{Float64, 1, Dates.DateTime, Vector{Float64}} 2020-01-01T08:00:00 to 2020-01-01T09:55:00
┌─────────────────────┬──────┐
│                     │ A    │
├─────────────────────┼──────┤
│ 2020-01-01T08:00:00 │  9.0 │
│ 2020-01-01T08:05:00 │  9.0 │
│ 2020-01-01T08:10:00 │  9.0 │
│ 2020-01-01T08:15:00 │  9.0 │
│ 2020-01-01T08:20:00 │ 12.0 │
│ 2020-01-01T08:25:00 │ 12.0 │
│ 2020-01-01T08:30:00 │ 12.0 │
│ 2020-01-01T08:35:00 │ 12.0 │
│          ⋮          │  ⋮   │
│ 2020-01-01T09:25:00 │ 24.0 │
│ 2020-01-01T09:30:00 │ 27.0 │
│ 2020-01-01T09:35:00 │ 24.0 │
│ 2020-01-01T09:40:00 │ 24.0 │
│ 2020-01-01T09:45:00 │ 24.0 │
│ 2020-01-01T09:50:00 │ 24.0 │
│ 2020-01-01T09:55:00 │ 24.0 │
└─────────────────────┴──────┘
                9 rows omitted

Observe the difference compared to load1's time series.

Finally, retrieve the underlying time series data with no scaling factor multiplier applied:

julia> get_time_series_array(SingleTimeSeries,
           load2,
           "max_active_power";
           ignore_scaling_factors = true,
       )24×1 TimeSeries.TimeArray{Float64, 1, Dates.DateTime, Vector{Float64}} 2020-01-01T08:00:00 to 2020-01-01T09:55:00
┌─────────────────────┬─────┐
│                     │ A   │
├─────────────────────┼─────┤
│ 2020-01-01T08:00:00 │ 0.3 │
│ 2020-01-01T08:05:00 │ 0.3 │
│ 2020-01-01T08:10:00 │ 0.3 │
│ 2020-01-01T08:15:00 │ 0.3 │
│ 2020-01-01T08:20:00 │ 0.4 │
│ 2020-01-01T08:25:00 │ 0.4 │
│ 2020-01-01T08:30:00 │ 0.4 │
│ 2020-01-01T08:35:00 │ 0.4 │
│          ⋮          │  ⋮  │
│ 2020-01-01T09:25:00 │ 0.8 │
│ 2020-01-01T09:30:00 │ 0.9 │
│ 2020-01-01T09:35:00 │ 0.8 │
│ 2020-01-01T09:40:00 │ 0.8 │
│ 2020-01-01T09:45:00 │ 0.8 │
│ 2020-01-01T09:50:00 │ 0.8 │
│ 2020-01-01T09:55:00 │ 0.8 │
└─────────────────────┴─────┘
               9 rows omitted

Notice that this is the normalized input data, which is still being stored underneath. Each load is using a reference to that data when we call get_time_series_array to avoid unnecessary data duplication.

Transform a SingleTimeSeries into a Forecast

Finally, let's use a workaround to handle the missing load forecast data. We will assume a perfect forecast where the forecast is based on the SingleTimeSeries we just added.

Rather than unnecessarily duplicating and reformatting data, use PowerSystems.jl's dedicated transform_single_time_series! function to generate a DeterministicSingleTimeSeries, which saves memory while behaving just like a Deterministic forecast:

julia> transform_single_time_series!(
           system,
           Dates.Hour(1), # horizon
           Dates.Minute(30), # interval
       );

Let's see the results for load1's time series summary:

julia> show_time_series(load1)┌───────────────────────────────┬──────────────────┬─────────────────────┬─────────────────────┬────────────┬──────────────┬───────┬─────────────────────┐
│              time_series_type │             name │   initial_timestamp │          resolution │    horizon │     interval │ count │            features │
│                        String │           String │      Dates.DateTime │   Dates.Millisecond │ Dates.Hour │ Dates.Minute │ Int64 │   Dict{String, Any} │
├───────────────────────────────┼──────────────────┼─────────────────────┼─────────────────────┼────────────┼──────────────┼───────┼─────────────────────┤
│ DeterministicSingleTimeSeries │ max_active_power │ 2020-01-01T08:00:00 │ 300000 milliseconds │     1 hour │   30 minutes │     3 │ Dict{String, Any}() │
└───────────────────────────────┴──────────────────┴─────────────────────┴─────────────────────┴────────────┴──────────────┴───────┴─────────────────────┘
┌──────────────────┬──────────────────┬─────────────────────┬─────────────────────┬────────┬─────────────────────┐
│ time_series_type │             name │   initial_timestamp │          resolution │ length │            features │
│           String │           String │      Dates.DateTime │   Dates.Millisecond │  Int64 │   Dict{String, Any} │
├──────────────────┼──────────────────┼─────────────────────┼─────────────────────┼────────┼─────────────────────┤
│ SingleTimeSeries │ max_active_power │ 2020-01-01T08:00:00 │ 300000 milliseconds │     24 │ Dict{String, Any}() │
└──────────────────┴──────────────────┴─────────────────────┴─────────────────────┴────────┴─────────────────────┘

Notice we now have a load forecast data set with the resolution, horizon, and, interval matching our wind forecasts.

Retrieve the first forecast window:

julia> get_time_series_array(
           DeterministicSingleTimeSeries,
           load1,
           "max_active_power";
           start_time = DateTime("2020-01-01T08:00:00"),
       )12×1 TimeSeries.TimeArray{Float64, 1, Dates.DateTime, Vector{Float64}} 2020-01-01T08:00:00 to 2020-01-01T08:55:00
┌─────────────────────┬─────┐
│                     │ A   │
├─────────────────────┼─────┤
│ 2020-01-01T08:00:00 │ 3.0 │
│ 2020-01-01T08:05:00 │ 3.0 │
│ 2020-01-01T08:10:00 │ 3.0 │
│ 2020-01-01T08:15:00 │ 3.0 │
│ 2020-01-01T08:20:00 │ 4.0 │
│ 2020-01-01T08:25:00 │ 4.0 │
│ 2020-01-01T08:30:00 │ 4.0 │
│ 2020-01-01T08:35:00 │ 4.0 │
│ 2020-01-01T08:40:00 │ 5.0 │
│ 2020-01-01T08:45:00 │ 5.0 │
│ 2020-01-01T08:50:00 │ 6.0 │
│ 2020-01-01T08:55:00 │ 6.0 │
└─────────────────────┴─────┘

See that load1's scaling factor multiplier is still being applied as expected.

Continue to the next section to address one more impact of calling transform_single_time_series! on the entire System.

Finding, Retrieving, and Inspecting Time Series

Now, let's complete this tutorial by doing a few sanity checks on the data that we've added, where are we will also examine components with time series and retrieve the time series data in a few more ways.

First, recall that we can print a component to check its has_time_series field:

julia> load1PowerLoad: load1:
   name: load1
   available: true
   bus: ACBus: bus1
   active_power: 0.0
   reactive_power: 0.0
   base_power: 10.0
   max_active_power: 10.0
   max_reactive_power: 0.0
   services: 0-element Vector{Service}
   dynamic_injector: nothing
   ext: Dict{String, Any}()
   InfrastructureSystems.SystemUnitsSettings:
      base_value: 100.0
      unit_system: UnitSystem.NATURAL_UNITS = 2
   has_supplemental_attributes: false
   has_time_series: true

Also, recall we can print the System to summarize the data in our system:

julia> systemSystem
┌───────────────────┬───────────────┐
│ Property          │ Value         │
├───────────────────┼───────────────┤
│ Name              │               │
│ Description       │               │
│ System Units Base │ NATURAL_UNITS │
│ Base Power        │ 100.0         │
│ Base Frequency    │ 60.0          │
│ Num Components    │ 4             │
└───────────────────┴───────────────┘

Static Components
┌───────────────────┬───────┐
│ Type              │ Count │
├───────────────────┼───────┤
│ ACBus             │ 1     │
│ PowerLoad         │ 2     │
│ RenewableDispatch │ 1     │
└───────────────────┴───────┘

Time Series Summary
┌───────────────────┬────────────────┬───────────────────────────────┬──────────
│ owner_type        │ owner_category │ time_series_type              │ time_se ⋯
│ String            │ String         │ String                        │ String  ⋯
├───────────────────┼────────────────┼───────────────────────────────┼──────────
│ PowerLoad         │ Component      │ DeterministicSingleTimeSeries │ Forecas ⋯
│ PowerLoad         │ Component      │ SingleTimeSeries              │ StaticT ⋯
│ RenewableDispatch │ Component      │ Deterministic                 │ Forecas ⋯
│ RenewableDispatch │ Component      │ DeterministicSingleTimeSeries │ Forecas ⋯
│ RenewableDispatch │ Component      │ SingleTimeSeries              │ StaticT ⋯
└───────────────────┴────────────────┴───────────────────────────────┴──────────
                                                               4 columns omitted

Notice that a new table has been added – the Time Series Summary, showing the count of each Type of component that has a given time series type.

Additionally, see that there are both Deterministic and DeterministicSingleTimeSeries forecasts for our RenewableDispatch generator (wind1). This was a side effect of transform_single_time_series! which added DeterministicSingleTimeSeries for all StaticTimeSeries in the system, even though we don't need one for wind.

Let's remove it with remove_time_series!. Since we have one wind generator, we could easily do it for that component, but let's do programmatically instead by its Type:

julia> for g in get_components(x -> has_time_series(x), RenewableDispatch, system)
           remove_time_series!(system, DeterministicSingleTimeSeries, g, "max_active_power")
       end

Notice that we also filtered for components where has_time_series is true, which is a simple way to find and manipulate components with time series.

Let's double check wind1 now:

julia> show_time_series(wind1)┌──────────────────┬──────────────────┬─────────────────────┬──────────────┬──────────────┬──────────────────────┬───────┬─────────────────────┐
│ time_series_type │             name │   initial_timestamp │   resolution │      horizon │             interval │ count │            features │
│           String │           String │      Dates.DateTime │ Dates.Minute │ Dates.Minute │    Dates.Millisecond │ Int64 │   Dict{String, Any} │
├──────────────────┼──────────────────┼─────────────────────┼──────────────┼──────────────┼──────────────────────┼───────┼─────────────────────┤
│    Deterministic │ max_active_power │ 2020-01-01T08:00:00 │    5 minutes │   60 minutes │ 1800000 milliseconds │     3 │ Dict{String, Any}() │
└──────────────────┴──────────────────┴─────────────────────┴──────────────┴──────────────┴──────────────────────┴───────┴─────────────────────┘
┌──────────────────┬──────────────────┬─────────────────────┬─────────────────────┬────────┬─────────────────────┐
│ time_series_type │             name │   initial_timestamp │          resolution │ length │            features │
│           String │           String │      Dates.DateTime │   Dates.Millisecond │  Int64 │   Dict{String, Any} │
├──────────────────┼──────────────────┼─────────────────────┼─────────────────────┼────────┼─────────────────────┤
│ SingleTimeSeries │ max_active_power │ 2020-01-01T08:00:00 │ 300000 milliseconds │     24 │ Dict{String, Any}() │
└──────────────────┴──────────────────┴─────────────────────┴─────────────────────┴────────┴─────────────────────┘

See the unnecessary data is gone.

Finally, let's do a last data sanity check on the forecasts. Since we defined the wind time series in MW instead of scaling factors, let's make sure none of our forecasts exceeds the max_active_power parameter.

Instead of using get_time_series_array where we need to remember some details of the time series we're looking up, let's use get_time_series_keys to refresh our memories:

julia> keys = get_time_series_keys(wind1)2-element Vector{TimeSeriesKey}:
 ForecastKey(Deterministic, "max_active_power", Dates.DateTime("2020-01-01T08:00:00"), Dates.Minute(5), Dates.Minute(60), Dates.Millisecond(1800000), 3, Dict{String, Any}())
 StaticTimeSeriesKey(SingleTimeSeries, "max_active_power", Dates.DateTime("2020-01-01T08:00:00"), Dates.Millisecond(300000), 24, Dict{String, Any}())

See the forecast key is first, so let's retrieve it using get_time_series:

julia> forecast = get_time_series(wind1, keys[1])Deterministic("max_active_power", DataStructures.SortedDict(Dates.DateTime("2020-01-01T08:00:00") => [5.0, 6.0, 7.0, 7.0, 7.0, 8.0, 9.0, 10.0, 10.0, 9.0, 7.0, 5.0], Dates.DateTime("2020-01-01T08:30:00") => [9.0, 9.0, 9.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 5.0, 4.0, 4.0], Dates.DateTime("2020-01-01T09:00:00") => [6.0, 6.0, 5.0, 5.0, 4.0, 5.0, 6.0, 7.0, 7.0, 7.0, 6.0, 6.0]), Dates.Minute(5), nothing, InfrastructureSystems.InfrastructureSystemsInternal(Base.UUID("3ef68da0-03b6-4bf0-80c8-0da5814fd3e0"), nothing, nothing, nothing))

See that unlike when we used get_time_series_array, this returns an object we can manipulate.

Use iterate_windows to cycle through the 3 forecast windows and inspect the peak value:

julia> for window in iterate_windows(forecast)
           @show values(maximum(window))
       endvalues(maximum(window)) = [10.0]
values(maximum(window)) = [9.0]
values(maximum(window)) = [7.0]

Finally, use get_max_active_power to check the expected maximum:

julia> get_max_active_power(wind1)10.0

See that the forecasts are not exceeding this maximum – sanity check complete.

Tip

Unlike PowerLoad components, RenewableDispatch components do not have a max_active_power field, so check get_max_active_power to see how its calculated.

Next Steps

In this tutorial, you defined, added, and retrieved four time series data sets, including static time series and deterministic forecasts. Along the way, we reduced data duplication using normalized scaling factors for reuse by multiple components or component fields, as well as by referencing a StaticTimeSeries to address missing forecast data.

Next you might like to: