Skip to content

Commit

Permalink
Merge pull request #12 from NREL/timeseries-forwarding
Browse files Browse the repository at this point in the history
Forward forecast indexing and splitting methods to TimeSeries.
  • Loading branch information
jd-lara authored Sep 17, 2019
2 parents 828a055 + a4543ec commit b345229
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 2 deletions.
145 changes: 143 additions & 2 deletions src/forecasts.jl
Original file line number Diff line number Diff line change
Expand Up @@ -530,8 +530,10 @@ function convert_type!(
throw(DataFormatError("unmatched timeseries UUID: $uuid $forecast"))
end
timeseries = forecast_uuid_to_timeseries[uuid]
forecast_base_type = getfield(InfrastructureSystems, Symbol(strip_module_names(string(forecast.type))))
push!(forecasts_, convert_type(forecast_base_type, forecast, component_cache, timeseries))
forecast_base_type = getfield(InfrastructureSystems,
Symbol(strip_module_names(string(forecast.type))))
val = convert_type(forecast_base_type, forecast, component_cache, timeseries)
push!(forecasts_, val)
end

_add_forecasts!(forecasts, forecasts_)
Expand Down Expand Up @@ -605,3 +607,142 @@ function get_resolution(ts::TimeSeries.TimeArray)
return res[1]
end

"""
Creates a new forecast from an existing forecast with a split TimeArray.
# Arguments
- `is_copy::Bool=true`: Reset internal indices because the TimeArray is a fresh copy.
"""
function _split_forecast(
forecast::T,
data::TimeSeries.TimeArray;
is_copy=true,
) where T <: Forecast
vals = []
for (fname, ftype) in zip(fieldnames(T), fieldtypes(T))
if ftype <: TimeSeries.TimeArray
val = data
elseif ftype <: InfrastructureSystemsInternal
# Need to create a new UUID.
continue
else
val = getfield(forecast, fname)
end

push!(vals, val)
end

new_forecast = T(vals...)
if is_copy
new_forecast.start_index = 1
new_forecast.horizon = length(get_data(new_forecast))
end
return new_forecast
end

function Base.getindex(forecast::Forecast, args...)
return _split_forecast(forecast, getindex(get_timeseries(forecast), args...))
end

Base.first(forecast::Forecast) = head(forecast, 1)

Base.last(forecast::Forecast) = tail(forecast, 1)

Base.firstindex(forecast::Forecast) = firstindex(get_timeseries(forecast))

Base.lastindex(forecast::Forecast) = lastindex(get_timeseries(forecast))

Base.lastindex(forecast::Forecast, d) = lastindex(get_timeseries(forecast), d)

Base.eachindex(forecast::Forecast) = eachindex(get_timeseries(forecast))

Base.iterate(forecast::Forecast, n = 1) = iterate(get_timeseries(forecast), n)

"""
when(forecast::Forecast, period::Function, t::Integer)
Refer to TimeSeries.when(). Underlying data is copied.
"""
function when(forecast::Forecast, period::Function, t::Integer)
new = _split_forecast(forecast, TimeSeries.when(get_timeseries(forecast), period, t))

end

"""
from(forecast::Forecast, timestamp)
Return a forecast truncated starting with timestamp. Underlying data is not copied.
"""
function from(forecast::Forecast, timestamp)
# Don't use TimeSeries.from because it makes a copy.
start_index = get_start_index(forecast)
end_index = start_index + get_horizon(forecast)
for i in get_start_index(forecast) : end_index
if TimeSeries.timestamp(get_data(forecast))[i] >= timestamp
fcast = _split_forecast(forecast, get_data(forecast); is_copy=false)
fcast.start_index = i
fcast.horizon = end_index - i
return fcast
end
end

# Do whatever TimeSeries does if the timestamp is after the forecast.
return _split_forecast(forecast, TimeSeries.from(get_timeseries(forecast), timestamp))
end

"""
to(forecast::Forecast, timestamp)
Return a forecast truncated after timestamp. Underlying data is not copied.
"""
function to(forecast::Forecast, timestamp)
# Don't use TimeSeries.from because it makes a copy.
start_index = get_start_index(forecast)
end_index = start_index + get_horizon(forecast)
for i in get_start_index(forecast) : end_index
tstamp = TimeSeries.timestamp(get_data(forecast))[i]
if tstamp < timestamp
continue
elseif tstamp == timestamp
end_index = i
else
@assert tstamp > timestamp
end_index = i - 1
end

fcast = _split_forecast(forecast, get_data(forecast); is_copy=false)
fcast.horizon = end_index - start_index + 1
return fcast
end

# Do whatever TimeSeries does if the timestamp is after the forecast.
return _split_forecast(forecast, TimeSeries.to(get_timeseries(forecast), timestamp))
end

"""
head(forecast::Forecast)
head(forecast::Forecast, num)
Return a forecast with only the first num values.
"""
function head(forecast::Forecast)
return _split_forecast(forecast, TimeSeries.head(get_timeseries(forecast)))
end

function head(forecast::Forecast, num)
return _split_forecast(forecast, TimeSeries.head(get_timeseries(forecast), num))
end

"""
tail(forecast::Forecast)
tail(forecast::Forecast, num)
Return a forecast with only the ending num values.
"""
function tail(forecast::Forecast)
return _split_forecast(forecast, TimeSeries.tail(get_timeseries(forecast)))
end

function tail(forecast::Forecast, num)
return _split_forecast(forecast, TimeSeries.tail(get_timeseries(forecast), num))
end
3 changes: 3 additions & 0 deletions src/generated/Deterministic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ function Deterministic(; component, label, resolution, initial_time, data, start
Deterministic(component, label, resolution, initial_time, data, start_index, horizon, )
end

function Deterministic{T}(component, label, resolution, initial_time, data, start_index, horizon, ) where T <: InfrastructureSystemsType
Deterministic(component, label, resolution, initial_time, data, start_index, horizon, InfrastructureSystemsInternal())
end

"""Get Deterministic component."""
get_component(value::Deterministic) = value.component
Expand Down
3 changes: 3 additions & 0 deletions src/generated/Probabilistic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ function Probabilistic(; component, label, resolution, initial_time, percentiles
Probabilistic(component, label, resolution, initial_time, percentiles, data, start_index, horizon, )
end

function Probabilistic{T}(component, label, resolution, initial_time, percentiles, data, start_index, horizon, ) where T <: InfrastructureSystemsType
Probabilistic(component, label, resolution, initial_time, percentiles, data, start_index, horizon, InfrastructureSystemsInternal())
end

"""Get Probabilistic component."""
get_component(value::Probabilistic) = value.component
Expand Down
3 changes: 3 additions & 0 deletions src/generated/ScenarioBased.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ function ScenarioBased(; component, label, resolution, initial_time, scenario_co
ScenarioBased(component, label, resolution, initial_time, scenario_count, data, start_index, horizon, )
end

function ScenarioBased{T}(component, label, resolution, initial_time, scenario_count, data, start_index, horizon, ) where T <: InfrastructureSystemsType
ScenarioBased(component, label, resolution, initial_time, scenario_count, data, start_index, horizon, InfrastructureSystemsInternal())
end

"""Get ScenarioBased component."""
get_component(value::ScenarioBased) = value.component
Expand Down
7 changes: 7 additions & 0 deletions src/utils/generate_structs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ function {{struct_name}}(; {{#parameters}}{{^internal}}{{name}}, {{/internal}}{{
{{struct_name}}({{#parameters}}{{^internal}}{{name}}, {{/internal}}{{/parameters}})
end
{{#parametric}}
function {{struct_name}}{T}({{#parameters}}{{^internal}}{{name}}, {{/internal}}{{/parameters}}) where T <: InfrastructureSystemsType
{{#parameters}}
{{/parameters}}
{{struct_name}}({{#parameters}}{{^internal}}{{name}}, {{/internal}}{{/parameters}}InfrastructureSystemsInternal())
end
{{/parametric}}
{{#has_null_values}}
# Constructor for demo purposes; non-functional.
Expand Down
88 changes: 88 additions & 0 deletions test/test_forecasts.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@

import Dates
import TimeSeries

function does_forecast_have_component(data::SystemData, component)
found = false
for forecast in iterate_forecasts(data)
Expand Down Expand Up @@ -86,3 +89,88 @@ end
data = create_system_data(; with_forecasts=true)
summary(devnull, data.forecasts)
end

@testset "Test split_forecast" begin
data = create_system_data(; with_forecasts=true)
forecast = get_all_forecasts(data)[1]

forecasts = get_forecasts(IS.Deterministic, data, IS.get_initial_time(forecast))
split_forecasts!(data, forecasts, Dates.Hour(6), 12)
initial_times = get_forecast_initial_times(data)
@test length(initial_times) == 3

for initial_time in initial_times
for fcast in get_forecasts(Deterministic, data, initial_time)
# The backing TimeArray must be the same.
@test get_data(fcast) === get_data(forecast)
@test length(fcast) == 12
end
end
end

@testset "Test forecast forwarding methods" begin
data = create_system_data(; with_forecasts=true)
forecast = get_all_forecasts(data)[1]

# Iteration
size = 24
@test length(forecast) == size
i = 0
for x in forecast
i += 1
end
@test i == size

# Indexing
@test length(forecast[1:16]) == 16

# when
fcast = IS.when(forecast, TimeSeries.hour, 3)
@test length(fcast) == 1
end

@testset "Test forecast head" begin
data = create_system_data(; with_forecasts=true)
forecast = get_all_forecasts(data)[1]
fcast = IS.head(forecast)
# head returns a length of 6 by default, but don't hard-code that.
@test length(fcast) < length(forecast)

fcast = IS.head(forecast, 10)
@test length(fcast) == 10
end

@testset "Test forecast tail" begin
data = create_system_data(; with_forecasts=true)
forecast = get_all_forecasts(data)[1]
fcast = IS.tail(forecast)
# tail returns a length of 6 by default, but don't hard-code that.
@test length(fcast) < length(forecast)

fcast = IS.head(forecast, 10)
@test length(fcast) == 10
end

@testset "Test forecast from" begin
data = create_system_data(; with_forecasts=true)
forecast = get_all_forecasts(data)[1]
start_time = Dates.DateTime(Dates.today()) + Dates.Hour(3)
fcast = IS.from(forecast, start_time)
@test get_data(fcast) === get_data(forecast)
@test get_start_index(fcast) == 4
@test length(fcast) == 21
@test TimeSeries.timestamp(IS.get_timeseries(fcast))[1] == start_time
end

@testset "Test forecast from" begin
data = create_system_data(; with_forecasts=true)
forecast = get_all_forecasts(data)[1]
for end_time in (Dates.DateTime(Dates.today()) + Dates.Hour(15),
Dates.DateTime(Dates.today()) + Dates.Hour(15) + Dates.Minute(5))
fcast = IS.to(forecast, end_time)
@test get_data(fcast) === get_data(forecast)
@test get_start_index(fcast) + get_horizon(fcast) == 17
@test length(fcast) == 16
@test TimeSeries.timestamp(IS.get_timeseries(fcast))[end] <= end_time
end
end

2 comments on commit b345229

@jd-lara
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register()

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/3589

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if Julia TagBot is installed, or can be done manually through the github interface, or via:

git tag -a v0.1.1 -m "<description of version>" b345229a57e9c34bd27e4c01271b0a34cab27a02
git push origin v0.1.1

Please sign in to comment.