From 060c5b5fe342d95d68c753925d0c0b1a6740b122 Mon Sep 17 00:00:00 2001 From: Abel Soares Siqueira Date: Thu, 26 Oct 2023 14:48:27 +0200 Subject: [PATCH] Add files with time intervals of assets and flows and functions to parse it --- src/input-tables.jl | 7 + src/io.jl | 181 ++++++++++++++++++-- test/inputs/Norse/assets-time-intervals.csv | 5 + test/inputs/Norse/flows-time-intervals.csv | 2 + test/inputs/Tiny/assets-time-intervals.csv | 2 + test/inputs/Tiny/flows-time-intervals.csv | 2 + test/test-io.jl | 49 ++++++ 7 files changed, 232 insertions(+), 16 deletions(-) create mode 100644 test/inputs/Norse/assets-time-intervals.csv create mode 100644 test/inputs/Norse/flows-time-intervals.csv create mode 100644 test/inputs/Tiny/assets-time-intervals.csv create mode 100644 test/inputs/Tiny/flows-time-intervals.csv diff --git a/src/input-tables.jl b/src/input-tables.jl index 2c575bc3..69110b4e 100644 --- a/src/input-tables.jl +++ b/src/input-tables.jl @@ -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 diff --git a/src/io.jl b/src/io.jl index b216e0bc..ba35785c 100644 --- a/src/io.jl +++ b/src/io.jl @@ -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) @@ -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] @@ -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) @@ -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, @@ -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 @@ -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 +_parse_time_intervals(Val(:uniform), "3", 1:12) + +# output + +4-element Vector{UnitRange{Int64}}: + 1:3 + 4:6 + 7:9 + 10:12 +``` + +```jldoctest +_parse_time_intervals(Val(:explicit), "4;4;4", 1:12) + +# output + +3-element Vector{UnitRange{Int64}}: + 1:4 + 5:8 + 9:12 +``` + +```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 +``` +""" +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 diff --git a/test/inputs/Norse/assets-time-intervals.csv b/test/inputs/Norse/assets-time-intervals.csv new file mode 100644 index 00000000..f49b187b --- /dev/null +++ b/test/inputs/Norse/assets-time-intervals.csv @@ -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 diff --git a/test/inputs/Norse/flows-time-intervals.csv b/test/inputs/Norse/flows-time-intervals.csv new file mode 100644 index 00000000..b73ed779 --- /dev/null +++ b/test/inputs/Norse/flows-time-intervals.csv @@ -0,0 +1,2 @@ +,,, +id,rep_period_id,specification,time_intervals diff --git a/test/inputs/Tiny/assets-time-intervals.csv b/test/inputs/Tiny/assets-time-intervals.csv new file mode 100644 index 00000000..b73ed779 --- /dev/null +++ b/test/inputs/Tiny/assets-time-intervals.csv @@ -0,0 +1,2 @@ +,,, +id,rep_period_id,specification,time_intervals diff --git a/test/inputs/Tiny/flows-time-intervals.csv b/test/inputs/Tiny/flows-time-intervals.csv new file mode 100644 index 00000000..b73ed779 --- /dev/null +++ b/test/inputs/Tiny/flows-time-intervals.csv @@ -0,0 +1,2 @@ +,,, +id,rep_period_id,specification,time_intervals diff --git a/test/test-io.jl b/test/test-io.jl index 4cab0884..0f369e14 100644 --- a/test/test-io.jl +++ b/test/test-io.jl @@ -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