From f36621d01d112639357e1ed6a30d3cbabb708a96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Thu, 28 May 2026 15:15:25 +0200 Subject: [PATCH 1/5] Introduces WindFarmParameters more in line with PVParameters and also made the corresponding nodes more consistent [skip ci] --- NEWS.md | 6 +- docs/src/library/public.md | 8 +- src/EnergyModelsLanguageInterfaces.jl | 4 +- src/datastructures.jl | 232 +++++++++++++++++++------- test/test_PV.jl | 7 +- test/test_checks.jl | 98 +++++------ test/test_windpower.jl | 60 +++++++ test/utils.jl | 40 ++--- 8 files changed, 317 insertions(+), 138 deletions(-) diff --git a/NEWS.md b/NEWS.md index f0cd9db..70d5add 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,10 @@ # Release notes -## Version 0.1.0 (2026-04-25) +## Version 0.1.0 (2026-05-28) + +### Added WindFarmParameters + +* Introduces `WindFarmParameters` more in line with `PVParameters` and also made the corresponding nodes more consistent. ### Add Building node diff --git a/docs/src/library/public.md b/docs/src/library/public.md index cf233c1..206423d 100644 --- a/docs/src/library/public.md +++ b/docs/src/library/public.md @@ -10,6 +10,7 @@ EMLI.ResourceBio ```@docs EMLI.PVParameters +EMLI.WindFarmParameters ``` ## [New nodal types](@id lib-pub-nodal_types) @@ -29,12 +30,12 @@ EMLI.BioCHP EMLI.WindPower( ::Any, ::TimeStruct.TimeProfile, - ::Dict, - ::String, - ::String, ::TimeStruct.TimeProfile, ::TimeStruct.TimeProfile, ::Dict{<:EnergyModelsBase.Resource,<:Real}, + ::WindFarmParameters, + ::DateTime, + ::DateTime, ) EMLI.PV( ::Any, @@ -108,4 +109,5 @@ EMLI.BioCHP( ```@docs EMLI.call_python_function EMLI.fetch_element +EMLI.to_dict ``` diff --git a/src/EnergyModelsLanguageInterfaces.jl b/src/EnergyModelsLanguageInterfaces.jl index eded97f..15d22d1 100644 --- a/src/EnergyModelsLanguageInterfaces.jl +++ b/src/EnergyModelsLanguageInterfaces.jl @@ -34,9 +34,9 @@ include("constraint_functions.jl") include("utils.jl") export call_python_function, fetch_element -export WindPower, CSPandPV, MultipleBuildingTypes +export PVParameters, WindFarmParameters +export PV, WindPower, CSPandPV, MultipleBuildingTypes export ResourceBio, BioCHP -export PV, PVParameters export Building end # module EnergyModelsLanguageInterfaces diff --git a/src/datastructures.jl b/src/datastructures.jl index 5b34432..44bf690 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -10,18 +10,18 @@ abstract type AbstractParameters end A structure to hold parameters for photovoltaic (PV) power generation. # Fields -- **`lat::Real`**: Latitude of the location in decimal degrees (e.g., 52.0 for 52°N). -- **`lon::Real`**: Longitude of the location in decimal degrees (e.g., 13.0 for 13°E). -- **`loss::Real=14.0`**: Total system losses in percentage (e.g., 14.0 for 14% losses). -- **`pvtechchoice::String="crystSi"`**: Type of PV technology. Options include: +- **`lat::Real`** is the latitude of the location in decimal degrees (e.g., 52.0 for 52°N). +- **`lon::Real`** is the longitude of the location in decimal degrees (e.g., 13.0 for 13°E). +- **`loss::Real=14.0`** is the total system losses in percentage (e.g., 14.0 for 14% losses). +- **`pvtechchoice::String="crystSi"`** is the type of PV technology. Options include: - `"crystSi"`: Crystalline silicon (default). - `"CIS"`: Copper indium selenide. - `"CdTe"`: Cadmium telluride. -- **`mountingplace::String="free"`**: Mounting type of the PV system. Options include: +- **`mountingplace::String="free"`** is the mounting type of the PV system. Options include: - `"free"`: Free-standing system (default). - `"building"`: Building-integrated system. -- **`optimalangles::Bool=true`**: Whether to use optimal tilt and azimuth angles for the PV system. -- **`usehorizon::Bool=true`**: Whether to include the effect of the horizon in the calculations. +- **`optimalangles::Bool=true`** is a flag for whether to use optimal tilt and azimuth angles for the PV system. +- **`usehorizon::Bool=true`** is a flag for whether to include the effect of the horizon in the calculations. """ struct PVParameters <: AbstractParameters lat::Real @@ -41,6 +41,12 @@ struct PVParameters <: AbstractParameters usehorizon::Bool, ) errors = String[] + if lat < -90 || lat > 90 + push!(errors, "lat must be in [-90, 90].") + end + if lon < -180 || lon > 180 + push!(errors, "lon must be in [-180, 180].") + end if loss < 0 push!(errors, "Loss must be non-negative.") end @@ -79,6 +85,128 @@ function PVParameters( optimalangles, usehorizon) end +""" + WindFarmParameters + WindFarmParameters( + id::String, + lat::Real, + lon::Real, + turbine_height::Real; + orientation = missing, + shape = missing, + method::String = "Ninja", + source::String = "NORA3", + ) + +A structure to hold wind farm parameters and metadata for wind power time series generation. + +# Fields +- **`id`**: Identifier for the wind farm. +- **`lat`**: Latitude of the wind farm. +- **`lon`**: Longitude of the wind farm. +- **`turbine_height`**: Height of the wind turbines in meters. +- **`orientation`**: Orientation of the wind farm (default: `missing`). +- **`shape`**: Shape of the wind farm (default: `missing`). +- **`method`** is the chosen method for data retrieval. The user can choose between the + strings "Ninja", "Tradewind_offshore", "Tradewind_upland", and "Tradewind_lowland". + The default value is "Ninja". +- **`source`** is the data source for wind data. The user can choose between the strings + "NORA3" and "ERA5". The default value is "NORA3". +""" +struct WindFarmParameters <: AbstractParameters + id::String + lat::Real + lon::Real + turbine_height::Real + orientation::Any + shape::Any + method::String + source::String + function WindFarmParameters( + id::String, + lat::Real, + lon::Real, + turbine_height::Real, + orientation::Any, + shape::Any, + method::String, + source::String, + ) + errors = String[] + if lat < -90 || lat > 90 + push!(errors, "lat must be in [-90, 90].") + end + if lon < -180 || lon > 180 + push!(errors, "lon must be in [-180, 180].") + end + if turbine_height <= 0 + push!(errors, "turbine_height must be positive.") + end + + methods = ("Ninja", "Tradewind_offshore", "Tradewind_upland", "Tradewind_lowland") + if !(method in methods) + push!(errors, "method must be one of $(methods).") + end + + sources = ("NORA3", "ERA5") + if !(source in sources) + push!(errors, "source must be one of $(sources).") + end + + if !isempty(errors) + throw(ArgumentError(join(errors, " "))) + end + + return new( + id, + lat, + lon, + turbine_height, + orientation, + shape, + method, + source, + ) + end +end +function WindFarmParameters( + id::String, + lat::Real, + lon::Real, + turbine_height::Real; + orientation = missing, + shape = missing, + method::String = "Ninja", + source::String = "NORA3", +) + return WindFarmParameters( + id, + lat, + lon, + turbine_height, + orientation, + shape, + method, + source, + ) +end + +""" + to_dict(params::WindFarmParameters) + +Convert a `WindFarmParameters` instance to a dictionary for use in Python calls. +""" +function to_dict(params::WindFarmParameters) + return Dict( + "id" => params.id, + "lat" => params.lat, + "lon" => params.lon, + "turbine_height" => params.turbine_height, + "orientation" => params.orientation, + "shape" => params.shape, + ) +end + """ WindPower <: AbstractNonDisRES @@ -120,16 +248,14 @@ end WindPower( id::Any, cap::TimeProfile, - windfarm::Dict, - time_start::String, - time_end::String, opex_var::TimeProfile, opex_fixed::TimeProfile, - output::Dict{<:Resource,<:Real}; + output::Dict{<:Resource,<:Real}, + wind_params::WindFarmParameters, + time_start::DateTime, + time_end::DateTime; data::Vector{<:ExtensionData} = ExtensionData[], - method::String = "Ninja", data_path::String = "", - source::String = "NORA3", ) Constructs a [`WindPower`](@ref) instance where the power production profile is sampled from @@ -137,37 +263,18 @@ a Python function. # Arguments - **`id`** is the name or identifier of the node. -- **`cap`** is the installed capacity. -- **`windfarm`** is a dictionary containing the wind farm parameters. An example dictionary - is given by: - - ```julia - windfarm = Dict( - "id" => "windfarm_1", # The identifier of the windfarm - "lat" => 56.8233, # The latitude coordinates of the windfarm - "lon" => 4.3467, # The longitude of the wind farm - "orientation" => missing, # The orientation - "shape" => missing, - "turbine_height" => 150, # The turbine height - ) - ``` -- **`time_start`** is the starting time (as a string) for the wind power time series sampling. - The format is "YYYY-MM-DD". -- **`time_end`** is the end time (as a string) for the wind power time series sampling. - The format is "YYYY-MM-DD". -- **`opex_var`** is the variable operating expense per energy unit produced. -- **`opex_fixed`** is the fixed operating expense. -- **`output`** are the generated `Resource`s, normally Power, with conversion value `Real`. +- **`cap::TimeProfile`** is the installed capacity. +- **`opex_var::TimeProfile`** is the variable operating expense per energy unit produced. +- **`opex_fixed::TimeProfile`** is the fixed operating expense. +- **`output::Dict{<:Resource,<:Real}`** are the generated `Resource`s, normally Power, with conversion value `Real`. +- **`wind_params::WindFarmParameters`** are the parameters for the wind farm. See [`WindFarmParameters`](@ref) for details. +- **`time_start::DateTime`** is the starting time for the wind power time series sampling. +- **`time_end::DateTime`** is the end time for the wind power time series sampling. # Keyword arguments - **`data`** is the additional data (*e.g.*, for investments). The default value is no `data`. -- **`method`** is the chosen method for data retrieval. The user can choose between the - strings "Ninja", "Tradewind_offshore", "Tradewind_upland", and "Tradewind_lowland". - The default value is "Ninja". - **`data_path`** is an optional file path for already downloaded data. The default value is an empty datapath. -- **`source`** is the data source for wind data. The user can choose between the strings - "NORA3" and "ERA5". The default value is "NORA3". !!! note "Usage of the ERA5 data source in wind_power_timeseries" For use of the "ERA5" data source, the user needs to register and obtain a CDS API key. @@ -176,26 +283,24 @@ a Python function. function WindPower( id::Any, cap::TimeProfile, - windfarm::Dict, - time_start::String, - time_end::String, opex_var::TimeProfile, opex_fixed::TimeProfile, - output::Dict{<:Resource,<:Real}; + output::Dict{<:Resource,<:Real}, + wind_params::WindFarmParameters, + time_start::DateTime, + time_end::DateTime; data::Vector{<:ExtensionData} = ExtensionData[], - method::String = "Ninja", data_path::String = "", - source::String = "NORA3", ) power = call_python_function( "wind_power_timeseries", "sample.wind_power"; - windfarm = windfarm, - time_start = time_start, - time_end = time_end, - method = method, + windfarm = to_dict(wind_params), + time_start = Dates.format(time_start, "yyyy-mm-dd"), + time_end = Dates.format(time_end, "yyyy-mm-dd"), + method = wind_params.method, data_path = data_path, - source = source, + source = wind_params.source, ) profile = OperationalProfile(power) @@ -256,22 +361,23 @@ end ) Constructs a [`PV`](@ref) instance where the power production profile is sampled from -the PVGIS API. +the PVGIS API tool from the EU Science Hub (available at https://re.jrc.ec.europa.eu/pvg_tools). -# Arguments -- **`id`**: The name or identifier of the node. -- **`cap`**: The installed capacity. -- **`opex_var`**: The variable operating expense per energy unit produced. -- **`opex_fixed`**: The fixed operating expense. -- **`output`**: The generated `Resource`s, normally Power, with conversion value `Real`. -- **`time_start::DateTime`**: The start of the time range for which the PV output data is requested. -- **`time_end::DateTime`**: The end of the time range for which the PV output data is requested. -- **`params::PVParameters`**: Parameters for the PV system. See [`PVParameters`](@ref) for details. +# Fields +- **`id`** is the name/identifier of the node. +- **`cap::TimeProfile`** is the installed capacity. +- **`opex_var::TimeProfile`** is the variable operating expense per energy unit produced. +- **`opex_fixed::TimeProfile`** is the fixed operating expense. +- **`output::Dict{<:Resource,<:Real}`** are the generated `Resource`s, normally Power. +- **`time_start::DateTime`** is the start of the time range for which the PV output data is requested. +- **`time_end::DateTime`** is the end of the time range for which the PV output data is requested. +- **`params::PVParameters`** are the parameters for the PV system. See [`PVParameters`](@ref) for details. # Keyword arguments -- **`data`**: Additional data (e.g., for investments). Default is no `data`. -- **`data_path`**: Directory where the cached CSV file will be stored. Default is `"pvgis_cache"`. -- **`filename_hint`**: Optional string to include in the cache file name for identification. Default is `""`. +- **`data::Vector{<:ExtensionData}`** is the additional data (e.g., for investments). + The field `data` is conditional through usage of a constructor. +- **`data_path::String`** is the directory where the cached CSV file will be stored. Default is `"pvgis_cache"`. +- **`filename_hint::String`** is an optional string to include in the cache file name for identification. Default is `""`. """ function PV( id::Any, diff --git a/test/test_PV.jl b/test/test_PV.jl index 1d44bc3..648830e 100644 --- a/test/test_PV.jl +++ b/test/test_PV.jl @@ -1,5 +1,4 @@ @testset "PVParameters" begin - # Test the constructor and field access @testset "Constructor" begin params1 = PVParameters( 52.0, @@ -23,6 +22,12 @@ end @testset "Invalid parameters" begin + # Test that invalid lat/lon throws an error + @test_throws ArgumentError PVParameters(-91.0, 5.0) + @test_throws ArgumentError PVParameters(91.0, 5.0) + @test_throws ArgumentError PVParameters(52.0, -181.0) + @test_throws ArgumentError PVParameters(52.0, 181.0) + # Test that invalid loss throws an error @test_throws ArgumentError PVParameters(52.0, 5.0; loss = -1.0) diff --git a/test/test_checks.jl b/test/test_checks.jl index 01c908f..3eb8368 100644 --- a/test/test_checks.jl +++ b/test/test_checks.jl @@ -35,80 +35,80 @@ end @testset "Test checks - MultipleBuildingTypes" begin # Test missing resource in cap @test_throws AssertionError simple_graph_buildings(; - cap_p = Dict(HeatHT=>FixedProfile(10.0)), - input = Dict(HeatHT=>1.0, Power=>2.0), + cap_p = Dict(HeatHT => FixedProfile(10.0)), + input = Dict(HeatHT => 1.0, Power => 2.0), ) # Test missing resource in penalty_surplus @test_throws AssertionError simple_graph_buildings(; - cap_p = Dict(HeatHT=>FixedProfile(10.0), Power=>FixedProfile(5.0)), - penalty_surplus = Dict(HeatHT=>FixedProfile(0.5)), - input = Dict(HeatHT=>1.0, Power=>2.0), + cap_p = Dict(HeatHT => FixedProfile(10.0), Power => FixedProfile(5.0)), + penalty_surplus = Dict(HeatHT => FixedProfile(0.5)), + input = Dict(HeatHT => 1.0, Power => 2.0), ) # Test missing resource in penalty_deficit @test_throws AssertionError simple_graph_buildings(; - cap_p = Dict(HeatHT=>FixedProfile(10.0), Power=>FixedProfile(5.0)), - penalty_deficit = Dict(Power=>FixedProfile(0.5)), - input = Dict(HeatHT=>1.0, Power=>2.0), + cap_p = Dict(HeatHT => FixedProfile(10.0), Power => FixedProfile(5.0)), + penalty_deficit = Dict(Power => FixedProfile(0.5)), + input = Dict(HeatHT => 1.0, Power => 2.0), ) # Test negative capacity @test_throws AssertionError simple_graph_buildings(; - cap_p = Dict(HeatHT=>FixedProfile(-10.0), Power=>FixedProfile(5.0)), + cap_p = Dict(HeatHT => FixedProfile(-10.0), Power => FixedProfile(5.0)), ) # Test negative input value @test_throws AssertionError simple_graph_buildings(; - cap_p = Dict(HeatHT=>FixedProfile(10.0), Power=>FixedProfile(5.0)), - input = Dict(HeatHT=>-1.0, Power=>2.0), + cap_p = Dict(HeatHT => FixedProfile(10.0), Power => FixedProfile(5.0)), + input = Dict(HeatHT => -1.0, Power => 2.0), ) # Test infeasible penalty combination (sum negative) @test_throws AssertionError simple_graph_buildings(; - cap_p = Dict(HeatHT=>FixedProfile(10.0), Power=>FixedProfile(5.0)), - penalty_surplus = Dict(HeatHT=>FixedProfile(-2.0), Power=>FixedProfile(0.5)), - penalty_deficit = Dict(HeatHT=>FixedProfile(-1.0), Power=>FixedProfile(0.5)), + cap_p = Dict(HeatHT => FixedProfile(10.0), Power => FixedProfile(5.0)), + penalty_surplus = Dict(HeatHT => FixedProfile(-2.0), Power => FixedProfile(0.5)), + penalty_deficit = Dict(HeatHT => FixedProfile(-1.0), Power => FixedProfile(0.5)), ) end @testset "Test checks - Building" begin # Test missing resource in cap @test_throws AssertionError simple_graph_building(; - cap_p = Dict(HeatHT=>FixedProfile(10.0)), - input = Dict(HeatHT=>1.0, Power=>2.0), + cap_p = Dict(HeatHT => FixedProfile(10.0)), + input = Dict(HeatHT => 1.0, Power => 2.0), ) # Test missing resource in penalty_surplus @test_throws AssertionError simple_graph_building(; - cap_p = Dict(HeatHT=>FixedProfile(10.0), Power=>FixedProfile(5.0)), - penalty_surplus = Dict(HeatHT=>FixedProfile(0.5)), - input = Dict(HeatHT=>1.0, Power=>2.0), + cap_p = Dict(HeatHT => FixedProfile(10.0), Power => FixedProfile(5.0)), + penalty_surplus = Dict(HeatHT => FixedProfile(0.5)), + input = Dict(HeatHT => 1.0, Power => 2.0), ) # Test missing resource in penalty_deficit @test_throws AssertionError simple_graph_building(; - cap_p = Dict(HeatHT=>FixedProfile(10.0), Power=>FixedProfile(5.0)), - penalty_deficit = Dict(Power=>FixedProfile(0.5)), - input = Dict(HeatHT=>1.0, Power=>2.0), + cap_p = Dict(HeatHT => FixedProfile(10.0), Power => FixedProfile(5.0)), + penalty_deficit = Dict(Power => FixedProfile(0.5)), + input = Dict(HeatHT => 1.0, Power => 2.0), ) # Test negative capacity @test_throws AssertionError simple_graph_building(; - cap_p = Dict(HeatHT=>FixedProfile(-10.0), Power=>FixedProfile(5.0)), + cap_p = Dict(HeatHT => FixedProfile(-10.0), Power => FixedProfile(5.0)), ) # Test negative input value @test_throws AssertionError simple_graph_building(; - cap_p = Dict(HeatHT=>FixedProfile(10.0), Power=>FixedProfile(5.0)), - input = Dict(HeatHT=>-1.0, Power=>2.0), + cap_p = Dict(HeatHT => FixedProfile(10.0), Power => FixedProfile(5.0)), + input = Dict(HeatHT => -1.0, Power => 2.0), ) # Test infeasible penalty combination (sum negative) @test_throws AssertionError simple_graph_building(; - cap_p = Dict(HeatHT=>FixedProfile(10.0), Power=>FixedProfile(5.0)), - penalty_surplus = Dict(HeatHT=>FixedProfile(-2.0), Power=>FixedProfile(0.5)), - penalty_deficit = Dict(HeatHT=>FixedProfile(-1.0), Power=>FixedProfile(0.5)), + cap_p = Dict(HeatHT => FixedProfile(10.0), Power => FixedProfile(5.0)), + penalty_surplus = Dict(HeatHT => FixedProfile(-2.0), Power => FixedProfile(0.5)), + penalty_deficit = Dict(HeatHT => FixedProfile(-1.0), Power => FixedProfile(0.5)), ) # Test that a unsupported source is caught by the checks @@ -118,65 +118,67 @@ end @testset "Test checks - CSPandPV" begin # Test missing resource in cap @test_throws AssertionError simple_graph_csp_pv(; - cap_p = Dict(Power=>FixedProfile(10.0)), + cap_p = Dict(Power => FixedProfile(10.0)), ) # Test missing resource in opex_fixed @test_throws AssertionError simple_graph_csp_pv(; - cap_p = Dict(Power=>FixedProfile(10.0), CSPHeat=>FixedProfile(5.0)), - opex_fixed_p = Dict(Power=>FixedProfile(5.0)), + cap_p = Dict(Power => FixedProfile(10.0), CSPHeat => FixedProfile(5.0)), + opex_fixed_p = Dict(Power => FixedProfile(5.0)), ) # Test missing resource in opex_var @test_throws AssertionError simple_graph_csp_pv(; - cap_p = Dict(Power=>FixedProfile(10.0), CSPHeat=>FixedProfile(5.0)), - opex_var_p = Dict(Power=>FixedProfile(0.1)), + cap_p = Dict(Power => FixedProfile(10.0), CSPHeat => FixedProfile(5.0)), + opex_var_p = Dict(Power => FixedProfile(0.1)), ) # Test missing resource in profile @test_throws AssertionError simple_graph_csp_pv(; - cap_p = Dict(Power=>FixedProfile(10.0), CSPHeat=>FixedProfile(5.0)), - profile = Dict(Power=>FixedProfile(0.8)), + cap_p = Dict(Power => FixedProfile(10.0), CSPHeat => FixedProfile(5.0)), + profile = Dict(Power => FixedProfile(0.8)), ) # Test that a wrong capacity is caught by the checks @test_throws AssertionError simple_graph_csp_pv(; - cap_p = Dict(Power=>FixedProfile(-10.0), CSPHeat=>FixedProfile(5.0)), + cap_p = Dict(Power => FixedProfile(-10.0), CSPHeat => FixedProfile(5.0)), ) # Test negative opex_fixed value @test_throws AssertionError simple_graph_csp_pv(; - cap_p = Dict(Power=>FixedProfile(10.0), CSPHeat=>FixedProfile(5.0)), - opex_fixed_p = Dict(Power=>FixedProfile(-5.0), CSPHeat=>FixedProfile(2.0)), + cap_p = Dict(Power => FixedProfile(10.0), CSPHeat => FixedProfile(5.0)), + opex_fixed_p = Dict(Power => FixedProfile(-5.0), CSPHeat => FixedProfile(2.0)), ) # Test that a wrong profile is caught by the checks @test_throws AssertionError simple_graph_csp_pv(; - cap_p = Dict(Power=>FixedProfile(10.0), CSPHeat=>FixedProfile(5.0)), - profile = Dict(Power=>FixedProfile(-0.2), CSPHeat=>FixedProfile(0.7)), + cap_p = Dict(Power => FixedProfile(10.0), CSPHeat => FixedProfile(5.0)), + profile = Dict(Power => FixedProfile(-0.2), CSPHeat => FixedProfile(0.7)), ) @test_throws AssertionError simple_graph_csp_pv(; - cap_p = Dict(Power=>FixedProfile(10.0), CSPHeat=>FixedProfile(5.0)), - profile = Dict(Power=>FixedProfile(1.2), CSPHeat=>FixedProfile(0.7)), + cap_p = Dict(Power => FixedProfile(10.0), CSPHeat => FixedProfile(5.0)), + profile = Dict(Power => FixedProfile(1.2), CSPHeat => FixedProfile(0.7)), ) # Test that a wrong fixed OPEX is caught by the checks @test_throws AssertionError simple_graph_csp_pv(; - cap_p = Dict(Power=>FixedProfile(10.0), CSPHeat=>FixedProfile(5.0)), + cap_p = Dict(Power => FixedProfile(10.0), CSPHeat => FixedProfile(5.0)), opex_fixed_p = Dict( - Power=>FixedProfile(5.0), - CSPHeat=>FixedProfile(-5), + Power => FixedProfile(5.0), + CSPHeat => FixedProfile(-5), ), ) # Test that a wrong output dictionary is caught @test_throws AssertionError simple_graph_csp_pv(; - cap_p = Dict(Power=>FixedProfile(10.0), CSPHeat=>FixedProfile(5.0)), - output = Dict(CSPHeat=>-1.0, Power=>1.0), + cap_p = Dict(Power => FixedProfile(10.0), CSPHeat => FixedProfile(5.0)), + output = Dict(CSPHeat => -1.0, Power => 1.0), ) end @testset "Test checks - BioCHP" begin # Test that missing electricity_resource in outputs is caught - @test_throws AssertionError simple_graph_biochp(; output = Dict(Heat1=>1.0, Heat2=>1.0)) + @test_throws AssertionError simple_graph_biochp(; + output = Dict(Heat1 => 1.0, Heat2 => 1.0), + ) end diff --git a/test/test_windpower.jl b/test/test_windpower.jl index 1c01012..d21625b 100644 --- a/test/test_windpower.jl +++ b/test/test_windpower.jl @@ -1,3 +1,63 @@ +@testset "WindFarmParameters" begin + @testset "Constructor" begin + params1 = WindFarmParameters( + "wf1", + 52.0, + 5.0, + 100.0; + orientation = 180, + shape = "circular", + method = "Ninja", + source = "NORA3", + ) + + params2 = WindFarmParameters( + "wf1", + 52.0, + 5.0, + 100.0, + 180, + "circular", + "Ninja", + "NORA3", + ) + + for params ∈ (params1, params2) + @test params.id == "wf1" + @test params.lat == 52.0 + @test params.lon == 5.0 + @test params.turbine_height == 100.0 + @test params.orientation == 180 + @test params.shape == "circular" + @test params.method == "Ninja" + @test params.source == "NORA3" + end + end + + @testset "Invalid parameters" begin + # lat/lon validation + @test_throws ArgumentError WindFarmParameters("wf1", -91.0, 5.0, 100.0) + @test_throws ArgumentError WindFarmParameters("wf1", 91.0, 5.0, 100.0) + @test_throws ArgumentError WindFarmParameters("wf1", 52.0, -181.0, 100.0) + @test_throws ArgumentError WindFarmParameters("wf1", 52.0, 181.0, 100.0) + + # turbine height + @test_throws ArgumentError WindFarmParameters("wf1", 52.0, 5.0, 0.0) + @test_throws ArgumentError WindFarmParameters("wf1", 52.0, 5.0, -10.0) + + # invalid method + @test_throws ArgumentError WindFarmParameters( + "wf1", 52.0, 5.0, 100.0; + method = "invalid", + ) + + # invalid source + @test_throws ArgumentError WindFarmParameters( + "wf1", 52.0, 5.0, 100.0; + source = "invalid", + ) + end +end @testset "WindPower" begin case, modeltype = simple_graph_wind() diff --git a/test/utils.jl b/test/utils.jl index 113b679..b6e9a20 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -72,27 +72,27 @@ function simple_graph_wind(; profile = nothing, ) # Creation of the initial problem with the NonDisRES node - time_start = "2022-05-01" - time_end = "2022-05-03" - windfarm = Dict( - "id" => "windfarm_1", - "lat" => 55, - "lon" => 9, - "orientation" => missing, - "shape" => missing, - "turbine_height" => 150, + time_start = DateTime("2022-05-01T00:00:00") + time_end = DateTime("2022-05-03T23:00:00") + wind_params = WindFarmParameters( + "windfarm_1", + 55, + 9, + 150; + orientation = missing, + shape = missing, ) data_path = mkpath(joinpath(testdir, "data", "WindPower")) if isnothing(profile) wind = WindPower( "Windfarm", # Node id cap, # Capacity in MW - windfarm, # Windfarm data - time_start, # Start time for the data - time_end, # End time for the data opex_var, # Variable operational cost in €/MWh opex_fixed, # Fixed operational cost in €/MW/year - output; # The generated resources with conversion value 1 + output, # The generated resources with conversion value 1 + wind_params, # Windfarm data + time_start, # Start time for the data + time_end; # End time for the data data_path, # Path to the data ) else @@ -219,9 +219,9 @@ function simple_graph_buildings(; cap_p = nothing, end function simple_graph_building(; cap_p = nothing, - penalty_surplus = Dict(HeatHT=>FixedProfile(100), Power=>FixedProfile(100)), - penalty_deficit = Dict(HeatHT=>FixedProfile(1e4), Power=>FixedProfile(1e4)), - input = Dict(HeatHT=>1.0, Power=>1.0), source = "NORA3") + penalty_surplus = Dict(HeatHT => FixedProfile(100), Power => FixedProfile(100)), + penalty_deficit = Dict(HeatHT => FixedProfile(1e4), Power => FixedProfile(1e4)), + input = Dict(HeatHT => 1.0, Power => 1.0), source = "NORA3") # Creation of the initial problem with the NonDisRES node time_start_str = "2019-01-01" time_end_str = "2019-01-01" @@ -292,10 +292,10 @@ function simple_graph_building(; cap_p = nothing, end function simple_graph_csp_pv(; cap_p = nothing, - profile = Dict(Power=>FixedProfile(0.8), CSPHeat=>FixedProfile(0.7)), - opex_var_p = Dict(Power=>FixedProfile(0.1), CSPHeat=>FixedProfile(0.2)), - opex_fixed_p = Dict(Power=>FixedProfile(5.0), CSPHeat=>FixedProfile(2.0)), - output = Dict(CSPHeat=>1.0, Power=>1.0), + profile = Dict(Power => FixedProfile(0.8), CSPHeat => FixedProfile(0.7)), + opex_var_p = Dict(Power => FixedProfile(0.1), CSPHeat => FixedProfile(0.2)), + opex_fixed_p = Dict(Power => FixedProfile(5.0), CSPHeat => FixedProfile(2.0)), + output = Dict(CSPHeat => 1.0, Power => 1.0), ) # Creation of the initial problem with the NonDisRES node time_start_str = "2019-01-01" From 1be73e6fccb73bb952449d6e1a80d6e8c6e04696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Fri, 29 May 2026 10:53:42 +0200 Subject: [PATCH 2/5] Apply suggestions from code review [skip ci] Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/datastructures.jl | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/datastructures.jl b/src/datastructures.jl index 44bf690..cae69c2 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -41,10 +41,14 @@ struct PVParameters <: AbstractParameters usehorizon::Bool, ) errors = String[] - if lat < -90 || lat > 90 + if !isfinite(lat) + push!(errors, "lat must be finite.") + elseif lat < -90 || lat > 90 push!(errors, "lat must be in [-90, 90].") end - if lon < -180 || lon > 180 + if !isfinite(lon) + push!(errors, "lon must be finite.") + elseif lon < -180 || lon > 180 push!(errors, "lon must be in [-180, 180].") end if loss < 0 @@ -133,14 +137,14 @@ struct WindFarmParameters <: AbstractParameters source::String, ) errors = String[] - if lat < -90 || lat > 90 - push!(errors, "lat must be in [-90, 90].") + if !isfinite(lat) || lat < -90 || lat > 90 + push!(errors, "lat must be finite and in [-90, 90].") end - if lon < -180 || lon > 180 - push!(errors, "lon must be in [-180, 180].") + if !isfinite(lon) || lon < -180 || lon > 180 + push!(errors, "lon must be finite and in [-180, 180].") end - if turbine_height <= 0 - push!(errors, "turbine_height must be positive.") + if !isfinite(turbine_height) || turbine_height <= 0 + push!(errors, "turbine_height must be finite and positive.") end methods = ("Ninja", "Tradewind_offshore", "Tradewind_upland", "Tradewind_lowland") From 48c3afa8fdb5896de8d21fe168b576f0265efcc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Fri, 29 May 2026 10:58:45 +0200 Subject: [PATCH 3/5] Add suggestions from copilot review --- NEWS.md | 2 +- src/datastructures.jl | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/NEWS.md b/NEWS.md index 70d5add..daa4cc5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -20,7 +20,7 @@ ### Initial version of the package * Provide sampling routines for C++ and Python for incorporation into `EnergyModelsX` models. -* Utilize the sampling routines for sampling from:. +* Utilize the sampling routines for sampling from: * C++: `BioCHP` node. * Python: `MultipleBuildingTypes`, `CSPandPV`, and `WindPower` nodes. * Incorporation of a `BioResource` for `BiOCHP` plant. diff --git a/src/datastructures.jl b/src/datastructures.jl index cae69c2..0a6be8a 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -255,9 +255,9 @@ end opex_var::TimeProfile, opex_fixed::TimeProfile, output::Dict{<:Resource,<:Real}, - wind_params::WindFarmParameters, time_start::DateTime, - time_end::DateTime; + time_end::DateTime, + wind_params::WindFarmParameters; data::Vector{<:ExtensionData} = ExtensionData[], data_path::String = "", ) @@ -271,9 +271,9 @@ a Python function. - **`opex_var::TimeProfile`** is the variable operating expense per energy unit produced. - **`opex_fixed::TimeProfile`** is the fixed operating expense. - **`output::Dict{<:Resource,<:Real}`** are the generated `Resource`s, normally Power, with conversion value `Real`. -- **`wind_params::WindFarmParameters`** are the parameters for the wind farm. See [`WindFarmParameters`](@ref) for details. - **`time_start::DateTime`** is the starting time for the wind power time series sampling. - **`time_end::DateTime`** is the end time for the wind power time series sampling. +- **`wind_params::WindFarmParameters`** are the parameters for the wind farm. See [`WindFarmParameters`](@ref) for details. # Keyword arguments - **`data`** is the additional data (*e.g.*, for investments). The default value is no `data`. @@ -290,9 +290,9 @@ function WindPower( opex_var::TimeProfile, opex_fixed::TimeProfile, output::Dict{<:Resource,<:Real}, - wind_params::WindFarmParameters, time_start::DateTime, - time_end::DateTime; + time_end::DateTime, + wind_params::WindFarmParameters; data::Vector{<:ExtensionData} = ExtensionData[], data_path::String = "", ) @@ -367,7 +367,7 @@ end Constructs a [`PV`](@ref) instance where the power production profile is sampled from the PVGIS API tool from the EU Science Hub (available at https://re.jrc.ec.europa.eu/pvg_tools). -# Fields +# Arguments - **`id`** is the name/identifier of the node. - **`cap::TimeProfile`** is the installed capacity. - **`opex_var::TimeProfile`** is the variable operating expense per energy unit produced. From e71261052f46d59837c60e89a86535a62fe8991d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Fri, 29 May 2026 11:10:05 +0200 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/src/library/public.md | 2 +- test/utils.jl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/library/public.md b/docs/src/library/public.md index 206423d..795ce01 100644 --- a/docs/src/library/public.md +++ b/docs/src/library/public.md @@ -33,9 +33,9 @@ EMLI.WindPower( ::TimeStruct.TimeProfile, ::TimeStruct.TimeProfile, ::Dict{<:EnergyModelsBase.Resource,<:Real}, - ::WindFarmParameters, ::DateTime, ::DateTime, + ::WindFarmParameters, ) EMLI.PV( ::Any, diff --git a/test/utils.jl b/test/utils.jl index b6e9a20..a20f3be 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -90,9 +90,9 @@ function simple_graph_wind(; opex_var, # Variable operational cost in €/MWh opex_fixed, # Fixed operational cost in €/MW/year output, # The generated resources with conversion value 1 - wind_params, # Windfarm data time_start, # Start time for the data - time_end; # End time for the data + time_end, # End time for the data + wind_params; # Windfarm data data_path, # Path to the data ) else From a815a5e77459cbd1f068579271e20d7b86e04a45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Fri, 29 May 2026 14:10:49 +0200 Subject: [PATCH 5/5] Add suggestions from review --- src/datastructures.jl | 65 ++++++++++++++++++++++++------------------- test/utils.jl | 6 ++-- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/datastructures.jl b/src/datastructures.jl index 0a6be8a..9a5f48f 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -2,16 +2,21 @@ abstract type AbstractParameters end """ PVParameters - PVParameters(lat::Real, lon::Real; loss::Real = 14.0, - pvtechchoice::String = "crystSi", mountingplace::String = "free", - optimalangles::Bool = true, usehorizon::Bool = true, + PVParameters( + lat::Real, + lon::Real; + loss::Real = 14.0, + pvtechchoice::String = "crystSi", + mountingplace::String = "free", + optimalangles::Bool = true, + usehorizon::Bool = true, ) A structure to hold parameters for photovoltaic (PV) power generation. # Fields -- **`lat::Real`** is the latitude of the location in decimal degrees (e.g., 52.0 for 52°N). -- **`lon::Real`** is the longitude of the location in decimal degrees (e.g., 13.0 for 13°E). +- **`lat::Real`** is the latitude of the location in decimal degrees (e.g., `52.5` for 52°30′ N, `-33.75` for 33°45′ S). +- **`lon::Real`** is the longitude of the location in decimal degrees (e.g., `13.5` for 13°30′ E, `-122.25` for 122°15′ W). - **`loss::Real=14.0`** is the total system losses in percentage (e.g., 14.0 for 14% losses). - **`pvtechchoice::String="crystSi"`** is the type of PV technology. Options include: - `"crystSi"`: Crystalline silicon (default). @@ -22,6 +27,9 @@ A structure to hold parameters for photovoltaic (PV) power generation. - `"building"`: Building-integrated system. - **`optimalangles::Bool=true`** is a flag for whether to use optimal tilt and azimuth angles for the PV system. - **`usehorizon::Bool=true`** is a flag for whether to include the effect of the horizon in the calculations. + +!!! note "Key word argument in constructors" + If not all fields with default values are provided, the user must use the keyword arguments. """ struct PVParameters <: AbstractParameters lat::Real @@ -41,14 +49,10 @@ struct PVParameters <: AbstractParameters usehorizon::Bool, ) errors = String[] - if !isfinite(lat) - push!(errors, "lat must be finite.") - elseif lat < -90 || lat > 90 + if lat < -90 || lat > 90 push!(errors, "lat must be in [-90, 90].") end - if !isfinite(lon) - push!(errors, "lon must be finite.") - elseif lon < -180 || lon > 180 + if lon < -180 || lon > 180 push!(errors, "lon must be in [-180, 180].") end if loss < 0 @@ -105,17 +109,20 @@ end A structure to hold wind farm parameters and metadata for wind power time series generation. # Fields -- **`id`**: Identifier for the wind farm. -- **`lat`**: Latitude of the wind farm. -- **`lon`**: Longitude of the wind farm. -- **`turbine_height`**: Height of the wind turbines in meters. -- **`orientation`**: Orientation of the wind farm (default: `missing`). -- **`shape`**: Shape of the wind farm (default: `missing`). -- **`method`** is the chosen method for data retrieval. The user can choose between the +- **`id`** is the identifier for the wind farm. +- **`lat::Real`** is the latitude of the location in decimal degrees (e.g., `52.5` for 52°30′ N, `-33.75` for 33°45′ S). +- **`lon::Real`** is the longitude of the location in decimal degrees (e.g., `13.5` for 13°30′ E, `-122.25` for 122°15′ W). +- **`turbine_height::Real`** is the height of the wind turbines in meters. +- **`orientation`** is the orientation of the wind farm (default: `missing`). +- **`shape`** is the shape of the wind farm (default: `missing`). +- **`method::String`** is the chosen method for data retrieval. The user can choose between the strings "Ninja", "Tradewind_offshore", "Tradewind_upland", and "Tradewind_lowland". The default value is "Ninja". -- **`source`** is the data source for wind data. The user can choose between the strings +- **`source::String`** is the data source for wind data. The user can choose between the strings "NORA3" and "ERA5". The default value is "NORA3". + +!!! note "Key word argument in constructors" + If not all fields with default values are provided, the user must use the keyword arguments. """ struct WindFarmParameters <: AbstractParameters id::String @@ -137,14 +144,14 @@ struct WindFarmParameters <: AbstractParameters source::String, ) errors = String[] - if !isfinite(lat) || lat < -90 || lat > 90 - push!(errors, "lat must be finite and in [-90, 90].") + if lat < -90 || lat > 90 + push!(errors, "lat must be in [-90, 90].") end - if !isfinite(lon) || lon < -180 || lon > 180 - push!(errors, "lon must be finite and in [-180, 180].") + if lon < -180 || lon > 180 + push!(errors, "lon must be in [-180, 180].") end - if !isfinite(turbine_height) || turbine_height <= 0 - push!(errors, "turbine_height must be finite and positive.") + if turbine_height <= 0 + push!(errors, "turbine_height must be positive.") end methods = ("Ninja", "Tradewind_offshore", "Tradewind_upland", "Tradewind_lowland") @@ -225,7 +232,7 @@ sampling the profile from a Python code through a constructor. - **`opex_var::TimeProfile`** is the variable operating expense per energy unit produced. - **`opex_fixed::TimeProfile`** is the fixed operating expense. - **`output::Dict{Resource, Real}`** are the generated `Resource`s, normally Power. -- **`data::Vector{<:ExtensionData}`** is the additional data (e.g. for investments). The field `data` +- **`data::Vector{<:ExtensionData}`** is the additional data (*e.g.* for investments). The field `data` is conditional through usage of a constructor. """ struct WindPower <: AbstractNonDisRES @@ -326,7 +333,7 @@ through a constructor. - **`opex_var::TimeProfile`** is the variable operating expense per energy unit produced. - **`opex_fixed::TimeProfile`** is the fixed operating expense. - **`output::Dict{Resource, Real}`** are the generated `Resource`s, normally Power. -- **`data::Vector{<:ExtensionData}`** is the additional data (e.g. for investments). The field `data` +- **`data::Vector{<:ExtensionData}`** is the additional data (*e.g.* for investments). The field `data` is conditional through usage of a constructor. """ struct PV <: AbstractNonDisRES @@ -378,7 +385,7 @@ the PVGIS API tool from the EU Science Hub (available at https://re.jrc.ec.europ - **`params::PVParameters`** are the parameters for the PV system. See [`PVParameters`](@ref) for details. # Keyword arguments -- **`data::Vector{<:ExtensionData}`** is the additional data (e.g., for investments). +- **`data::Vector{<:ExtensionData}`** is the additional data (*e.g.*, for investments). The field `data` is conditional through usage of a constructor. - **`data_path::String`** is the directory where the cached CSV file will be stored. Default is `"pvgis_cache"`. - **`filename_hint::String`** is an optional string to include in the cache file name for identification. Default is `""`. @@ -423,7 +430,7 @@ the strategic level. - **`opex_fixed::Dict{<:Resource,<:TimeProfile}`** is the fixed operating expense (for all resources in a Dict). - **`output::Dict{Resource, Real}`** are the generated `Resource`s, normally Power. -- **`data::Vector{<:ExtensionData}`** is the additional data (e.g. for investments). The field `data` +- **`data::Vector{<:ExtensionData}`** is the additional data (*e.g.* for investments). The field `data` is conditional through usage of a constructor. !!! danger diff --git a/test/utils.jl b/test/utils.jl index a20f3be..4593289 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -109,9 +109,9 @@ function simple_graph_wind(; end function simple_graph_buildings(; cap_p = nothing, - penalty_surplus = Dict(HeatHT=>FixedProfile(0.5), Power=>FixedProfile(0.5)), - penalty_deficit = Dict(HeatHT=>FixedProfile(0.5), Power=>FixedProfile(0.5)), - input = Dict(HeatHT=>1.0, Power=>1.0), + penalty_surplus = Dict(HeatHT => FixedProfile(0.5), Power => FixedProfile(0.5)), + penalty_deficit = Dict(HeatHT => FixedProfile(0.5), Power => FixedProfile(0.5)), + input = Dict(HeatHT => 1.0, Power => 1.0), ) # Creation of the initial problem with the NonDisRES node