Skip to content

Commit

Permalink
Add files with time intervals of assets and flows and functions to pa…
Browse files Browse the repository at this point in the history
…rse it
  • Loading branch information
abelsiqueira committed Oct 27, 2023
1 parent f1712c7 commit 060c5b5
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 16 deletions.
7 changes: 7 additions & 0 deletions src/input-tables.jl
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,10 @@ struct RepPeriodData
num_time_steps::Int # Numer of time steps
time_scale::Float64 # Duration of each time steps (hours)
end

struct TimeIntervalsData
id::Int
rep_period_id::Int
specification::Symbol
time_intervals::String
end
181 changes: 165 additions & 16 deletions src/io.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export create_parameters_and_sets_from_file, create_graph, save_solution_to_file
export create_parameters_and_sets_from_file,
create_graph, save_solution_to_file, compute_time_intervals

"""
parameters, sets = create_parameters_and_sets_from_file(input_folder)
Expand All @@ -9,22 +10,28 @@ input files in the `input_folder`.
function create_parameters_and_sets_from_file(input_folder::AbstractString)
# Read data
fillpath(filename) = joinpath(input_folder, filename)
assets_data_df = read_csv_with_schema(fillpath("assets-data.csv"), AssetData)
assets_profiles_df = read_csv_with_schema(fillpath("assets-profiles.csv"), AssetProfiles)
flows_data_df = read_csv_with_schema(fillpath("flows-data.csv"), FlowData)
flows_profiles_df = read_csv_with_schema(fillpath("flows-profiles.csv"), FlowProfiles)
rep_period_df = read_csv_with_schema(fillpath("rep-periods-data.csv"), RepPeriodData)

assets_data_df = read_csv_with_schema(fillpath("assets-data.csv"), AssetData)
assets_profiles_df = read_csv_with_schema(fillpath("assets-profiles.csv"), AssetProfiles)
flows_data_df = read_csv_with_schema(fillpath("flows-data.csv"), FlowData)
flows_profiles_df = read_csv_with_schema(fillpath("flows-profiles.csv"), FlowProfiles)
rep_period_df = read_csv_with_schema(fillpath("rep-periods-data.csv"), RepPeriodData)
assets_intervals_df = read_csv_with_schema(fillpath("assets-time-intervals.csv"), TimeIntervalsData)
flows_intervals_df = read_csv_with_schema(fillpath("flows-time-intervals.csv"), TimeIntervalsData)

# Sets and subsets that depend on input data
assets = assets_data_df[assets_data_df.active.==true, :].name #assets in the energy system that are active
assets_producer = assets_data_df[assets_data_df.type.=="producer", :].name #producer assets in the energy system
assets_consumer = assets_data_df[assets_data_df.type.=="consumer", :].name #consumer assets in the energy system
assets_storage = assets_data_df[assets_data_df.type.=="storage", :].name #storage assets in the energy system
assets_hub = assets_data_df[assets_data_df.type.=="hub", :].name #hub assets in the energy system
assets_conversion = assets_data_df[assets_data_df.type.=="conversion", :].name #conversion assets in the energy system
assets_investment = assets_data_df[assets_data_df.investable.==true, :].name #assets with investment method in the energy system
rep_periods = unique(assets_profiles_df.rep_period_id) #representative periods
time_steps = Dict(row.id => 1:row.num_time_steps for row in eachrow(rep_period_df)) #time steps in the RP (e.g., hours), that are dependent on RP
assets = assets_data_df[assets_data_df.active.==true, :].name #assets in the energy system that are active
assets_producer = assets_data_df[assets_data_df.type.=="producer", :].name #producer assets in the energy system
assets_consumer = assets_data_df[assets_data_df.type.=="consumer", :].name #consumer assets in the energy system
assets_storage = assets_data_df[assets_data_df.type.=="storage", :].name #storage assets in the energy system
assets_hub = assets_data_df[assets_data_df.type.=="hub", :].name #hub assets in the energy system
assets_conversion = assets_data_df[assets_data_df.type.=="conversion", :].name #conversion assets in the energy system
assets_investment = assets_data_df[assets_data_df.investable.==true, :].name #assets with investment method in the energy system
rep_periods = unique(assets_profiles_df.rep_period_id) #representative periods
time_steps = Dict(row.id => 1:row.num_time_steps for row in eachrow(rep_period_df)) #time steps in the RP (e.g., hours), that are dependent on RP
flows = [(row.from_asset, row.to_asset) for row in eachrow(flows_data_df)]
time_intervals_per_asset = compute_time_intervals(assets_intervals_df, assets, time_steps)
time_intervals_per_flow = compute_time_intervals(flows_intervals_df, flows, time_steps)

# Parameters for system
rep_weight = Dict((row.id) => row.weight for row in eachrow(rep_period_df)) #representative period weight [h]
Expand All @@ -36,7 +43,6 @@ function create_parameters_and_sets_from_file(input_folder::AbstractString)
) # asset profile [p.u.]

# Parameter for profile of flow
flows = [(row.from_asset, row.to_asset) for row in eachrow(flows_data_df)]
flows_profile = Dict(
(flows[row.id], row.rep_period_id, row.time_step) => row.value for
row in eachrow(flows_profiles_df)
Expand Down Expand Up @@ -94,6 +100,7 @@ function create_parameters_and_sets_from_file(input_folder::AbstractString)
flows_unit_capacity[(row.from_asset, row.to_asset)] = max(row.export_capacity, row.import_capacity)
end

# Define time intervals
params = (
assets_init_capacity = assets_init_capacity,
assets_investment_cost = assets_investment_cost,
Expand Down Expand Up @@ -125,6 +132,8 @@ function create_parameters_and_sets_from_file(input_folder::AbstractString)
assets_conversion = assets_conversion,
rep_periods = rep_periods,
time_steps = time_steps,
time_intervals_per_asset = time_intervals_per_asset,
time_intervals_per_flow = time_intervals_per_flow,
)

return params, sets
Expand Down Expand Up @@ -198,3 +207,143 @@ function create_graph(assets_path, flows_path)

return graph
end

"""
_parse_time_intervals(Val(specification), time_step_string, rp_time_steps)
Parses the time_step_string according to the specification.
The representative period time steps (`rp_time_steps`) might not be used in the computation,
but it will be used for validation.
The specification defines what is expected from the `time_step_string`:
- `:uniform`: The `time_step_string` should be a single number indicating the duration of
each interval. Examples: "3", "4", "1".
- `:explicit`: The `time_step_string` should be a semicolon-separated list of integers.
Each integer is a duration of an interval. Examples: "3;3;3;3", "4;4;4",
"1;1;1;1;1;1;1;1;1;1;1;1", and "3;3;4;2".
- `:math`: The `time_step_string` should be an expression of the form `NxD+NxD…`, where `D`
is the duration of the interval and `N` is the number of intervals. Examples: "4x3", "3x4",
"12x1", and "2x3+1x4+1x2".
The generated intervals will be ranges (`a:b`). The first interval starts at `1`, and the last
interval ends at `length(rp_time_steps)`.
The following table summarizes the formats for a `rp_time_steps = 1:12`:
| Output | :uniform | :explicit | :math |
|:--------------------- |:-------- |:----------------------- |:----------- |
| 1:3, 4:6, 7:9, 10:12 | 3 | 3;3;3;3 | 4x3 |
| 1:4, 5:8, 9:12 | 4 | 4;4;4 | 3x4 |
| 1:1, 2:2, …, 12:12 | 1 | 1;1;1;1;1;1;1;1;1;1;1;1 | 12x1 |
| 1:3, 4:6, 7:10, 11:12 | NA | 3;3;4;2 | 2x3+1x4+1x2 |
## Examples
```jldoctest

Check failure on line 243 in src/io.jl

View workflow job for this annotation

GitHub Actions / Documentation

doctest failure in ~/work/TulipaEnergyModel.jl/TulipaEnergyModel.jl/src/io.jl:243-253 ```jldoctest _parse_time_intervals(Val(:uniform), "3", 1:12) # output 4-element Vector{UnitRange{Int64}}: 1:3 4:6 7:9 10:12 ``` Subexpression: _parse_time_intervals(Val(:uniform), "3", 1:12) Evaluated output: ERROR: UndefVarError: `_parse_time_intervals` not defined Stacktrace: [1] top-level scope @ none:1 Expected output: 4-element Vector{UnitRange{Int64}}: 1:3 4:6 7:9 10:12 diff = Warning: Diff output requires color. 4-element Vector{UnitRange{Int64}}: 1:3 4:6 7:9 10:12ERROR: UndefVarError: `_parse_time_intervals` not defined Stacktrace: [1] top-level scope @ none:1
_parse_time_intervals(Val(:uniform), "3", 1:12)
# output
4-element Vector{UnitRange{Int64}}:
1:3
4:6
7:9
10:12
```
```jldoctest

Check failure on line 255 in src/io.jl

View workflow job for this annotation

GitHub Actions / Documentation

doctest failure in ~/work/TulipaEnergyModel.jl/TulipaEnergyModel.jl/src/io.jl:255-264 ```jldoctest _parse_time_intervals(Val(:explicit), "4;4;4", 1:12) # output 3-element Vector{UnitRange{Int64}}: 1:4 5:8 9:12 ``` Subexpression: _parse_time_intervals(Val(:explicit), "4;4;4", 1:12) Evaluated output: ERROR: UndefVarError: `_parse_time_intervals` not defined Stacktrace: [1] top-level scope @ none:1 Expected output: 3-element Vector{UnitRange{Int64}}: 1:4 5:8 9:12 diff = Warning: Diff output requires color. 3-element Vector{UnitRange{Int64}}: 1:4 5:8 9:12ERROR: UndefVarError: `_parse_time_intervals` not defined Stacktrace: [1] top-level scope @ none:1
_parse_time_intervals(Val(:explicit), "4;4;4", 1:12)
# output
3-element Vector{UnitRange{Int64}}:
1:4
5:8
9:12
```
```jldoctest

Check failure on line 266 in src/io.jl

View workflow job for this annotation

GitHub Actions / Documentation

doctest failure in ~/work/TulipaEnergyModel.jl/TulipaEnergyModel.jl/src/io.jl:266-276 ```jldoctest _parse_time_intervals(Val(:math), "2x3+1x4+1x2", 1:12) # output 4-element Vector{UnitRange{Int64}}: 1:3 4:6 7:10 11:12 ``` Subexpression: _parse_time_intervals(Val(:math), "2x3+1x4+1x2", 1:12) Evaluated output: ERROR: UndefVarError: `_parse_time_intervals` not defined Stacktrace: [1] top-level scope @ none:1 Expected output: 4-element Vector{UnitRange{Int64}}: 1:3 4:6 7:10 11:12 diff = Warning: Diff output requires color. 4-element Vector{UnitRange{Int64}}: 1:3 4:6 7:10 11:12ERROR: UndefVarError: `_parse_time_intervals` not defined Stacktrace: [1] top-level scope @ none:1
_parse_time_intervals(Val(:math), "2x3+1x4+1x2", 1:12)
# output
4-element Vector{UnitRange{Int64}}:
1:3
4:6
7:10
11:12
```
"""
function _parse_time_intervals end

function _parse_time_intervals(::Val{:uniform}, time_step_string, rp_time_steps)
duration = parse(Int, time_step_string)
intervals = [i:i+duration-1 for i = 1:duration:length(rp_time_steps)]
@assert intervals[end][end] == length(rp_time_steps)
return intervals
end

function _parse_time_intervals(::Val{:explicit}, time_step_string, rp_time_steps)
intervals = UnitRange{Int}[]
interval_begin = 1
interval_lengths = parse.(Int, split(time_step_string, ";"))
for interval_length in interval_lengths
interval_end = interval_begin + interval_length - 1
push!(intervals, interval_begin:interval_end)
interval_begin = interval_end + 1
end
@assert interval_begin - 1 == length(rp_time_steps)
return intervals
end

function _parse_time_intervals(::Val{:math}, time_step_string, rp_time_steps)
intervals = UnitRange{Int}[]
interval_begin = 1
interval_instruction = split(time_step_string, "+")
for R in interval_instruction
num, len = parse.(Int, split(R, "x"))
for _ = 1:num
interval = (1:len) .+ (interval_begin - 1)
interval_begin += len
push!(intervals, interval)
end
end
@assert interval_begin - 1 == length(rp_time_steps)
intervals
end

"""
compute_time_intervals(df, elements, time_steps_per_rp)
For each element in `elements` (assets or flows), parse the time intervals defined in `df`,
a DataFrame of the file `assets-time-intervals.csv` or `flows-time-intervals.csv`.
`time_steps_per_rp` must be a dictionary indexed by `rp` and its values are the time steps of
that `rp`.
To obtain the time intervals, the columns `specification` and `time_intervals` from `df` are passed
to the function [`_parse_time_intervals`](@ref).
"""
function compute_time_intervals(df, elements, time_steps_per_rp)
time_resolution = Dict(
(element, rp) => begin
N = length(time_steps)
# Look for index in df that matches this element and rp
j = findfirst((df.id .== element_id) .& (df.rep_period_id .== rp))
if j === nothing
# If there is no time interval specification, use default of 1
[k:k for k = 1:N]
else
_parse_time_intervals(
Val(df[j, :specification]),
df[j, :time_intervals],
time_steps,
)
end
end for (element_id, element) in enumerate(elements),
(rp, time_steps) in time_steps_per_rp
)

return time_resolution
end
5 changes: 5 additions & 0 deletions test/inputs/Norse/assets-time-intervals.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
,,,
id,rep_period_id,specification,time_intervals
2,1,uniform,4
3,1,explicit,7;7;7;21;21;21;21;21;21;21
6,1,math,20x1+16x2+12x3+10x4+8x5
2 changes: 2 additions & 0 deletions test/inputs/Norse/flows-time-intervals.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
,,,
id,rep_period_id,specification,time_intervals
2 changes: 2 additions & 0 deletions test/inputs/Tiny/assets-time-intervals.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
,,,
id,rep_period_id,specification,time_intervals
2 changes: 2 additions & 0 deletions test/inputs/Tiny/flows-time-intervals.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
,,,
id,rep_period_id,specification,time_intervals
49 changes: 49 additions & 0 deletions test/test-io.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,52 @@ end
[Graphs.Edge(e) for e in [(1, 6), (2, 6), (3, 6), (4, 6), (5, 6)]]
end
end

@testset "Test parsing of time intervals" begin
@testset "compute_time_intervals manages all cases" begin
time_steps_per_rp = Dict(1 => 1:12, 2 => 1:24)
df = DataFrame(
:id => [1, 2, 2, 3],
:rep_period_id => [1, 1, 2, 2],
:specification => [:uniform, :explicit, :math, :math],
:time_intervals => ["3", "4;4;4", "3x4+4x3", "2x2+2x3+2x4+1x6"],
)
elements = [1, 2, 3] # Doesn't matter if it is assets or flows for test
time_intervals = compute_time_intervals(df, elements, time_steps_per_rp)
expected = Dict(
(1, 1) => [1:3, 4:6, 7:9, 10:12],
(2, 1) => [1:4, 5:8, 9:12],
(3, 1) => [i:i for i = 1:12],
(1, 2) => [i:i for i = 1:24],
(2, 2) => [1:4, 5:8, 9:12, 13:15, 16:18, 19:21, 22:24],
(3, 2) => [1:2, 3:4, 5:7, 8:10, 11:14, 15:18, 19:24],
)
for id = 1:3, rp = 1:2
@test time_intervals[(id, rp)] == expected[(id, rp)]
end
end

@testset "If the math doesn't match, raise exception" begin
TEM = TulipaEnergyModel
@test_throws AssertionError TEM._parse_time_intervals(Val(:uniform), "3", 1:13)
@test_throws AssertionError TEM._parse_time_intervals(Val(:uniform), "3", 1:14)
@test_throws AssertionError TEM._parse_time_intervals(
Val(:explicit),
"3;3;3;3",
1:11,
)
@test_throws AssertionError TEM._parse_time_intervals(
Val(:explicit),
"3;3;3;3",
1:13,
)
@test_throws AssertionError TEM._parse_time_intervals(
Val(:explicit),
"3;3;3;3",
1:14,
)
@test_throws AssertionError TEM._parse_time_intervals(Val(:math), "3x4", 1:11)
@test_throws AssertionError TEM._parse_time_intervals(Val(:math), "3x4", 1:13)
@test_throws AssertionError TEM._parse_time_intervals(Val(:math), "3x4", 1:14)
end
end

0 comments on commit 060c5b5

Please sign in to comment.