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> wind1
RenewableDispatch: 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> system
System ┌───────────────────┬───────────────┐ │ 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.TimeArray
s 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_multiplier
parameter 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> load1
PowerLoad: 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}() │ └──────────────────┴──────────────────┴─────────────────────┴─────────────────────┴────────┴─────────────────────┘
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 MW
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 │ 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 MW
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 │ 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> load1
PowerLoad: 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> system
System ┌───────────────────┬───────────────┐ │ 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)) end
values(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.
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: