From ef6fd504eed95bd9aa4adfcef484e14cda741349 Mon Sep 17 00:00:00 2001 From: pesap Date: Mon, 26 Aug 2024 16:41:58 -0600 Subject: [PATCH 1/8] fix: Adding support for quadratic functions using the table data parsers Currently only works for heat rate and not cost curves. Issue #1177 --- src/descriptors/power_system_inputs.json | 15 ++++++++ src/parsers/power_system_table_data.jl | 48 +++++++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/descriptors/power_system_inputs.json b/src/descriptors/power_system_inputs.json index 7d114a1c4c..37d25fb7f1 100644 --- a/src/descriptors/power_system_inputs.json +++ b/src/descriptors/power_system_inputs.json @@ -366,6 +366,21 @@ "description": "Heat required to startup from cold", "default_value": null }, + { + "name": "heat_rate_a0", + "description": "Heat rate Average 0 TODO", + "default_value": null + }, + { + "name": "heat_rate_a1", + "description": "Heat rate Average 0 TODO", + "default_value": null + }, + { + "name": "heat_rate_a2", + "description": "Heat rate Average 0 TODO", + "default_value": null + }, { "name": "heat_rate_avg_0", "description": "Heat rate Average 0 TODO", diff --git a/src/parsers/power_system_table_data.jl b/src/parsers/power_system_table_data.jl index 7c9ea3f464..b245dcc3c7 100644 --- a/src/parsers/power_system_table_data.jl +++ b/src/parsers/power_system_table_data.jl @@ -545,6 +545,7 @@ function gen_csv_parser!(sys::System, data::PowerSystemTableData) throw(IS.ConflictingInputsError("Heat rate and cost points are both defined")) elseif length(heat_rate_fields) > 0 cost_colnames = _HeatRateColumns(zip(heat_rate_fields, output_point_fields)) + @warn cost_colnames elseif length(cost_point_fields) > 0 cost_colnames = _CostPointColumns(zip(cost_point_fields, output_point_fields)) else @@ -823,8 +824,17 @@ function make_cost( ) where {T <: ThermalGen} fuel_price = gen.fuel_price / 1000.0 - cost_pairs = get_cost_pairs(gen, cost_colnames) - var_cost, fixed = create_pwinc_cost(cost_pairs) + # We check if there is any Quadratic or Linear Data defined. If not we fall back to create PowerLoad + @warn typeof(gen) + quadratic_fields = (gen.heat_rate_a0, gen.heat_rate_a1, gen.heat_rate_a2) + + if any(field -> field != nothing, quadratic_fields) + var_cost, fixed = + create_poly_cost(gen, ["heat_rate_a0", "heat_rate_a1", "heat_rate_a2"]) + else + cost_pairs = get_cost_pairs(gen, cost_colnames) + var_cost, fixed = create_pwinc_cost(cost_pairs) + end startup_cost, shutdown_cost = calculate_uc_cost(data, gen, fuel_price) @@ -956,6 +966,40 @@ function create_pwl_cost( return var_cost end +function create_poly_cost( + gen, cost_colnames, +) + vals = [] + for coeff in cost_colnames + a = getfield(gen, Symbol(coeff)) + if a != nothing + push!(vals, tryparse(Float64, a)) + end + end + + # Three supported cases, + # 1. If three values are passed then we have data looking like: a^2 + a + c, + # 2. If two then data is looking like: a + c + # 3. If two then data is looking like: c + if length(vals) > 3 + throw( + DataFormatError( + "Higher order polynomial functions currently not supported.", + ), + ) + elseif length(vals) == 3 + var_cost = QuadraticCurve(vals[3], vals[2], vals[1]) + @debug "Quadratic curve created for $(gen.name)" + elseif length(vals) == 2 + var_cost = LinearCurve(vals[2], vals[1]) + else + length(vals) == 1 + var_cost = LinearCurve(vals[1]) + end + + return var_cost, 0.0 +end + function create_pwinc_cost( cost_pairs, ) From 8f9bed114b53be71f6e4b0e3af074e0b120d97a8 Mon Sep 17 00:00:00 2001 From: pesap Date: Mon, 26 Aug 2024 17:40:41 -0600 Subject: [PATCH 2/8] fixup! fix: Adding support for quadratic functions using the table data parsers --- src/parsers/power_system_table_data.jl | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/parsers/power_system_table_data.jl b/src/parsers/power_system_table_data.jl index b245dcc3c7..362c3455ac 100644 --- a/src/parsers/power_system_table_data.jl +++ b/src/parsers/power_system_table_data.jl @@ -545,7 +545,6 @@ function gen_csv_parser!(sys::System, data::PowerSystemTableData) throw(IS.ConflictingInputsError("Heat rate and cost points are both defined")) elseif length(heat_rate_fields) > 0 cost_colnames = _HeatRateColumns(zip(heat_rate_fields, output_point_fields)) - @warn cost_colnames elseif length(cost_point_fields) > 0 cost_colnames = _CostPointColumns(zip(cost_point_fields, output_point_fields)) else @@ -825,7 +824,6 @@ function make_cost( fuel_price = gen.fuel_price / 1000.0 # We check if there is any Quadratic or Linear Data defined. If not we fall back to create PowerLoad - @warn typeof(gen) quadratic_fields = (gen.heat_rate_a0, gen.heat_rate_a1, gen.heat_rate_a2) if any(field -> field != nothing, quadratic_fields) @@ -978,9 +976,9 @@ function create_poly_cost( end # Three supported cases, - # 1. If three values are passed then we have data looking like: a^2 + a + c, - # 2. If two then data is looking like: a + c - # 3. If two then data is looking like: c + # 1. If three values are passed then we have data looking like: a * x^2 + b * x + c, + # 2. If two then data is looking like: a * x + b + # 3. If two then data is looking like: a * x if length(vals) > 3 throw( DataFormatError( @@ -989,12 +987,13 @@ function create_poly_cost( ) elseif length(vals) == 3 var_cost = QuadraticCurve(vals[3], vals[2], vals[1]) - @debug "Quadratic curve created for $(gen.name)" + @debug "QuadraticCurve created for $(gen.name)" elseif length(vals) == 2 var_cost = LinearCurve(vals[2], vals[1]) + @debug "LinearCurve curve created for $(gen.name)" else - length(vals) == 1 var_cost = LinearCurve(vals[1]) + @debug "LinearCurve curve created for $(gen.name)" end return var_cost, 0.0 From f43769c3cbad10934ae0f9bb9f5bbc142eac7424 Mon Sep 17 00:00:00 2001 From: pesap Date: Mon, 26 Aug 2024 18:25:06 -0600 Subject: [PATCH 3/8] fix: Added better handling of the coefficients and docstring Co-authored-by: GabrielKS <23368820+GabrielKS@users.noreply.github.com> --- src/parsers/power_system_table_data.jl | 50 ++++++++++++++------------ 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/src/parsers/power_system_table_data.jl b/src/parsers/power_system_table_data.jl index 362c3455ac..baa3a15319 100644 --- a/src/parsers/power_system_table_data.jl +++ b/src/parsers/power_system_table_data.jl @@ -964,39 +964,43 @@ function create_pwl_cost( return var_cost end +""" + create_poly_cost(gen, cost_colnames) + +Return a Polynomial function cost based on the coeffiecients provided on gen. + +Three supported cases, + 1. If three values are passed then we have data looking like: `a2 * x^2 + a1 * x + a0`, + 2. If `a1` and `a0` are passed then we have data looking like: `a1 * x + a0`, + 3. If only `a1` is passed then we have data looking like: `a1 * x`. +""" function create_poly_cost( gen, cost_colnames, ) - vals = [] - for coeff in cost_colnames - a = getfield(gen, Symbol(coeff)) - if a != nothing - push!(vals, tryparse(Float64, a)) - end - end + fixed_cost = 0.0 + parse_maybe_nothing(x) = isnothing(x) ? nothing : tryparse(Float64, x) + a2 = parse_maybe_nothing(getfield(gen, Symbol("heat_rate_a2"))) + a1 = parse_maybe_nothing(getfield(gen, Symbol("heat_rate_a1"))) + a0 = parse_maybe_nothing(getfield(gen, Symbol("heat_rate_a0"))) - # Three supported cases, - # 1. If three values are passed then we have data looking like: a * x^2 + b * x + c, - # 2. If two then data is looking like: a * x + b - # 3. If two then data is looking like: a * x - if length(vals) > 3 + if !isnothing(a2) && (isnothing(a1) || isnothing(a0)) throw( DataFormatError( - "Higher order polynomial functions currently not supported.", + "All coefficients must be passed if quadratic term is passed.", ), ) - elseif length(vals) == 3 - var_cost = QuadraticCurve(vals[3], vals[2], vals[1]) - @debug "QuadraticCurve created for $(gen.name)" - elseif length(vals) == 2 - var_cost = LinearCurve(vals[2], vals[1]) - @debug "LinearCurve curve created for $(gen.name)" - else - var_cost = LinearCurve(vals[1]) - @debug "LinearCurve curve created for $(gen.name)" end - return var_cost, 0.0 + if !any(isnothing.([a2, a1, a0])) + @debug "QuadraticCurve created for $(gen.name)" + return QuadraticCurve(a2, a1, a0), fixed_cost + end + if all(isnothing.([a2, a0])) && !isnothing(a1) + @debug "LinearCurve created for $(gen.name)" + return LinearCurve(a1), fixed_cost + end + @debug "LinearCurve created for $(gen.name)" + return LinearCurve(a1, a0), fixed_cost end function create_pwinc_cost( From 08fbaf4b2ae1a2e6cecf1bf1d8ad8896a44608ca Mon Sep 17 00:00:00 2001 From: pesap Date: Mon, 26 Aug 2024 18:27:57 -0600 Subject: [PATCH 4/8] fix: Improved description of new fields. --- src/descriptors/power_system_inputs.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/descriptors/power_system_inputs.json b/src/descriptors/power_system_inputs.json index 37d25fb7f1..3a2ac9220e 100644 --- a/src/descriptors/power_system_inputs.json +++ b/src/descriptors/power_system_inputs.json @@ -368,17 +368,17 @@ }, { "name": "heat_rate_a0", - "description": "Heat rate Average 0 TODO", + "description": "Heat rate constant term", "default_value": null }, { "name": "heat_rate_a1", - "description": "Heat rate Average 0 TODO", + "description": "Heat rate proportional term", "default_value": null }, { "name": "heat_rate_a2", - "description": "Heat rate Average 0 TODO", + "description": "Heat rate quadratic term", "default_value": null }, { From 096af583183047c181642683a7038d1e449aa7d7 Mon Sep 17 00:00:00 2001 From: pesap Date: Mon, 26 Aug 2024 18:47:18 -0600 Subject: [PATCH 5/8] fix: Correct typo on comment --- src/parsers/power_system_table_data.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parsers/power_system_table_data.jl b/src/parsers/power_system_table_data.jl index baa3a15319..39b2340811 100644 --- a/src/parsers/power_system_table_data.jl +++ b/src/parsers/power_system_table_data.jl @@ -823,7 +823,7 @@ function make_cost( ) where {T <: ThermalGen} fuel_price = gen.fuel_price / 1000.0 - # We check if there is any Quadratic or Linear Data defined. If not we fall back to create PowerLoad + # We check if there is any Quadratic or Linear Data defined. If not we fall back to create PiecewiseIncrementalCurve quadratic_fields = (gen.heat_rate_a0, gen.heat_rate_a1, gen.heat_rate_a2) if any(field -> field != nothing, quadratic_fields) From 18ff011e122e4c2d05c739c32925e455ca331445 Mon Sep 17 00:00:00 2001 From: pesap Date: Tue, 27 Aug 2024 10:13:26 -0600 Subject: [PATCH 6/8] test: Adding testing for new function --- src/PowerSystems.jl | 3 ++ test/test_power_system_table_data.jl | 48 ++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/PowerSystems.jl b/src/PowerSystems.jl index 79097c2c34..66fa9f00ac 100644 --- a/src/PowerSystems.jl +++ b/src/PowerSystems.jl @@ -390,6 +390,9 @@ export get_compression_settings export CompressionSettings export CompressionTypes +# Parsing functions +export create_poly_cost + #export make_time_series export get_bus_numbers export get_name diff --git a/test/test_power_system_table_data.jl b/test/test_power_system_table_data.jl index fc668cf0c8..75c66a5fdb 100644 --- a/test/test_power_system_table_data.jl +++ b/test/test_power_system_table_data.jl @@ -181,3 +181,51 @@ end g = get_components(ThermalStandard, sys) @test get_variable.(get_operation_cost.(g)) == get_variable.(get_operation_cost.(g)) end + +@testset "Test create_poly_cost function" begin + + cost_colnames = ["heat_rate_a0", "heat_rate_a1", "heat_rate_a2"] + + # Coefficients for a CC using natura gas + a2 = -0.000531607 + a1 = 0.060554675 + a0 = 8.951100118 + + # First test that return quadratic if all coefficients are provided. + # We convert the coefficients to string to mimic parsing from csv + example_generator = (name="test-gen", heat_rate_a0=string(a0), heat_rate_a1=string(a1), heat_rate_a2=string(a2)) + cost_curve, fixed_cost = create_poly_cost(example_generator, cost_colnames) + @assert cost_curve isa QuadraticCurve + @assert isapprox(get_quadratic_term(cost_curve), a2, atol=0.01) + @assert isapprox(get_proportional_term(cost_curve), a1, atol=0.01) + @assert isapprox(get_constant_term(cost_curve), a0, atol=0.01) + + # Test return linear with both proportional and constant term + example_generator = (name="test-gen", heat_rate_a0=string(a0), heat_rate_a1=string(a1), heat_rate_a2=nothing) + cost_curve, fixed_cost = create_poly_cost(example_generator, cost_colnames) + @assert cost_curve isa LinearCurve + @assert isapprox(get_proportional_term(cost_curve), a1, atol=0.01) + @assert isapprox(get_constant_term(cost_curve), a0, atol=0.01) + + # Test return linear with just proportional term + example_generator = (name="test-gen", heat_rate_a0=nothing, heat_rate_a1=string(a1), heat_rate_a2=nothing) + cost_curve, fixed_cost = create_poly_cost(example_generator, cost_colnames) + @assert cost_curve isa LinearCurve + @assert isapprox(get_proportional_term(cost_curve), a1, atol=0.01) + + # Test raises error if a2 is passed but other coefficients are nothing + example_generator = (name="test-gen", heat_rate_a0=nothing, heat_rate_a1=nothing, heat_rate_a2=string(a2)) + @test_throws IS.DataFormatError create_poly_cost(example_generator, cost_colnames) + example_generator = (name="test-gen", heat_rate_a0=nothing, heat_rate_a1=string(a1), heat_rate_a2=string(a2)) + @test_throws IS.DataFormatError create_poly_cost(example_generator, cost_colnames) + example_generator = (name="test-gen", heat_rate_a0=string(a0), heat_rate_a1=nothing, heat_rate_a2=string(a2)) + @test_throws IS.DataFormatError create_poly_cost(example_generator, cost_colnames) + + # Test that it works with zero proportional and constant term + example_generator = (name="test-gen", heat_rate_a0=string(0.0), heat_rate_a1=string(0.0), heat_rate_a2=string(a2)) + cost_curve, fixed_cost = create_poly_cost(example_generator, cost_colnames) + @assert cost_curve isa QuadraticCurve + @assert isapprox(get_quadratic_term(cost_curve), a2, atol=0.01) + @assert isapprox(get_proportional_term(cost_curve), 0.0, atol=0.01) + @assert isapprox(get_constant_term(cost_curve), 0.0, atol=0.01) +end From 6988ef644d5c9ea3bfe7f079a3e0edd67738863b Mon Sep 17 00:00:00 2001 From: pesap Date: Tue, 27 Aug 2024 10:18:49 -0600 Subject: [PATCH 7/8] fixup! test: Adding testing for new function --- test/test_power_system_table_data.jl | 68 +++++++++++++++++++++------- 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/test/test_power_system_table_data.jl b/test/test_power_system_table_data.jl index 75c66a5fdb..dfda723e97 100644 --- a/test/test_power_system_table_data.jl +++ b/test/test_power_system_table_data.jl @@ -183,7 +183,6 @@ end end @testset "Test create_poly_cost function" begin - cost_colnames = ["heat_rate_a0", "heat_rate_a1", "heat_rate_a2"] # Coefficients for a CC using natura gas @@ -193,39 +192,74 @@ end # First test that return quadratic if all coefficients are provided. # We convert the coefficients to string to mimic parsing from csv - example_generator = (name="test-gen", heat_rate_a0=string(a0), heat_rate_a1=string(a1), heat_rate_a2=string(a2)) + example_generator = ( + name = "test-gen", + heat_rate_a0 = string(a0), + heat_rate_a1 = string(a1), + heat_rate_a2 = string(a2), + ) cost_curve, fixed_cost = create_poly_cost(example_generator, cost_colnames) @assert cost_curve isa QuadraticCurve - @assert isapprox(get_quadratic_term(cost_curve), a2, atol=0.01) - @assert isapprox(get_proportional_term(cost_curve), a1, atol=0.01) - @assert isapprox(get_constant_term(cost_curve), a0, atol=0.01) + @assert isapprox(get_quadratic_term(cost_curve), a2, atol = 0.01) + @assert isapprox(get_proportional_term(cost_curve), a1, atol = 0.01) + @assert isapprox(get_constant_term(cost_curve), a0, atol = 0.01) # Test return linear with both proportional and constant term - example_generator = (name="test-gen", heat_rate_a0=string(a0), heat_rate_a1=string(a1), heat_rate_a2=nothing) + example_generator = ( + name = "test-gen", + heat_rate_a0 = string(a0), + heat_rate_a1 = string(a1), + heat_rate_a2 = nothing, + ) cost_curve, fixed_cost = create_poly_cost(example_generator, cost_colnames) @assert cost_curve isa LinearCurve - @assert isapprox(get_proportional_term(cost_curve), a1, atol=0.01) - @assert isapprox(get_constant_term(cost_curve), a0, atol=0.01) + @assert isapprox(get_proportional_term(cost_curve), a1, atol = 0.01) + @assert isapprox(get_constant_term(cost_curve), a0, atol = 0.01) # Test return linear with just proportional term - example_generator = (name="test-gen", heat_rate_a0=nothing, heat_rate_a1=string(a1), heat_rate_a2=nothing) + example_generator = ( + name = "test-gen", + heat_rate_a0 = nothing, + heat_rate_a1 = string(a1), + heat_rate_a2 = nothing, + ) cost_curve, fixed_cost = create_poly_cost(example_generator, cost_colnames) @assert cost_curve isa LinearCurve - @assert isapprox(get_proportional_term(cost_curve), a1, atol=0.01) + @assert isapprox(get_proportional_term(cost_curve), a1, atol = 0.01) # Test raises error if a2 is passed but other coefficients are nothing - example_generator = (name="test-gen", heat_rate_a0=nothing, heat_rate_a1=nothing, heat_rate_a2=string(a2)) + example_generator = ( + name = "test-gen", + heat_rate_a0 = nothing, + heat_rate_a1 = nothing, + heat_rate_a2 = string(a2), + ) @test_throws IS.DataFormatError create_poly_cost(example_generator, cost_colnames) - example_generator = (name="test-gen", heat_rate_a0=nothing, heat_rate_a1=string(a1), heat_rate_a2=string(a2)) + example_generator = ( + name = "test-gen", + heat_rate_a0 = nothing, + heat_rate_a1 = string(a1), + heat_rate_a2 = string(a2), + ) @test_throws IS.DataFormatError create_poly_cost(example_generator, cost_colnames) - example_generator = (name="test-gen", heat_rate_a0=string(a0), heat_rate_a1=nothing, heat_rate_a2=string(a2)) + example_generator = ( + name = "test-gen", + heat_rate_a0 = string(a0), + heat_rate_a1 = nothing, + heat_rate_a2 = string(a2), + ) @test_throws IS.DataFormatError create_poly_cost(example_generator, cost_colnames) # Test that it works with zero proportional and constant term - example_generator = (name="test-gen", heat_rate_a0=string(0.0), heat_rate_a1=string(0.0), heat_rate_a2=string(a2)) + example_generator = ( + name = "test-gen", + heat_rate_a0 = string(0.0), + heat_rate_a1 = string(0.0), + heat_rate_a2 = string(a2), + ) cost_curve, fixed_cost = create_poly_cost(example_generator, cost_colnames) @assert cost_curve isa QuadraticCurve - @assert isapprox(get_quadratic_term(cost_curve), a2, atol=0.01) - @assert isapprox(get_proportional_term(cost_curve), 0.0, atol=0.01) - @assert isapprox(get_constant_term(cost_curve), 0.0, atol=0.01) + @assert isapprox(get_quadratic_term(cost_curve), a2, atol = 0.01) + @assert isapprox(get_proportional_term(cost_curve), 0.0, atol = 0.01) + @assert isapprox(get_constant_term(cost_curve), 0.0, atol = 0.01) end From 319f8a99a7e6f81e3a8951c7470b6b673417cbef Mon Sep 17 00:00:00 2001 From: pesap Date: Tue, 27 Aug 2024 11:14:18 -0600 Subject: [PATCH 8/8] fix: typo Co-authored-by: Gabriel Konar-Steenberg <23368820+GabrielKS@users.noreply.github.com> --- test/test_power_system_table_data.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_power_system_table_data.jl b/test/test_power_system_table_data.jl index dfda723e97..04b3fac1ff 100644 --- a/test/test_power_system_table_data.jl +++ b/test/test_power_system_table_data.jl @@ -185,7 +185,7 @@ end @testset "Test create_poly_cost function" begin cost_colnames = ["heat_rate_a0", "heat_rate_a1", "heat_rate_a2"] - # Coefficients for a CC using natura gas + # Coefficients for a CC using natural gas a2 = -0.000531607 a1 = 0.060554675 a0 = 8.951100118