From 0f5258f0b727236e5440e2873d0d5d688b2cc11d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Wed, 22 Apr 2026 15:39:52 +0200 Subject: [PATCH 01/20] Add new Building node --- NEWS.md | 9 +- Project.toml | 8 +- docs/make.jl | 3 +- docs/src/library/internals/methods-EMLI.md | 18 ++- docs/src/library/internals/types-EMLI.md | 13 ++ docs/src/library/public.md | 20 +++ docs/src/nodes/multiplebuildingtypes.md | 62 +++---- ext/EMGUIExt/descriptive_names.yml | 3 + src/EnergyModelsLanguageInterfaces.jl | 1 + src/checks.jl | 6 +- src/constraint_functions.jl | 18 +-- src/datastructures.jl | 180 ++++++++++++++++++--- src/model.jl | 6 +- src/utils.jl | 155 ++++++++++++++++++ test/test_buildings.jl | 65 ++++++++ test/utils.jl | 82 ++++++++++ 16 files changed, 566 insertions(+), 83 deletions(-) create mode 100644 docs/src/library/internals/types-EMLI.md diff --git a/NEWS.md b/NEWS.md index b726e37..243d98b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,13 @@ # Release notes -## Version 0.1.0 (2026-04-15) +## Version 0.1.0 (2026-04-25) + +### Add Building node + +* Added a `Building` node to the package, which provides sampling routines for building heat demand profiles based on temperature data. + A function `heat_demand_profile` is included to generate the heat demand profile from temperature data downloaded using hind cast data. + A function `get_met_data` is included to handle data retrieval, caching, and storage (using the python [`metocean_api_data`](https://metocean-api.readthedocs.io/en/latest/) library) for the meteorological data used in the `heat_demand_profile` function. + This function enables implementation of meteorological data dependent nodes. ### Add PV node diff --git a/Project.toml b/Project.toml index c023a41..d495503 100644 --- a/Project.toml +++ b/Project.toml @@ -28,10 +28,10 @@ EMIExt = "EnergyModelsInvestments" CSV = "0.10.16" DataFrames = "1.8.1" Dates = "1.10" -EnergyModelsBase = "0.9.0" -EnergyModelsHeat = "0.1.1" -EnergyModelsInvestments = "0.8.1" -EnergyModelsRenewableProducers = "0.6.5" +EnergyModelsBase = "0.10" +EnergyModelsHeat = "0.2" +EnergyModelsInvestments = "0.9" +EnergyModelsRenewableProducers = "0.7" HTTP = "1.11" JSON = "1.5.0" JuMP = "1.23" diff --git a/docs/make.jl b/docs/make.jl index b471828..d28b996 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -45,7 +45,7 @@ makedocs( "WindPower"=>"nodes/windpower.md", "PV"=>"nodes/pv.md", "PVandCSP"=>"nodes/pvandcsp.md", - "MultipleBuildingTypes"=>"nodes/multiplebuildingtypes.md", + "Buildings"=>"nodes/multiplebuildingtypes.md", ], "Resources" => Any["ResourceBio"=>"resources/resourcebio.md"], "Utility functions" => Any["Reference"=>"util-fun/reference.md"], @@ -55,6 +55,7 @@ makedocs( "Library" => Any[ "Public"=>"library/public.md", "Internals"=>String[ + "library/internals/types-EMLI.md", "library/internals/methods-EMLI.md", "library/internals/methods-EMB.md", "library/internals/methods-EMR.md", diff --git a/docs/src/library/internals/methods-EMLI.md b/docs/src/library/internals/methods-EMLI.md index 3854e25..c720c4f 100644 --- a/docs/src/library/internals/methods-EMLI.md +++ b/docs/src/library/internals/methods-EMLI.md @@ -9,14 +9,16 @@ Pages = ["methods-EMLI.md"] ## [Utility methods](@id lib-int-met-util) ```@docs -EnergyModelsLanguageInterfaces.cleanup_libraries -EnergyModelsLanguageInterfaces.getfirst -EnergyModelsLanguageInterfaces.get_python_function -EnergyModelsLanguageInterfaces.moisture -EnergyModelsLanguageInterfaces.bio_type -EnergyModelsLanguageInterfaces.electricity_resource -EnergyModelsLanguageInterfaces.pvgis_profile -EnergyModelsLanguageInterfaces.get_pvgis_data +EMLI.cleanup_libraries +EMLI.getfirst +EMLI.get_python_function +EMLI.moisture +EMLI.bio_type +EMLI.electricity_resource +EMLI.pvgis_profile +EMLI.get_pvgis_data +EMLI.get_met_data +EMLI.heat_demand_profile ``` ## [Macros](@id lib-int-mac-util) diff --git a/docs/src/library/internals/types-EMLI.md b/docs/src/library/internals/types-EMLI.md new file mode 100644 index 0000000..12c93be --- /dev/null +++ b/docs/src/library/internals/types-EMLI.md @@ -0,0 +1,13 @@ +# [Types](@id lib-int-types) + +## [Index](@id lib-int-types-idx) + +```@index +Pages = ["types-EMLI.md"] +``` + +## [Nodal supertypes](@id lib-int-types-node) + +```@docs +EMLI.AbstractBuildings +``` \ No newline at end of file diff --git a/docs/src/library/public.md b/docs/src/library/public.md index ad44468..471c28f 100644 --- a/docs/src/library/public.md +++ b/docs/src/library/public.md @@ -18,6 +18,7 @@ EnergyModelsLanguageInterfaces.PVParameters EnergyModelsLanguageInterfaces.WindPower EnergyModelsLanguageInterfaces.PV EnergyModelsLanguageInterfaces.CSPandPV +EnergyModelsLanguageInterfaces.Building EnergyModelsLanguageInterfaces.MultipleBuildingTypes EnergyModelsLanguageInterfaces.BioCHP ``` @@ -59,6 +60,25 @@ EnergyModelsLanguageInterfaces.CSPandPV( data_path::String = "", source::String = "NORA3", ) +EnergyModelsLanguageInterfaces.Building( + ::Any, + ::Dict{<:Resource,<:TimeProfile}, + ::Dict{<:Resource,<:TimeProfile}, + ::Dict{<:Resource,<:TimeProfile}, + ::Dict{<:Resource,<:Real}, + ::DateTime, + ::DateTime, + ::Real, + ::Real, + ::Resource, + ::Function; + data::Vector{<:Data} = Data[], + data_path::String = joinpath(tempdir(), "building"), + source::String = "NORA3", + reload::Bool = true, + save_csv::Bool = true, + use_cache::Bool = true, +) EnergyModelsLanguageInterfaces.MultipleBuildingTypes( ::Any, ::Dict, diff --git a/docs/src/nodes/multiplebuildingtypes.md b/docs/src/nodes/multiplebuildingtypes.md index 9ad82d0..7db1036 100644 --- a/docs/src/nodes/multiplebuildingtypes.md +++ b/docs/src/nodes/multiplebuildingtypes.md @@ -1,24 +1,30 @@ -# [Multiple building types sink node](@id nodes-MultipleBuildingTypes) +# [Building nodes](@id nodes-AbstractBuildings) -The [`MultipleBuildingTypes`](@ref) node creates sinks for all demand resources with penalties for both surplus and deficit. -The implementation uses `Dict` structures for the fields `cap`, `penalty_surplus`, and `penalty_deficit` to facilitate multiple [Resource](@extref EnergyModelsBase.Resource)s. +The [`EMLI.AbstractBuildings`](@ref) node types creates sinks for all demand resources with penalties for both surplus and deficit. +The implementation uses `Dict` structures for the fields `cap`, `penalty_surplus`, and `penalty_deficit` to facilitate +multiple [Resource](@extref EnergyModelsBase.Resource)s. This approach allows modeling building demands with flexible penalty mechanisms for over- and under-supply. -The type is also used to enable a specialized constructor that samples the [`Tecnalia_Building-Stock-Energy-Model`](https://github.com/iDesignRES/Tecnalia_Building-Stock-Energy-Model) module. + +The subtype [`MultipleBuildingTypes`](@ref) is used to enable a specialized constructor that samples +the [`Tecnalia_Building-Stock-Energy-Model`](https://github.com/iDesignRES/Tecnalia_Building-Stock-Energy-Model) module. + +The subtype [`Building`](@ref) can be used to model a simple building demand where the heat demand is calculated from +the temperature at a single location and a user-defined temperature-to-demand function. See the [constructor](@ref lib-pub-sampling_constructors) for more details. !!! note "Sampling Tecnalia_Building-Stock-Energy-Model module" To use the [constructor](@ref lib-pub-sampling_constructors) for [`MultipleBuildingTypes`](@ref) that samples the [`Tecnalia_Building-Stock-Energy-Model`](https://github.com/iDesignRES/Tecnalia_Building-Stock-Energy-Model) module, follow the installation in the [Use nodes](@ref how_to-utilize-use_nodes) section. !!! danger - Investments are currently not available for this node. + Investments are currently not available for these nodes. -## [Introduced type and its field](@id nodes-MultipleBuildingTypes-fields) +## [Introduced type and its field](@id nodes-AbstractBuildings-fields) -The [`MultipleBuildingTypes`](@ref) is a subtype of [`Sink`](@extref EnergyModelsBase.Sink) and is implemented as a specialized sink node. -Hence, it utilizes the same functions declared in `EnergyModelsBase`. +[`EMLI.AbstractBuildings`](@ref) nodes are a subtype of [`Sink`](@extref EnergyModelsBase.Sink) and are implemented as specialized sink nodes. +Hence, they utilize the same functions declared in `EnergyModelsBase`. -### [Standard fields](@id nodes-MultipleBuildingTypes-fields-stand) +### [Standard fields](@id nodes-AbstractBuildings-fields-stand) -Standard fields of a [`MultipleBuildingTypes`](@ref) node are given as: +Standard fields of [`EMLI.AbstractBuildings`](@ref) nodes are given as: - **`id`**:\ The field `id` is only used for providing a name to the node. @@ -30,16 +36,16 @@ Standard fields of a [`MultipleBuildingTypes`](@ref) node are given as: An entry for providing additional data to the model. In the current version, it is not applicable. We intend to change this in future releases to enable investments. - !!! note "Constructor for `MultipleBuildingTypes`" + !!! note "Constructor for `EMLI.AbstractBuildings` nodes" The field `data` is not required as we include a constructor when the value is excluded. !!! danger "Using `CaptureData`" As a `Sink` node does not have any output, it is not possible to utilize `CaptureData`. If you still plan to specify it, you will receive an error in the model building. -### [Additional fields](@id nodes-MultipleBuildingTypes-fields-new) +### [Additional fields](@id nodes-AbstractBuildings-fields-new) -[`MultipleBuildingTypes`](@ref) nodes introduce additional fields for demand and penalty specifications: +[`EMLI.AbstractBuildings`](@ref) nodes introduce additional fields for demand and penalty specifications: - **`cap::Dict{<:Resource,<:TimeProfile}`**:\ The demand capacity for each of the input resources. @@ -53,7 +59,7 @@ Standard fields of a [`MultipleBuildingTypes`](@ref) node are given as: These penalties are added to the variable operating expenses. All values have to be non-negative. -## [Mathematical description](@id nodes-MultipleBuildingTypes-math) +## [Mathematical description](@id nodes-AbstractBuildings-math) In the following mathematical equations, we use the name for variables and functions used in the model. Variables are in general represented as @@ -66,11 +72,11 @@ with square brackets, while functions are represented as with parantheses. -### [Variables](@id nodes-MultipleBuildingTypes-math-var) +### [Variables](@id nodes-AbstractBuildings-math-var) -#### [Standard variables](@id nodes-MultipleBuildingTypes-math-var-stand) +#### [Standard variables](@id nodes-AbstractBuildings-math-var-stand) -The [`MultipleBuildingTypes`](@ref) node type utilizes standard variables from the [`Sink`](@extref EnergyModelsBase.Sink) node type and includes: +[`EMLI.AbstractBuildings`](@ref) nodes utilize standard variables from the [`Sink`](@extref EnergyModelsBase.Sink) node type and includes: - [``\texttt{opex\_var}``](@extref EnergyModelsBase man-opt_var-opex) - [``\texttt{opex\_fixed}``](@extref EnergyModelsBase man-opt_var-opex) @@ -80,28 +86,28 @@ The [`MultipleBuildingTypes`](@ref) node type utilizes standard variables from t - [``\texttt{emissions\_node}``](@extref EnergyModelsBase man-opt_var-emissions) if `EmissionsData` is added to the field `data` !!! note "cap\_use and cap\_inst" - A `MultipleBuildingTypes` has an individual capacity for all its resources, that is each `Resource` has its own capacity which must be satisfied. - As a consequence, the standard variables [``\texttt{cap\_use}``](@extref EnergyModelsBase man-opt_var-cap) and [``\texttt{cap\_use}``](@extref EnergyModelsBase man-opt_var-cap) are not defined for [`MultipleBuildingTypes`](@ref) nodes through a new method for the function [`has_capacity`](@ref EnergyModelsBase.capacity). + [`EMLI.AbstractBuildings`](@ref) nodes have an individual capacity for all their resources, that is each `Resource` has its own capacity which must be satisfied. + As a consequence, the standard variables [``\texttt{cap\_use}``](@extref EnergyModelsBase man-opt_var-cap) and [``\texttt{cap\_use}``](@extref EnergyModelsBase man-opt_var-cap) are not defined for [`EMLI.AbstractBuildings`](@ref) nodes through a new method for the function [`has_capacity`](@ref EnergyModelsBase.capacity). -#### [Additional variables](@id nodes-MultipleBuildingTypes-math-add) +#### [Additional variables](@id nodes-AbstractBuildings-math-add) -[`MultipleBuildingTypes`](@ref) introduces the following variables: +[`EMLI.AbstractBuildings`](@ref) nodes introduce the following variables: - ``\texttt{buildings\_surplus}[n, t, p]``: Surplus (over-supply) for node ``n`` in operational period ``t`` for resource ``p``. - ``\texttt{buildings\_deficit}[n, t, p]``: Deficit (under-supply) for node ``n`` in operational period ``t`` for resource ``p``. - ``\texttt{sink\_surplus}[n, t]``: Total surplus aggregated across all resources. - ``\texttt{sink\_deficit}[n, t]``: Total deficit aggregated across all resources. -### [Constraints](@id nodes-MultipleBuildingTypes-math-con) +### [Constraints](@id nodes-AbstractBuildings-math-con) -The following sections omit the direct inclusion of the vector of [`MultipleBuildingTypes`](@ref) nodes. -Instead, it is implicitly assumed that the constraints are valid ``\forall n โˆˆ N^{\text{MultipleBuildingTypes}}`` if not stated differently. +The following sections omit the direct inclusion of the vector of [`EMLI.AbstractBuildings`](@ref) nodes. +Instead, it is implicitly assumed that the constraints are valid ``\forall n โˆˆ N^{\text{EMLI.AbstractBuildings}}`` if not stated differently. In addition, all constraints are valid ``\forall t \in T`` (that is in all operational periods) or ``\forall t_{inv} \in T^{Inv}`` (that is in all strategic periods). Finally, all constraints are valid ``\forall p \in inputs(n)`` (that is in all input resources). -#### [Standard constraints](@id nodes-MultipleBuildingTypes-math-con-stand) +#### [Standard constraints](@id nodes-AbstractBuildings-math-con-stand) -[`MultipleBuildingTypes`](@ref) nodes utilize the following standard constraint functions: +[`EMLI.AbstractBuildings`](@ref) nodes utilize the following standard constraint functions: - `constraints_opex_fixed`:\ The current implementation fixes the fixed operating expenses of a sink to 0. @@ -149,6 +155,6 @@ The function `constraints_opex_var` is extended with a new method to allow for i It also takes into account potential operational scenarios and their probability as well as representative periods. -#### [Additional constraints](@id nodes-MultipleBuildingTypes-math-con-add) +#### [Additional constraints](@id nodes-AbstractBuildings-math-con-add) -[`MultipleBuildingTypes`](@ref) nodes do not add additional constraints. +[`EMLI.AbstractBuildings`](@ref) nodes do not add additional constraints. diff --git a/ext/EMGUIExt/descriptive_names.yml b/ext/EMGUIExt/descriptive_names.yml index c1ecf09..096cb71 100644 --- a/ext/EMGUIExt/descriptive_names.yml +++ b/ext/EMGUIExt/descriptive_names.yml @@ -3,5 +3,8 @@ structures: EnergyModelsLanguageInterfaces: MultipleBuildingTypes: + penalty_surplus: "Penalty for surplus" + penalty_deficit: "Penalty for deficits" + Building: penalty_surplus: "Penalty for surplus" penalty_deficit: "Penalty for deficits" \ No newline at end of file diff --git a/src/EnergyModelsLanguageInterfaces.jl b/src/EnergyModelsLanguageInterfaces.jl index 6816fb6..eded97f 100644 --- a/src/EnergyModelsLanguageInterfaces.jl +++ b/src/EnergyModelsLanguageInterfaces.jl @@ -37,5 +37,6 @@ export call_python_function, fetch_element export WindPower, CSPandPV, MultipleBuildingTypes export ResourceBio, BioCHP export PV, PVParameters +export Building end # module EnergyModelsLanguageInterfaces diff --git a/src/checks.jl b/src/checks.jl index b7ec67d..e719f06 100644 --- a/src/checks.jl +++ b/src/checks.jl @@ -35,9 +35,9 @@ function EMB.check_node( end """ - EMB.check_node(n::MultipleBuildingTypes, ๐’ฏ, ::EnergyModel, ::Bool) + EMB.check_node(n::AbstractBuildings, ๐’ฏ, ::EnergyModel, ::Bool) -This method checks that the [`MultipleBuildingTypes`](@ref) node is valid. +This method checks that the [`AbstractBuildings`](@ref) node is valid. ## Checks - The field `cap_p` is required to be non-negative for all resources `p`. @@ -45,7 +45,7 @@ This method checks that the [`MultipleBuildingTypes`](@ref) node is valid. - The sum of the fields `penalty_surplus` and `penalty_deficit` has to be non-negative to avoid an infeasible model. """ -function EMB.check_node(n::MultipleBuildingTypes, ๐’ฏ, ::EnergyModel, ::Bool) +function EMB.check_node(n::AbstractBuildings, ๐’ฏ, ::EnergyModel, ::Bool) ๐’ซ = inputs(n) for p โˆˆ ๐’ซ @assert_or_log( diff --git a/src/constraint_functions.jl b/src/constraint_functions.jl index 2e0cdb0..1c6058f 100644 --- a/src/constraint_functions.jl +++ b/src/constraint_functions.jl @@ -1,12 +1,12 @@ """ - EMB.constraints_capacity(m, n::MultipleBuildingTypes, ๐’ฏ::TimeStructure, modeltype::EnergyModel) + EMB.constraints_capacity(m, n::AbstractBuildings, ๐’ฏ::TimeStructure, modeltype::EnergyModel) Function for creating the constraints on the maximum capacity of a -[`MultipleBuildingTypes`](@ref) node. +[`AbstractBuildings`](@ref) node. """ function EMB.constraints_capacity( m, - n::MultipleBuildingTypes, + n::AbstractBuildings, ๐’ฏ::TimeStructure, ::EnergyModel, ) @@ -57,12 +57,12 @@ function EMB.constraints_capacity(m, n::CSPandPV, ๐’ฏ::TimeStructure, ::EnergyM end """ - EMB.constraints_flow_in(m, n::MultipleBuildingTypes, ๐’ฏ::TimeStructure, ::EnergyModel) + EMB.constraints_flow_in(m, n::AbstractBuildings, ๐’ฏ::TimeStructure, ::EnergyModel) -The constraints on the inlet flow for a [`MultipleBuildingTypes`](@ref) node are implemented +The constraints on the inlet flow for a [`AbstractBuildings`](@ref) node are implemented directly in the function `EMB.constraints_capacity`. """ -function EMB.constraints_flow_in(m, ::MultipleBuildingTypes, ::TimeStructure, ::EnergyModel) +function EMB.constraints_flow_in(m, ::AbstractBuildings, ::TimeStructure, ::EnergyModel) end """ @@ -81,15 +81,15 @@ function EMB.constraints_flow_out(m, n::CSPandPV, ๐’ฏ::TimeStructure, modeltype end """ - EMB.constraints_opex_var(m, n::MultipleBuildingTypes, ๐’ฏแดตโฟแต›, ::EnergyModel) + EMB.constraints_opex_var(m, n::AbstractBuildings, ๐’ฏแดตโฟแต›, ::EnergyModel) -Function for creating the constraint on the variable OPEX of a [`MultipleBuildingTypes`](@ref) +Function for creating the constraint on the variable OPEX of a [`AbstractBuildings`](@ref) node. The variable OPEX is calculate through the penalties for both `surplus` and `deficit` for each of the individual resource demands. """ -function EMB.constraints_opex_var(m, n::MultipleBuildingTypes, ๐’ฏแดตโฟแต›, ::EnergyModel) +function EMB.constraints_opex_var(m, n::AbstractBuildings, ๐’ฏแดตโฟแต›, ::EnergyModel) @constraint(m, [t_inv โˆˆ ๐’ฏแดตโฟแต›], m[:opex_var][n, t_inv] == sum( diff --git a/src/datastructures.jl b/src/datastructures.jl index 561fcaa..670b5c1 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -517,7 +517,135 @@ EMR.profile(n::CSPandPV, p::Resource) = n.profile[p] EMR.profile(n::CSPandPV, t, p::Resource) = n.profile[p][t] """ - struct MultipleBuildingTypes <: EMB.Sink + abstract type AbstractBuildings <: EMB.Sink + +Abstract supertype for all building nodes. +""" +abstract type AbstractBuildings <: EMB.Sink end + +""" + struct Building <: AbstractBuildings + +A [`MultipleBuildingTypes`](@ref) node that creates sinks for all demand resources. The +demand for each resources has a penalty for both surplus and deficit. +The penalties introduced in the field `penalty` affect the variable OPEX for both a surplus +and deficit. + +# Fields +- **`id`** is the name/identifier of the node. +- **`cap::Dict{<:Resource,<:TimeProfile}`** is the demand. +- **`penalty_surplus::Dict{<:Resource,<:TimeProfile}`** are the penalties for surplus. +- **`penalty_deficit::Dict{<:Resource,<:TimeProfile}`** are the penalties for deficit. +- **`input::Dict{<:Resource,<:Real}`** are the input + [`Resource`](@extref EnergyModelsBase.Resource)s with conversion value `Real`. +- **`data::Vector{<:Data}`** is the additional data (*e.g.*, for investments). The field `data` + is conditional through usage of a constructor. + +!!! danger + Investments are not available for this node. +""" +struct Building <: AbstractBuildings + id::Any + cap::Dict{<:Resource,<:TimeProfile} + penalty_surplus::Dict{<:Resource,<:TimeProfile} + penalty_deficit::Dict{<:Resource,<:TimeProfile} + input::Dict{<:Resource,<:Real} + data::Vector{<:Data} +end +function Building( + id::Any, + cap::Dict{<:Resource,<:TimeProfile}, + penalty_surplus::Dict{<:Resource,<:TimeProfile}, + penalty_deficit::Dict{<:Resource,<:TimeProfile}, + input::Dict{<:Resource,<:Real}, +) + return Building(id, cap, penalty_surplus, penalty_deficit, input, Data[]) +end + +""" + Building( + id::Any, + cap::Dict{<:Resource,<:TimeProfile}, + time_start::DateTime, + time_end::DateTime, + locations::DataFrame, + penalty_surplus::Dict{<:Resource,<:TimeProfile}, + penalty_deficit::Dict{<:Resource,<:TimeProfile}, + heat_resource::Resource, + temp_to_demand::Function; + data::Vector{<:Data} = Data[], + data_path::String = joinpath(tempdir(), "building"), + source::String = "NORA3", + reload::Bool = true, + save_csv::Bool = true, + use_cache::Bool = true, + ) + +Constructs a `Building` instance where the heat demand profile is generated from temperature data +downloaded using hind cast data (see [`heat_demand_profile`](@ref) for details). +The temperature-to-demand mapping is provided by `temp_to_demand`. + +# Arguments +- **`id`**: Identifier or name of the node. +- **`cap`**: Demand dictionary for resources (no need to provide heat demand, it will be generated). +- **`time_start`**, **`time_end`**: Start and end times as `DateTime` objects. +- **`locations`**: DataFrame containing "lat" and "lon" columns for each location. +- **`penalty_surplus`**, **`penalty_deficit`**: Penalty dictionaries for surplus and deficit, respectively. +- **`heat_resource`**: `Resource` object representing heat demand in the model. +- **`temp_to_demand`**: Function mapping temperature in Kelvin to demand. + +# Keyword Arguments +- **`data`**: Additional data to be used. +- **`data_path`**: Directory path for cached CSV files. +- **`source`**: Data source, e.g., "NORA3" or "ERA5". +- **`reload`**: Boolean flag to reload data from local CSV files. +- **`save_csv`**: Boolean flag to save data to CSV files. +- **`use_cache`**: Boolean flag to use local cache. +""" +function Building( + id::Any, + cap::Dict{<:Resource,<:TimeProfile}, + penalty_surplus::Dict{<:Resource,<:TimeProfile}, + penalty_deficit::Dict{<:Resource,<:TimeProfile}, + input::Dict{<:Resource,<:Real}, + time_start::DateTime, + time_end::DateTime, + lat::Real, + lon::Real, + heat_resource::Resource, + temp_to_demand::Function; + data::Vector{<:Data} = Data[], + data_path::String = joinpath(tempdir(), "building"), + source::String = "NORA3", + reload::Bool = true, + save_csv::Bool = true, + use_cache::Bool = true, +) + df = heat_demand_profile( + time_start, + time_end, + lat, + lon, + temp_to_demand; + data_path = data_path, + source = source, + reload = reload, + save_csv = save_csv, + use_cache = use_cache, + ) + if heat_resource โˆˆ keys(cap) + @warn "The provided capacity dictionary already contains a profile for the `heat_resource`. + The generated heat demand profile will overwrite the existing profile." + end + # Copy to ensure we don't modify the original `cap` dictionary and have the correct key type + cap_ext::Dict{Resource,TimeProfile} = copy(cap) + cap_ext[heat_resource] = OperationalProfile(df.heat_demand) + + return Building(id, cap_ext, penalty_surplus, penalty_deficit, input, data) +end + +""" + struct MultipleBuildingTypes <: AbstractBuildings A [`MultipleBuildingTypes`](@ref) node that creates sinks for all demand resources. The demand for each resources has a penalty for both surplus and deficit. @@ -537,7 +665,7 @@ and deficit. !!! danger Investments are not available for this node. """ -struct MultipleBuildingTypes <: EMB.Sink +struct MultipleBuildingTypes <: AbstractBuildings id::Any cap::Dict{<:Resource,<:TimeProfile} penalty_surplus::Dict{<:Resource,<:TimeProfile} @@ -721,47 +849,47 @@ function MultipleBuildingTypes( end """ - EMB.capacity(n::MultipleBuildingTypes) - EMB.capacity(n::MultipleBuildingTypes, p::Resource) - EMB.capacity(n::MultipleBuildingTypes, t, p::Resource) + EMB.capacity(n::AbstractBuildings) + EMB.capacity(n::AbstractBuildings, p::Resource) + EMB.capacity(n::AbstractBuildings, t, p::Resource) -Returns the capacity of a MultipleBuildingTypes `n` as a `Dictionary` or of resource `p` as `TimeProfile` +Returns the capacity of an AbstractBuildings `n` as a `Dictionary` or of resource `p` as `TimeProfile` or in operational period `t`. """ -EMB.capacity(n::MultipleBuildingTypes) = n.cap -EMB.capacity(n::MultipleBuildingTypes, p::Resource) = n.cap[p] -EMB.capacity(n::MultipleBuildingTypes, t, p::Resource) = n.cap[p][t] +EMB.capacity(n::AbstractBuildings) = n.cap +EMB.capacity(n::AbstractBuildings, p::Resource) = n.cap[p] +EMB.capacity(n::AbstractBuildings, t, p::Resource) = n.cap[p][t] """ - EMB.has_capacity(n::MultipleBuildingTypes) + EMB.has_capacity(n::AbstractBuildings) -A MultipleBuildingTypes has capacity for all its resources but not in a EMB sense. +An AbstractBuildings has capacity for all its resources but not in a EMB sense. """ -EMB.has_capacity(n::MultipleBuildingTypes) = false +EMB.has_capacity(n::AbstractBuildings) = false """ - EMB.surplus_penalty(n::MultipleBuildingTypes) - EMB.surplus_penalty(n::MultipleBuildingTypes, p::Resource) - EMB.surplus_penalty(n::MultipleBuildingTypes, t, p::Resource) + EMB.surplus_penalty(n::AbstractBuildings) + EMB.surplus_penalty(n::AbstractBuildings, p::Resource) + EMB.surplus_penalty(n::AbstractBuildings, t, p::Resource) -Returns the surplus penalty of MultipleBuildingTypes `n` as a `Dictionary` or of resource `p` as `TimeProfile` +Returns the surplus penalty of AbstractBuildings `n` as a `Dictionary` or of resource `p` as `TimeProfile` or in operational period `t`. """ -EMB.surplus_penalty(n::MultipleBuildingTypes) = n.penalty_surplus -EMB.surplus_penalty(n::MultipleBuildingTypes, p::Resource) = n.penalty_surplus[p] -EMB.surplus_penalty(n::MultipleBuildingTypes, t, p::Resource) = n.penalty_surplus[p][t] +EMB.surplus_penalty(n::AbstractBuildings) = n.penalty_surplus +EMB.surplus_penalty(n::AbstractBuildings, p::Resource) = n.penalty_surplus[p] +EMB.surplus_penalty(n::AbstractBuildings, t, p::Resource) = n.penalty_surplus[p][t] """ - EMB.deficit_penalty(n::MultipleBuildingTypes) - EMB.deficit_penalty(n::MultipleBuildingTypes, p::Resource) - EMB.deficit_penalty(n::MultipleBuildingTypes, t, p::Resource) + EMB.deficit_penalty(n::AbstractBuildings) + EMB.deficit_penalty(n::AbstractBuildings, p::Resource) + EMB.deficit_penalty(n::AbstractBuildings, t, p::Resource) -Returns the deficit penalty of MultipleBuildingTypes `n` as a `Dictionary` or of resource `p` as `TimeProfile` +Returns the deficit penalty of AbstractBuildings `n` as a `Dictionary` or of resource `p` as `TimeProfile` or in operational period `t`. """ -EMB.deficit_penalty(n::MultipleBuildingTypes) = n.penalty_deficit -EMB.deficit_penalty(n::MultipleBuildingTypes, p::Resource) = n.penalty_deficit[p] -EMB.deficit_penalty(n::MultipleBuildingTypes, t, p::Resource) = n.penalty_deficit[p][t] +EMB.deficit_penalty(n::AbstractBuildings) = n.penalty_deficit +EMB.deficit_penalty(n::AbstractBuildings, p::Resource) = n.penalty_deficit[p] +EMB.deficit_penalty(n::AbstractBuildings, t, p::Resource) = n.penalty_deficit[p][t] """ ResourceBio{T<:Real} <: Resource diff --git a/src/model.jl b/src/model.jl index c201388..e56b56e 100644 --- a/src/model.jl +++ b/src/model.jl @@ -1,13 +1,13 @@ """ - EMB.variables_node(m, ๐’ฉ::Vector{MultipleBuildingTypes}, ๐’ฏ, ::EnergyModel) + EMB.variables_node(m, ๐’ฉ::Vector{AbstractBuildings}, ๐’ฏ, ::EnergyModel) -For a [`MultipleBuildingTypes`](@ref) node, the following variables are created: +For a [`AbstractBuildings`](@ref) node, the following variables are created: - `buildings_surplus[n, t, p]` is the surplus of node `n` with resource `p` in operational period `t`. - `buildings_deficit[n, t, p]` is the surplus of node `n` with resource `p` in operational period `t`. """ -function EMB.variables_node(m, ๐’ฉ::Vector{MultipleBuildingTypes}, ๐’ฏ, ::EnergyModel) +function EMB.variables_node(m, ๐’ฉ::Vector{AbstractBuildings}, ๐’ฏ, ::EnergyModel) # Declaration of the required subsets. ๐’ซ = unique([p for n โˆˆ ๐’ฉ for p โˆˆ inputs(n)]) diff --git a/src/utils.jl b/src/utils.jl index 580d081..7fdc964 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -327,3 +327,158 @@ function get_pvgis_data( # WS10m [m/s] - Wind speed at 10m return DataFrame(rows) end + +""" + heat_demand_profile(time_start::DateTime, time_end::DateTime, lat::Float64, lon::Float64, + temp_to_demand::Function; data_path::String = "heat_data", + filename_hint::String = "", source::String = "NORA3", reload::Bool = true, + save_csv::Bool = true, use_cache::Bool = true) + +Generates a heat demand profile for a specified location and time period using temperature data +and a user-provided temperature-to-demand mapping function. + +The function retrieves temperature data for the given latitude and longitude from the specified +data source (e.g., "NORA3" or "ERA5") and applies the `temp_to_demand` function to convert +temperature values into heat demand. + +The data source is queried using the [`get_met_data`](@ref) function, which handles data retrieval, +caching, and storage. + +# Arguments +- **`time_start::DateTime`**: Start of the time period for the demand profile. +- **`time_end::DateTime`**: End of the time period for the demand profile. +- **`lat::Float64`**: Latitude of the location. +- **`lon::Float64`**: Longitude of the location. +- **`temp_to_demand::Function`**: Function mapping temperature in Kelvin to demand. +- **`data_path::String`**: Directory path to store or load temperature data (default: "heat_data"). +- **`filename_hint::String`**: Optional hint for naming the data file (default: ""). +- **`source::String`**: Data source for temperature (default: "NORA3"). +- **`reload::Bool`**: If true, reloads data even if cached data exists (default: true). +- **`save_csv::Bool`**: If true, saves the generated profile to a CSV file (default: true). +- **`use_cache::Bool`**: If true, uses cached data if available (default: true). +""" +function heat_demand_profile( + time_start::DateTime, + time_end::DateTime, + lat::Float64, + lon::Float64, + temp_to_demand::Function; + data_path::String = "metocean_api_data", + filename_hint::String = "", + source::String = "NORA3", + reload::Bool = true, + save_csv::Bool = true, + use_cache::Bool = true, +) + if source == "NORA3" + product = "NORA3_atm_sub" + variables = ["air_temperature_2m"] + elseif source == "ERA5" + product = "ERA5" + variables = ["2m_temperature"] + else + error("Unsupported data source: $source. Use 'NORA3' or 'ERA5'.") + end + df = get_met_data( + time_start, + time_end, + lat, + lon, + product, + variables; + data_path, + filename_hint, + reload, + save_csv, + use_cache, + ) + df.heat_demand = temp_to_demand.(df.air_temperature_2m) + return df +end + +""" + get_met_data(time_start::DateTime, time_end::DateTime, lat::Float64, lon::Float64, + product::String, variables::Vector{String}; data_path::String, + filename_hint::String, reload::Bool, save_csv::Bool, use_cache::Bool) + +Fetches meteorological data for a specified time range and geographic location. + +# Arguments +- `time_start::DateTime`: Start of the time range for data retrieval. +- `time_end::DateTime`: End of the time range for data retrieval. +- `lat::Float64`: Latitude of the location. +- `lon::Float64`: Longitude of the location. +- `product::String`: Name of the meteorological data product to use. +- `variables::Vector{String}`: List of meteorological variables to retrieve. +- `data_path::String`: Directory path where data files are stored or will be saved. +- `filename_hint::String`: Hint for naming the output file. +- `reload::Bool`: If `true`, forces re-download of data even if cached data exists. +- `save_csv::Bool`: If `true`, saves the retrieved data as a CSV file. +- `use_cache::Bool`: If `true`, uses cached data if available. + +# Notes +- The function may download data from remote sources if not available locally or if `reload` is + set to `true`. +- If `save_csv` is enabled, the data will be saved to a CSV file in the specified `data_path`. +- Caching behavior is controlled by the `use_cache` parameter. + +!!! note "Usage of the ERA5 data source" + For use of the "ERA5" data source, the user needs to register and obtain a CDS API key. + - Perform step 1: https://cds.climate.copernicus.eu/how-to-api +""" +function get_met_data( + time_start::DateTime, + time_end::DateTime, + lat::Float64, + lon::Float64, + product::String, + variables::Vector{String}; + data_path::String = "metocean_api_data", + filename_hint::String = "", + reload::Bool = true, + save_csv::Bool = true, + use_cache::Bool = true, +) + # Ensure the cache directory exists + isdir(data_path) || mkpath(data_path) + csv_path = joinpath( + data_path, + product * "_" * Dates.format(time_start, "yyyymmdd") * "_" * filename_hint * ".csv", + ) + + if reload && isfile(csv_path) && filesize(csv_path) > 0 + df = CSV.read( + csv_path, + DataFrame; + comment = "#", + dateformat = "yyyy-mm-dd HH:MM:SS", + types = Dict(:time => DateTime), + ) + rename!(df, :time => :time_utc) + return df + else + ts = pyimport("metocean_api.ts") + ts_data = ts.TimeSeries( + lon = lon, + lat = lat, + start_time = Dates.format(time_start, "yyyy-mm-dd"), + end_time = Dates.format(time_end, "yyyy-mm-dd"), + product = product, + variable = variables, + datafile = nothing, + ) + ts_data.datafile = csv_path + ts_data.import_data(save_csv = save_csv, save_nc = false, use_cache = use_cache) + idx_np = ts_data.data.index.to_numpy(copy = true) + time = DateTime(1970, 1, 1) .+ Nanosecond.(idx_np.astype("int64")) + data = ts_data.data.to_numpy(copy = true) + colnames = Symbol.(ts_data.data.columns.tolist()) + df = DataFrame([time data], [:time_utc; colnames...]) + # Ensure correct types + df.time_utc = DateTime.(df.time_utc) + for col โˆˆ colnames + df[!, col] = Float64.(df[!, col]) + end + return df + end +end diff --git a/test/test_buildings.jl b/test/test_buildings.jl index dcf2a9f..f2cd69a 100644 --- a/test/test_buildings.jl +++ b/test/test_buildings.jl @@ -92,3 +92,68 @@ @test EMB.has_capacity(buildings) == false end end + +@testset "Building" begin + for _ โˆˆ 1:2 # Run the test two times to also test running from stored files (from first run) + case, modeltype, m = simple_graph_building() + + # Run the model + m = EMB.run_model(case, modeltype, OPTIMIZER) + + building = get_node(case, "Building") + sink = get_node(case, "Source for HeatHT") + products = get_products(case) + building_res = products[1:(end-1)] # All resources except CO2 + HeatHT = products[2] + + # Extraction of the time structure + ๐’ฏ = get_time_struct(case) + + # Run of the general tests + general_tests(m) + + # Test that the source data is the same + ref_values = OperationalProfile([ + 15.0, + 15.61, + 16.37, + 16.45, + 16.78, + 17.70, + 17.80, + 17.91, + 17.90, + 17.0, + 14.91, + 14.02, + 14.01, + 14.45, + 14.91, + 15.01, + 15.87, + 16.67, + 16.58, + 17.04, + 17.57, + 17.26, + 17.76, + 18.03, + ]) + + @test all( + isapprox( + value.(m[:flow_out][sink, t, HeatHT]), + ref_values[t]; + atol = TEST_ATOL, + ) for t โˆˆ ๐’ฏ + ) + @test all( + value.(m[:buildings_surplus][building, t, p]) == 0.0 for + t โˆˆ ๐’ฏ, p โˆˆ building_res + ) + @test all( + value.(m[:buildings_deficit][building, t, p]) == 0.0 for + t โˆˆ ๐’ฏ, p โˆˆ building_res + ) + end +end diff --git a/test/utils.jl b/test/utils.jl index 693057b..03f8035 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -218,6 +218,88 @@ function simple_graph_buildings(; cap_p = nothing, return case, modeltype, create_model(case, modeltype) end +function simple_graph_building(; 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)) + # Creation of the initial problem with the NonDisRES node + time_start_str = "2019-01-01" + time_end_str = "2019-01-01" + op_duration = 1 + op_number = 24 * (Dates.value(Date(time_end_str) - Date(time_start_str)) + 1) + operational_periods = SimpleTimes(op_number, op_duration) + + sp_duration = [1, 2, 10] + T = TwoLevel(sp_duration, operational_periods; op_per_strat = 8760.0) + + time_start = DateTime(time_start_str * "T00:00:00") + time_end = DateTime(time_end_str * "T23:00:00") + + building_res = [Power, HeatHT] + products = [building_res..., CO2] + + sources = [ + RefSource( + "Source for " * resource.id, + FixedProfile(150e6), + FixedProfile(120), + FixedProfile(0), + Dict(resource => 1.0), + ) for resource โˆˆ building_res + ] + if isnothing(cap_p) + penalty_surplus = Dict{Resource,TimeProfile}( + resource => FixedProfile(100) for resource โˆˆ building_res + ) + penalty_deficit = Dict{Resource,TimeProfile}( + resource => FixedProfile(1e4) for resource โˆˆ building_res + ) + # Example temp_to_demand function (replace with your actual function) + temp_to_demand(temp) = max(0, 20 - (temp - 273.15)) + # Example location (replace with actual values or make it an argument) + lat, lon = 59.91, 10.75 # Oslo coordinates as example + cap = Dict( + resource => FixedProfile(120) for resource โˆˆ building_res if resource != HeatHT + ) + building = Building( + "Building", + cap, + penalty_surplus, + penalty_deficit, + input, + time_start, + time_end, + lat, + lon, + HeatHT, + temp_to_demand; + data_path = joinpath(pkgdir(EMLI), "test", "data", "building"), + source = "NORA3", + reload = true, + save_csv = true, + use_cache = true, + ) + else + building = Building( + "Buildings", + cap_p, + penalty_surplus, + penalty_deficit, + input, + ) + end + + nodes = [building, sources...] + links = [Direct(node.id * "-Buildings", node, building, Linear()) for node โˆˆ sources] + + case = Case(T, products, [nodes, links], [[get_nodes, get_links]]) + + em_limits = Dict(CO2 => FixedProfile(1e10)) # Emission cap for COโ‚‚ in t/year + em_cost = Dict(CO2 => FixedProfile(71.0)) # Emission price for COโ‚‚ in โ‚ฌ/t + modeltype = OperationalModel(em_limits, em_cost, CO2) + return case, modeltype, create_model(case, modeltype) +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)), From 933e48b4f7e37364aaaaa40f727051c4c5f6a10d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Sat, 25 Apr 2026 11:04:47 +0200 Subject: [PATCH 02/20] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/src/nodes/multiplebuildingtypes.md | 2 +- src/datastructures.jl | 9 +++++---- src/utils.jl | 6 +++++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/src/nodes/multiplebuildingtypes.md b/docs/src/nodes/multiplebuildingtypes.md index 7db1036..6dcafdb 100644 --- a/docs/src/nodes/multiplebuildingtypes.md +++ b/docs/src/nodes/multiplebuildingtypes.md @@ -1,6 +1,6 @@ # [Building nodes](@id nodes-AbstractBuildings) -The [`EMLI.AbstractBuildings`](@ref) node types creates sinks for all demand resources with penalties for both surplus and deficit. +The [`EMLI.AbstractBuildings`](@ref) node types create sinks for all demand resources with penalties for both surplus and deficit. The implementation uses `Dict` structures for the fields `cap`, `penalty_surplus`, and `penalty_deficit` to facilitate multiple [Resource](@extref EnergyModelsBase.Resource)s. This approach allows modeling building demands with flexible penalty mechanisms for over- and under-supply. diff --git a/src/datastructures.jl b/src/datastructures.jl index 670b5c1..3bd9e77 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -526,10 +526,11 @@ abstract type AbstractBuildings <: EMB.Sink end """ struct Building <: AbstractBuildings -A [`MultipleBuildingTypes`](@ref) node that creates sinks for all demand resources. The -demand for each resources has a penalty for both surplus and deficit. -The penalties introduced in the field `penalty` affect the variable OPEX for both a surplus -and deficit. +A `Building` node representing a single-location building heat-demand-from-temperature +constructor. It creates sinks for all demand resources, where the demand for each resource +can have separate penalties for surplus and deficit. +The penalties introduced in the fields `penalty_surplus` and `penalty_deficit` affect the +variable OPEX for surplus and deficit, respectively. # Fields - **`id`** is the name/identifier of the node. diff --git a/src/utils.jl b/src/utils.jl index 7fdc964..ea4e76a 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -392,7 +392,11 @@ function heat_demand_profile( save_csv, use_cache, ) - df.heat_demand = temp_to_demand.(df.air_temperature_2m) + temperature_column = Symbol(variables[1]) + if !hasproperty(df, temperature_column) + error("Temperature column $(variables[1]) not found in meteorological data.") + end + df.heat_demand = temp_to_demand.(df[!, temperature_column]) return df end From 073e4202aeabc988214e57fae039f299ab3e929e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Sat, 25 Apr 2026 11:59:40 +0200 Subject: [PATCH 03/20] Add suggestions from Copilot review --- src/datastructures.jl | 4 +-- src/utils.jl | 66 ++++++++++++++++++++++++++++--------------- test/utils.jl | 10 ++----- 3 files changed, 47 insertions(+), 33 deletions(-) diff --git a/src/datastructures.jl b/src/datastructures.jl index 3bd9e77..48e2b73 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -526,7 +526,7 @@ abstract type AbstractBuildings <: EMB.Sink end """ struct Building <: AbstractBuildings -A `Building` node representing a single-location building heat-demand-from-temperature +A [`Building`](@ref) node representing a single-location building heat-demand-from-temperature constructor. It creates sinks for all demand resources, where the demand for each resource can have separate penalties for surplus and deficit. The penalties introduced in the fields `penalty_surplus` and `penalty_deficit` affect the @@ -582,7 +582,7 @@ end use_cache::Bool = true, ) -Constructs a `Building` instance where the heat demand profile is generated from temperature data +Constructs a [`Building`](@ref) instance where the heat demand profile is generated from temperature data downloaded using hind cast data (see [`heat_demand_profile`](@ref) for details). The temperature-to-demand mapping is provided by `temp_to_demand`. diff --git a/src/utils.jl b/src/utils.jl index ea4e76a..cfb8fb7 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -96,6 +96,15 @@ function fetch_element(elements, id) return getfirst(element -> element.id == id, elements) end +function sanitize_filename_hint(filename_hint::String) + if isempty(filename_hint) + filehint = "" + else + filehint = "_" * replace(filename_hint, r"[^\w\.-]" => "_") + end + return filehint +end + """ pvgis_profile(time_start::DateTime, params::PVParameters; peakpower::Real=1.0, @@ -156,11 +165,7 @@ function pvgis_profile(time_start::DateTime, params::PVParameters; end # Create a sanitized file hint for the cache file name - if isempty(filename_hint) - filehint = "" - else - filehint = "_" * replace(filename_hint, r"[^\w\.-]" => "_") - end + filehint = sanitize_filename_hint(filename_hint) csv_path = joinpath( data_path, @@ -329,10 +334,19 @@ function get_pvgis_data( end """ - heat_demand_profile(time_start::DateTime, time_end::DateTime, lat::Float64, lon::Float64, - temp_to_demand::Function; data_path::String = "heat_data", - filename_hint::String = "", source::String = "NORA3", reload::Bool = true, - save_csv::Bool = true, use_cache::Bool = true) + heat_demand_profile( + time_start::DateTime, + time_end::DateTime, + lat::Float64, + lon::Float64, + temp_to_demand::Function; + data_path::String = "metocean_api_data", + filename_hint::String = "", + source::String = "NORA3", + reload::Bool = true, + save_csv::Bool = true, + use_cache::Bool = true, + ) Generates a heat demand profile for a specified location and time period using temperature data and a user-provided temperature-to-demand mapping function. @@ -350,10 +364,10 @@ caching, and storage. - **`lat::Float64`**: Latitude of the location. - **`lon::Float64`**: Longitude of the location. - **`temp_to_demand::Function`**: Function mapping temperature in Kelvin to demand. -- **`data_path::String`**: Directory path to store or load temperature data (default: "heat_data"). +- **`data_path::String`**: Directory path to store or load temperature data (default: "metocean_api_data"). - **`filename_hint::String`**: Optional hint for naming the data file (default: ""). - **`source::String`**: Data source for temperature (default: "NORA3"). -- **`reload::Bool`**: If true, reloads data even if cached data exists (default: true). +- **`reload::Bool`**: If true, reloads CSV data even if cached data exists (default: true). - **`save_csv::Bool`**: If true, saves the generated profile to a CSV file (default: true). - **`use_cache::Bool`**: If true, uses cached data if available (default: true). """ @@ -408,17 +422,17 @@ end Fetches meteorological data for a specified time range and geographic location. # Arguments -- `time_start::DateTime`: Start of the time range for data retrieval. -- `time_end::DateTime`: End of the time range for data retrieval. -- `lat::Float64`: Latitude of the location. -- `lon::Float64`: Longitude of the location. -- `product::String`: Name of the meteorological data product to use. -- `variables::Vector{String}`: List of meteorological variables to retrieve. -- `data_path::String`: Directory path where data files are stored or will be saved. -- `filename_hint::String`: Hint for naming the output file. -- `reload::Bool`: If `true`, forces re-download of data even if cached data exists. -- `save_csv::Bool`: If `true`, saves the retrieved data as a CSV file. -- `use_cache::Bool`: If `true`, uses cached data if available. +- **`time_start::DateTime`**: Start of the time range for data retrieval. +- **`time_end::DateTime`**: End of the time range for data retrieval. +- **`lat::Float64`**: Latitude of the location. +- **`lon::Float64`**: Longitude of the location. +- **`product::String`**: Name of the meteorological data product to use. +- **`variables::Vector{String}`**: List of meteorological variables to retrieve. +- **`data_path::String`**: Directory path where data files are stored or will be saved. +- **`filename_hint::String`**: Hint for naming the output file. +- **`reload::Bool`**: If true, reloads CSV data even if cached data exists (default: true). +- **`save_csv::Bool`**: If `true`, saves the retrieved data as a CSV file (default: true). +- **`use_cache::Bool`**: If `true`, uses cached data if available (default: true). # Notes - The function may download data from remote sources if not available locally or if `reload` is @@ -445,9 +459,15 @@ function get_met_data( ) # Ensure the cache directory exists isdir(data_path) || mkpath(data_path) + + # Create a sanitized file hint for the cache file name + filehint = sanitize_filename_hint(filename_hint) + csv_path = joinpath( data_path, - product * "_" * Dates.format(time_start, "yyyymmdd") * "_" * filename_hint * ".csv", + product * "_" * Dates.format(time_start, "yyyymmdd") * "_" * + Dates.format(time_end, "yyyymmdd") * "_lat" * string(lat) * "_lon" * + string(lon) * filehint * ".csv", ) if reload && isfile(csv_path) && filesize(csv_path) > 0 diff --git a/test/utils.jl b/test/utils.jl index 03f8035..984abbe 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -219,8 +219,8 @@ function simple_graph_buildings(; cap_p = nothing, end function simple_graph_building(; 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)), + 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)) # Creation of the initial problem with the NonDisRES node time_start_str = "2019-01-01" @@ -248,12 +248,6 @@ function simple_graph_building(; cap_p = nothing, ) for resource โˆˆ building_res ] if isnothing(cap_p) - penalty_surplus = Dict{Resource,TimeProfile}( - resource => FixedProfile(100) for resource โˆˆ building_res - ) - penalty_deficit = Dict{Resource,TimeProfile}( - resource => FixedProfile(1e4) for resource โˆˆ building_res - ) # Example temp_to_demand function (replace with your actual function) temp_to_demand(temp) = max(0, 20 - (temp - 273.15)) # Example location (replace with actual values or make it an argument) From 4fa1decf702f1a61ed213bcb5d0b9ba4d3a9513e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Sat, 25 Apr 2026 12:37:00 +0200 Subject: [PATCH 04/20] Adress Copilot second review [skip ci] --- src/datastructures.jl | 29 ++++++++++++++++++----------- test/utils.jl | 4 ++-- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/datastructures.jl b/src/datastructures.jl index 48e2b73..2003575 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -567,11 +567,13 @@ end Building( id::Any, cap::Dict{<:Resource,<:TimeProfile}, - time_start::DateTime, - time_end::DateTime, - locations::DataFrame, penalty_surplus::Dict{<:Resource,<:TimeProfile}, penalty_deficit::Dict{<:Resource,<:TimeProfile}, + input::Dict{<:Resource,<:Real}, + time_start::DateTime, + time_end::DateTime, + lat::Real, + lon::Real, heat_resource::Resource, temp_to_demand::Function; data::Vector{<:Data} = Data[], @@ -583,15 +585,19 @@ end ) Constructs a [`Building`](@ref) instance where the heat demand profile is generated from temperature data -downloaded using hind cast data (see [`heat_demand_profile`](@ref) for details). +downloaded using hindcast data (see [`heat_demand_profile`](@ref) for details). The temperature-to-demand mapping is provided by `temp_to_demand`. # Arguments - **`id`**: Identifier or name of the node. - **`cap`**: Demand dictionary for resources (no need to provide heat demand, it will be generated). -- **`time_start`**, **`time_end`**: Start and end times as `DateTime` objects. -- **`locations`**: DataFrame containing "lat" and "lon" columns for each location. -- **`penalty_surplus`**, **`penalty_deficit`**: Penalty dictionaries for surplus and deficit, respectively. +- **`penalty_surplus`**: Penalty dictionaries for surplus. +- **`penalty_deficit`**: Penalty dictionaries for deficit. +- **`input`**: Dictionary of input resources with conversion values. +- **`time_start`**: Start time for the demand profile as a `DateTime` object. +- **`time_end`**: End time for the demand profile as a `DateTime` object. +- **`lat`**: Latitude of the building location. +- **`lon`**: Longitude of the building location. - **`heat_resource`**: `Resource` object representing heat demand in the model. - **`temp_to_demand`**: Function mapping temperature in Kelvin to demand. @@ -635,11 +641,12 @@ function Building( use_cache = use_cache, ) if heat_resource โˆˆ keys(cap) - @warn "The provided capacity dictionary already contains a profile for the `heat_resource`. - The generated heat demand profile will overwrite the existing profile." + @warn "The provided capacity dictionary already contains a profile for the `heat_resource`. " * + "The generated heat demand profile will overwrite the existing profile." end - # Copy to ensure we don't modify the original `cap` dictionary and have the correct key type - cap_ext::Dict{Resource,TimeProfile} = copy(cap) + + # Copy to ensure we don't modify the original `cap` dictionary and widen key/value types + cap_ext = Dict{Resource,TimeProfile}(resource => profile for (resource, profile) โˆˆ cap) cap_ext[heat_resource] = OperationalProfile(df.heat_demand) return Building(id, cap_ext, penalty_surplus, penalty_deficit, input, data) diff --git a/test/utils.jl b/test/utils.jl index 984abbe..9c328b3 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -275,7 +275,7 @@ function simple_graph_building(; cap_p = nothing, ) else building = Building( - "Buildings", + "Building", cap_p, penalty_surplus, penalty_deficit, @@ -284,7 +284,7 @@ function simple_graph_building(; cap_p = nothing, end nodes = [building, sources...] - links = [Direct(node.id * "-Buildings", node, building, Linear()) for node โˆˆ sources] + links = [Direct(node.id * "-Building", node, building, Linear()) for node โˆˆ sources] case = Case(T, products, [nodes, links], [[get_nodes, get_links]]) From f36bf349f82a246cc43744906b0628da7402e8da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Sat, 25 Apr 2026 13:17:58 +0200 Subject: [PATCH 05/20] Adress Copilot third review [skip ci] --- NEWS.md | 2 +- docs/src/how-to/utilize.md | 4 ++- docs/src/nodes/multiplebuildingtypes.md | 16 +++++++-- src/datastructures.jl | 46 ++++++++++++------------- src/model.jl | 2 +- test/test_buildings.jl | 2 +- 6 files changed, 43 insertions(+), 29 deletions(-) diff --git a/NEWS.md b/NEWS.md index 243d98b..2c5bf08 100644 --- a/NEWS.md +++ b/NEWS.md @@ -5,7 +5,7 @@ ### Add Building node * Added a `Building` node to the package, which provides sampling routines for building heat demand profiles based on temperature data. - A function `heat_demand_profile` is included to generate the heat demand profile from temperature data downloaded using hind cast data. + A function `heat_demand_profile` is included to generate the heat demand profile from temperature data downloaded using hindcast data. A function `get_met_data` is included to handle data retrieval, caching, and storage (using the python [`metocean_api_data`](https://metocean-api.readthedocs.io/en/latest/) library) for the meteorological data used in the `heat_demand_profile` function. This function enables implementation of meteorological data dependent nodes. diff --git a/docs/src/how-to/utilize.md b/docs/src/how-to/utilize.md index 98b06ab..3648ef5 100644 --- a/docs/src/how-to/utilize.md +++ b/docs/src/how-to/utilize.md @@ -189,7 +189,9 @@ cd ../.. !!! note "Environments" If you are a developer, you probably want to install the python modules in a separate environment which can be done with, *e.g.*, [miniconda](https://www.anaconda.com/docs/getting-started/miniconda/install). -Enable these by starting a julia session in the main folder +### [Enable python modules](@id how_to-utilize-use_nodes-enable_python_modules) + +The python modules above (and other modules) can be enabled by starting a julia session in the main folder ```PowerShell julia --project=. diff --git a/docs/src/nodes/multiplebuildingtypes.md b/docs/src/nodes/multiplebuildingtypes.md index 6dcafdb..50c9a30 100644 --- a/docs/src/nodes/multiplebuildingtypes.md +++ b/docs/src/nodes/multiplebuildingtypes.md @@ -14,6 +14,18 @@ the temperature at a single location and a user-defined temperature-to-demand fu !!! note "Sampling Tecnalia_Building-Stock-Energy-Model module" To use the [constructor](@ref lib-pub-sampling_constructors) for [`MultipleBuildingTypes`](@ref) that samples the [`Tecnalia_Building-Stock-Energy-Model`](https://github.com/iDesignRES/Tecnalia_Building-Stock-Energy-Model) module, follow the installation in the [Use nodes](@ref how_to-utilize-use_nodes) section. +!!! note "Using the sampling constructor for `Building` nodes" + The sampling [constructor](@ref lib-pub-sampling_constructors) for [`Building`](@ref) node requires the installation of the python package `metocean_api` which can be done with + + ```bash + pip install metocean_api + ``` + + Enable this package in julia by following the instructions in the [Enable python modules](@ref how_to-utilize-use_nodes-enable_python_modules) section. + + Note that this package is already available if the module wind_power_timeseries is installed as described in the [Install python modules](@ref how_to-utilize-use_nodes) section. + + !!! danger Investments are currently not available for these nodes. @@ -82,12 +94,12 @@ with parantheses. - [``\texttt{opex\_fixed}``](@extref EnergyModelsBase man-opt_var-opex) - [``\texttt{flow\_in}``](@extref EnergyModelsBase man-opt_var-flow) - [``\texttt{sink\_surplus}``](@extref EnergyModelsBase man-opt_var-sink): Declared as the total surplus aggregated across all resources. -- [``\texttt{sink\_deficit}``](@extref EnergyModelsBase man-opt_var-sink): Declared as the total surplus aggregated across all resources. +- [``\texttt{sink\_deficit}``](@extref EnergyModelsBase man-opt_var-sink): Declared as the total deficit aggregated across all resources. - [``\texttt{emissions\_node}``](@extref EnergyModelsBase man-opt_var-emissions) if `EmissionsData` is added to the field `data` !!! note "cap\_use and cap\_inst" [`EMLI.AbstractBuildings`](@ref) nodes have an individual capacity for all their resources, that is each `Resource` has its own capacity which must be satisfied. - As a consequence, the standard variables [``\texttt{cap\_use}``](@extref EnergyModelsBase man-opt_var-cap) and [``\texttt{cap\_use}``](@extref EnergyModelsBase man-opt_var-cap) are not defined for [`EMLI.AbstractBuildings`](@ref) nodes through a new method for the function [`has_capacity`](@ref EnergyModelsBase.capacity). + As a consequence, the standard variables [``\texttt{cap\_use}``](@extref EnergyModelsBase man-opt_var-cap) and [``\texttt{cap\_inst}``](@extref EnergyModelsBase man-opt_var-cap) are not defined for [`EMLI.AbstractBuildings`](@ref) nodes through a new method for the function [`has_capacity`](@ref EnergyModelsBase.capacity). #### [Additional variables](@id nodes-AbstractBuildings-math-add) diff --git a/src/datastructures.jl b/src/datastructures.jl index 2003575..b37a0e4 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -589,25 +589,25 @@ downloaded using hindcast data (see [`heat_demand_profile`](@ref) for details). The temperature-to-demand mapping is provided by `temp_to_demand`. # Arguments -- **`id`**: Identifier or name of the node. -- **`cap`**: Demand dictionary for resources (no need to provide heat demand, it will be generated). -- **`penalty_surplus`**: Penalty dictionaries for surplus. -- **`penalty_deficit`**: Penalty dictionaries for deficit. -- **`input`**: Dictionary of input resources with conversion values. -- **`time_start`**: Start time for the demand profile as a `DateTime` object. -- **`time_end`**: End time for the demand profile as a `DateTime` object. -- **`lat`**: Latitude of the building location. -- **`lon`**: Longitude of the building location. -- **`heat_resource`**: `Resource` object representing heat demand in the model. -- **`temp_to_demand`**: Function mapping temperature in Kelvin to demand. - -# Keyword Arguments -- **`data`**: Additional data to be used. -- **`data_path`**: Directory path for cached CSV files. -- **`source`**: Data source, e.g., "NORA3" or "ERA5". -- **`reload`**: Boolean flag to reload data from local CSV files. -- **`save_csv`**: Boolean flag to save data to CSV files. -- **`use_cache`**: Boolean flag to use local cache. +- **`id`** is the name/identifier of the node. +- **`cap::Dict{<:Resource,<:TimeProfile}`** is the demand (no need to provide heat demand, it will be generated). +- **`penalty_surplus::Dict{<:Resource,<:TimeProfile}`** are the penalties for surplus. +- **`penalty_deficit::Dict{<:Resource,<:TimeProfile}`** are the penalties for deficit. +- **`input::Dict{<:Resource,<:Real}`** are the input [`Resource`](@extref EnergyModelsBase.Resource)s with conversion value `Real`. +- **`time_start::DateTime`** is the start time for the demand profile. +- **`time_end::DateTime`** is the end time for the demand profile. +- **`lat::Real`** is the latitude of the building location. +- **`lon::Real`** is the longitude of the building location. +- **`heat_resource::Resource`** is the `Resource` object representing heat demand in the model. +- **`temp_to_demand::Function`** is the function mapping temperature in Kelvin to demand. + +# Keyword arguments +- **`data::Vector{<:Data}`** is the additional data to be used. +- **`data_path::String`** is the directory path for cached CSV files. +- **`source::String`** is the data source, e.g., "NORA3" or "ERA5". +- **`reload::Bool`** is a boolean flag to reload data from local CSV files. +- **`save_csv::Bool`** is a boolean flag to save data to CSV files. +- **`use_cache::Bool`** is a boolean flag to use local cache. """ function Building( id::Any, @@ -755,15 +755,15 @@ Constructs a `MultipleBuildingTypes` instance where the demand profiles are samp "Heat|Solar" => SolarHeat, ) ``` -- **`T`** is the TimeStructure used in the model. +- **`T::TimeStructure`** is the TimeStructure used in the model. - **`penalty_surplus::Dict{<:Resource,<:TimeProfile}`** is the penalties for surplus. - **`penalty_deficit::Dict{<:Resource,<:TimeProfile}`** is the penalties for deficit. # Keyword arguments -- **`data`** is the additional data (*e.g.*, for investments). The default value is no `data`. -- **`data_location`** is the location where the data is saved. The default value is in the +- **`data::Vector{<:Data}`** is the additional data (*e.g.*, for investments). The default value is no `data`. +- **`data_location::String`** is the location where the data is saved. The default value is in the temporary directory. -- **`overwrite_saved_data`** is a boolean that determines if the stored data should be +- **`overwrite_saved_data::Bool`** is a boolean that determines if the stored data should be overwritten (in which case the building_energy_process is called). The default value is `false`. diff --git a/src/model.jl b/src/model.jl index e56b56e..4075b5f 100644 --- a/src/model.jl +++ b/src/model.jl @@ -4,7 +4,7 @@ For a [`AbstractBuildings`](@ref) node, the following variables are created: - `buildings_surplus[n, t, p]` is the surplus of node `n` with resource `p` in operational period `t`. -- `buildings_deficit[n, t, p]` is the surplus of node `n` with resource `p` in operational +- `buildings_deficit[n, t, p]` is the deficit of node `n` with resource `p` in operational period `t`. """ function EMB.variables_node(m, ๐’ฉ::Vector{AbstractBuildings}, ๐’ฏ, ::EnergyModel) diff --git a/test/test_buildings.jl b/test/test_buildings.jl index f2cd69a..20668d0 100644 --- a/test/test_buildings.jl +++ b/test/test_buildings.jl @@ -104,7 +104,7 @@ end sink = get_node(case, "Source for HeatHT") products = get_products(case) building_res = products[1:(end-1)] # All resources except CO2 - HeatHT = products[2] + HeatHT = fetch_element(products, "HeatHT") # Extraction of the time structure ๐’ฏ = get_time_struct(case) From edfbdf8bb56772e2a8265e39e46ed0c71da98f25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Sat, 25 Apr 2026 13:41:30 +0200 Subject: [PATCH 06/20] Adress Copilot fourth review [skip ci] --- NEWS.md | 2 +- docs/src/nodes/multiplebuildingtypes.md | 2 +- src/utils.jl | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/NEWS.md b/NEWS.md index 2c5bf08..f0cd9db 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,7 +6,7 @@ * Added a `Building` node to the package, which provides sampling routines for building heat demand profiles based on temperature data. A function `heat_demand_profile` is included to generate the heat demand profile from temperature data downloaded using hindcast data. - A function `get_met_data` is included to handle data retrieval, caching, and storage (using the python [`metocean_api_data`](https://metocean-api.readthedocs.io/en/latest/) library) for the meteorological data used in the `heat_demand_profile` function. + A function `get_met_data` is included to handle data retrieval, caching, and storage (using the python [`metocean_api`](https://metocean-api.readthedocs.io/en/latest/) library) for the meteorological data used in the `heat_demand_profile` function. This function enables implementation of meteorological data dependent nodes. ### Add PV node diff --git a/docs/src/nodes/multiplebuildingtypes.md b/docs/src/nodes/multiplebuildingtypes.md index 50c9a30..fce11ad 100644 --- a/docs/src/nodes/multiplebuildingtypes.md +++ b/docs/src/nodes/multiplebuildingtypes.md @@ -82,7 +82,7 @@ with square brackets, while functions are represented as ``func\_example(index_1, index_2)`` -with parantheses. +with parentheses. ### [Variables](@id nodes-AbstractBuildings-math-var) diff --git a/src/utils.jl b/src/utils.jl index cfb8fb7..462f398 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -435,8 +435,7 @@ Fetches meteorological data for a specified time range and geographic location. - **`use_cache::Bool`**: If `true`, uses cached data if available (default: true). # Notes -- The function may download data from remote sources if not available locally or if `reload` is - set to `true`. +- The function may download data from remote sources if not available locally. - If `save_csv` is enabled, the data will be saved to a CSV file in the specified `data_path`. - Caching behavior is controlled by the `use_cache` parameter. From 09c5ba59b39234c3f24e0d800c2417d142581103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Sat, 25 Apr 2026 14:41:19 +0200 Subject: [PATCH 07/20] Adress Copilot fifth review and replaced Data with ExtensionData [skip ci] --- docs/src/library/public.md | 12 +-- docs/src/nodes/multiplebuildingtypes.md | 2 +- docs/src/nodes/pv.md | 2 +- docs/src/nodes/pvandcsp.md | 2 +- docs/src/nodes/windpower.md | 2 +- ext/EMIExt/EMIExt.jl | 6 +- src/datastructures.jl | 116 +++++++++++++----------- src/utils.jl | 30 ++++-- test/utils.jl | 3 - 9 files changed, 95 insertions(+), 80 deletions(-) diff --git a/docs/src/library/public.md b/docs/src/library/public.md index 471c28f..a0ec62c 100644 --- a/docs/src/library/public.md +++ b/docs/src/library/public.md @@ -45,7 +45,7 @@ EnergyModelsLanguageInterfaces.PV( ::DateTime, ::DateTime, ::PVParameters; - data::Vector{<:Data} = Data[], + data::Vector{<:ExtensionData} = ExtensionData[], data_path::String = "pvgis_cache", filename_hint::String = "", ) @@ -55,7 +55,7 @@ EnergyModelsLanguageInterfaces.CSPandPV( ::DateTime, ::DateTime, ::Dict{String,<:EnergyModelsBase.Resource}; - data::Vector{<:Data} = Data[], + data::Vector{<:ExtensionData} = ExtensionData[], method::String = "Ninja", data_path::String = "", source::String = "NORA3", @@ -72,10 +72,10 @@ EnergyModelsLanguageInterfaces.Building( ::Real, ::Resource, ::Function; - data::Vector{<:Data} = Data[], + data::Vector{<:ExtensionData} = ExtensionData[], data_path::String = joinpath(tempdir(), "building"), source::String = "NORA3", - reload::Bool = true, + reload_csv::Bool = true, save_csv::Bool = true, use_cache::Bool = true, ) @@ -89,7 +89,7 @@ EnergyModelsLanguageInterfaces.MultipleBuildingTypes( ::TimeStruct.TimeStructure, ::Dict{<:EnergyModelsBase.Resource,<:TimeStruct.TimeProfile}, ::Dict{<:EnergyModelsBase.Resource,<:TimeStruct.TimeProfile}; - data::Vector{<:EnergyModelsBase.Data} = EnergyModelsBase.Data[], + data::Vector{<:ExtensionData} = ExtensionData[], data_location::String = joinpath(tempdir(), "buildings"), overwrite_saved_data::Bool = false, ) @@ -99,7 +99,7 @@ EnergyModelsLanguageInterfaces.BioCHP( ::Dict{<:EnergyModelsLanguageInterfaces.ResourceBio,<:Real}, ::Dict{<:EnergyModelsHeat.ResourceHeat,<:Real}, ::EnergyModelsBase.Resource; - data::Vector{<:EnergyModelsBase.Data} = EnergyModelsBase.Data[], + data::Vector{<:ExtensionData} = ExtensionData[], libpath::String = joinpath(@__DIR__, "..", "..", "CHP_modelling", "build", "lib", "libbioCHP_wrapper.so"), ) ``` diff --git a/docs/src/nodes/multiplebuildingtypes.md b/docs/src/nodes/multiplebuildingtypes.md index fce11ad..cb95a9c 100644 --- a/docs/src/nodes/multiplebuildingtypes.md +++ b/docs/src/nodes/multiplebuildingtypes.md @@ -44,7 +44,7 @@ Standard fields of [`EMLI.AbstractBuildings`](@ref) nodes are given as: - **`input::Dict{<:Resource, <:Real}`**:\ The field `input` includes [`Resource`](@extref EnergyModelsBase.Resource)s with their corresponding conversion factors as dictionaries. All values have to be non-negative. -- **`data::Vector{Data}`**:\ +- **`data::Vector{<:ExtensionData}`**:\ An entry for providing additional data to the model. In the current version, it is not applicable. We intend to change this in future releases to enable investments. diff --git a/docs/src/nodes/pv.md b/docs/src/nodes/pv.md index 4c4863a..ed73ff2 100644 --- a/docs/src/nodes/pv.md +++ b/docs/src/nodes/pv.md @@ -35,7 +35,7 @@ The standard fields (of a [`AbstractNonDisRES`](@extref EnergyModelsRenewablePro The field `output` includes [`Resource`](@extref EnergyModelsBase.Resource)s with their corresponding conversion factors as dictionaries. In the case of a non-dispatchable renewable energy source, `output` should always include your *electricity* resource. In practice, you should use a value of 1.\ All values have to be non-negative. -- **`data::Vector{Data}`**:\ +- **`data::Vector{<:ExtensionData}`**:\ An entry for providing additional data to the model. In the current version, it is only relevant for additional investment data when [`EnergyModelsInvestments`](https://energymodelsx.github.io/EnergyModelsInvestments.jl/stable/) is used. diff --git a/docs/src/nodes/pvandcsp.md b/docs/src/nodes/pvandcsp.md index aaaef6c..6ce026c 100644 --- a/docs/src/nodes/pvandcsp.md +++ b/docs/src/nodes/pvandcsp.md @@ -27,7 +27,7 @@ Standard fields (of an [`AbstractNonDisRES`](@extref EnergyModelsRenewableProduc The field `output` includes [`Resource`](@extref EnergyModelsBase.Resource)s with their corresponding conversion factors as dictionaries. In the case of a PV and CSP energy source, `output` should always include your *electricity* resource and a *heat* resource. In practice, you should use a value of 1.\ All values have to be non-negative. -- **`data::Vector{Data}`**:\ +- **`data::Vector{<:ExtensionData}`**:\ An entry for providing additional data to the model. In the current version, it is not applicable. We intend to change this in future releases to enable investments. diff --git a/docs/src/nodes/windpower.md b/docs/src/nodes/windpower.md index 2b28583..578b9c1 100644 --- a/docs/src/nodes/windpower.md +++ b/docs/src/nodes/windpower.md @@ -38,7 +38,7 @@ The standard fields (of a [`AbstractNonDisRES`](@extref EnergyModelsRenewablePro The field `output` includes [`Resource`](@extref EnergyModelsBase.Resource)s with their corresponding conversion factors as dictionaries. In the case of a non-dispatchable renewable energy source, `output` should always include your *electricity* resource.In practice, you should use a value of 1.\ All values have to be non-negative. -- **`data::Vector{Data}`**:\ +- **`data::Vector{<:ExtensionData}`**:\ An entry for providing additional data to the model. In the current version, it is only relevant for additional investment data when [`EnergyModelsInvestments`](https://energymodelsx.github.io/EnergyModelsInvestments.jl/stable/) is used. diff --git a/ext/EMIExt/EMIExt.jl b/ext/EMIExt/EMIExt.jl index 4a5d8d5..58162f3 100644 --- a/ext/EMIExt/EMIExt.jl +++ b/ext/EMIExt/EMIExt.jl @@ -21,7 +21,7 @@ const EMLI = EnergyModelsLanguageInterfaces mass_fractions::Dict{<:ResourceBio,<:Real}, heat_output_ratios::Dict{<:ResourceHeat,<:Real}, electricity_resource::Resource; - data::Vector{Data} = Data[], + data::Vector{<:ExtensionData} = ExtensionData[], libpath::String = joinpath( @__DIR__, "..", @@ -50,7 +50,7 @@ library file located at `libpath`. The BioCHP has electricity production of the - **`electricity_resource`** is the `Resource` for the electricity. # Keyword arguments -- **`data::Vector{Data}`** 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. - **`libpath`** is the absolute path of the `CHP_modelling` library file. @@ -65,7 +65,7 @@ function EMLI.BioCHP( mass_fractions::Dict{<:ResourceBio,<:Real}, heat_output_ratios::Dict{<:ResourceHeat,<:Real}, electricity_resource::Resource; - data::Vector{Data} = Data[], + data::Vector{<:ExtensionData} = ExtensionData[], libpath::String = joinpath( @__DIR__, "..", diff --git a/src/datastructures.jl b/src/datastructures.jl index b37a0e4..e656a5d 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -93,7 +93,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{<:Data}`** 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 @@ -103,7 +103,7 @@ struct WindPower <: AbstractNonDisRES opex_var::TimeProfile opex_fixed::TimeProfile output::Dict{<:Resource,<:Real} - data::Vector{<:Data} + data::Vector{<:ExtensionData} end function WindPower( id::Any, @@ -113,7 +113,7 @@ function WindPower( opex_fixed::TimeProfile, output::Dict{<:Resource,<:Real}, ) - return WindPower(id, cap, profile, opex_var, opex_fixed, output, Data[]) + return WindPower(id, cap, profile, opex_var, opex_fixed, output, ExtensionData[]) end """ @@ -126,7 +126,7 @@ end opex_var::TimeProfile, opex_fixed::TimeProfile, output::Dict{<:Resource,<:Real}; - data::Vector{<:Data} = Data[], + data::Vector{<:ExtensionData} = ExtensionData[], method::String = "Ninja", data_path::String = "", source::String = "NORA3", @@ -182,7 +182,7 @@ function WindPower( opex_var::TimeProfile, opex_fixed::TimeProfile, output::Dict{<:Resource,<:Real}; - data::Vector{<:Data} = Data[], + data::Vector{<:ExtensionData} = ExtensionData[], method::String = "Ninja", data_path::String = "", source::String = "NORA3", @@ -217,7 +217,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{<:Data}`** 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 @@ -227,7 +227,7 @@ struct PV <: AbstractNonDisRES opex_var::TimeProfile opex_fixed::TimeProfile output::Dict{<:Resource,<:Real} - data::Vector{<:Data} + data::Vector{<:ExtensionData} end function PV( id::Any, @@ -237,7 +237,7 @@ function PV( opex_fixed::TimeProfile, output::Dict{<:Resource,<:Real}, ) - return PV(id, cap, profile, opex_var, opex_fixed, output, Data[]) + return PV(id, cap, profile, opex_var, opex_fixed, output, ExtensionData[]) end """ @@ -250,28 +250,28 @@ end time_start::DateTime, time_end::DateTime, params::PVParameters; - data::Vector{<:Data} = Data[], + data::Vector{<:ExtensionData} = ExtensionData[], data_path::String = "pvgis_cache", filename_hint::String = "", ) -Constructs a [`PV`](@ref) instance where the power production profile is sampled from -the PVGIS API. - -# 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. +A PV source producing power. It extends the existing `AbstractNonDisRES` node by extracting +data from the PVGIS tool from the EU Science Hub (available at https://re.jrc.ec.europa.eu/pvg_tools) +through a constructor. -# 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 `""`. +# 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. +- **`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, @@ -282,7 +282,7 @@ function PV( time_start::DateTime, time_end::DateTime, params::PVParameters; - data::Vector{<:Data} = Data[], + data::Vector{<:ExtensionData} = ExtensionData[], data_path::String = "pvgis_cache", filename_hint::String = "", ) @@ -313,7 +313,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{<:Data}`** 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 @@ -326,7 +326,7 @@ struct CSPandPV <: AbstractNonDisRES opex_var::Dict{<:Resource,<:TimeProfile} opex_fixed::Dict{<:Resource,<:TimeProfile} output::Dict{<:Resource,<:Real} - data::Vector{<:Data} + data::Vector{<:ExtensionData} end function CSPandPV( id::Any, @@ -336,7 +336,7 @@ function CSPandPV( opex_fixed::Dict{<:Resource,<:TimeProfile}, output::Dict{<:Resource,<:Real}, ) - return CSPandPV(id, cap, profile, opex_var, opex_fixed, output, Data[]) + return CSPandPV(id, cap, profile, opex_var, opex_fixed, output, ExtensionData[]) end """ @@ -346,7 +346,7 @@ end time_start::DateTime, time_end::DateTime, resources_map::Dict{String,<:Resource}; - data::Vector{<:Data} = Data[], + data::Vector{<:ExtensionData} = ExtensionData[], data_location::String = joinpath(tempdir(), "CSPandPV"), overwrite_saved_data::Bool = false, ) @@ -396,7 +396,7 @@ function CSPandPV( time_start::DateTime, time_end::DateTime, resources_map::Dict{String,<:Resource}; - data::Vector{<:Data} = Data[], + data::Vector{<:ExtensionData} = ExtensionData[], data_location::String = joinpath(tempdir(), "CSPandPV"), overwrite_saved_data::Bool = false, ) @@ -539,7 +539,7 @@ variable OPEX for surplus and deficit, respectively. - **`penalty_deficit::Dict{<:Resource,<:TimeProfile}`** are the penalties for deficit. - **`input::Dict{<:Resource,<:Real}`** are the input [`Resource`](@extref EnergyModelsBase.Resource)s with conversion value `Real`. -- **`data::Vector{<:Data}`** 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 @@ -551,7 +551,7 @@ struct Building <: AbstractBuildings penalty_surplus::Dict{<:Resource,<:TimeProfile} penalty_deficit::Dict{<:Resource,<:TimeProfile} input::Dict{<:Resource,<:Real} - data::Vector{<:Data} + data::Vector{<:ExtensionData} end function Building( id::Any, @@ -560,7 +560,7 @@ function Building( penalty_deficit::Dict{<:Resource,<:TimeProfile}, input::Dict{<:Resource,<:Real}, ) - return Building(id, cap, penalty_surplus, penalty_deficit, input, Data[]) + return Building(id, cap, penalty_surplus, penalty_deficit, input, ExtensionData[]) end """ @@ -576,10 +576,10 @@ end lon::Real, heat_resource::Resource, temp_to_demand::Function; - data::Vector{<:Data} = Data[], + data::Vector{<:ExtensionData} = ExtensionData[], data_path::String = joinpath(tempdir(), "building"), source::String = "NORA3", - reload::Bool = true, + reload_csv::Bool = true, save_csv::Bool = true, use_cache::Bool = true, ) @@ -593,7 +593,8 @@ The temperature-to-demand mapping is provided by `temp_to_demand`. - **`cap::Dict{<:Resource,<:TimeProfile}`** is the demand (no need to provide heat demand, it will be generated). - **`penalty_surplus::Dict{<:Resource,<:TimeProfile}`** are the penalties for surplus. - **`penalty_deficit::Dict{<:Resource,<:TimeProfile}`** are the penalties for deficit. -- **`input::Dict{<:Resource,<:Real}`** are the input [`Resource`](@extref EnergyModelsBase.Resource)s with conversion value `Real`. +- **`input::Dict{<:Resource,<:Real}`** are the input [`Resource`](@extref EnergyModelsBase.Resource)s + with conversion value `Real`. - **`time_start::DateTime`** is the start time for the demand profile. - **`time_end::DateTime`** is the end time for the demand profile. - **`lat::Real`** is the latitude of the building location. @@ -602,10 +603,10 @@ The temperature-to-demand mapping is provided by `temp_to_demand`. - **`temp_to_demand::Function`** is the function mapping temperature in Kelvin to demand. # Keyword arguments -- **`data::Vector{<:Data}`** is the additional data to be used. +- **`data::Vector{<:ExtensionData}`** is the additional data to be used. - **`data_path::String`** is the directory path for cached CSV files. - **`source::String`** is the data source, e.g., "NORA3" or "ERA5". -- **`reload::Bool`** is a boolean flag to reload data from local CSV files. +- **`reload_csv::Bool`** is a boolean flag to reload data from local CSV files if available (default: true). - **`save_csv::Bool`** is a boolean flag to save data to CSV files. - **`use_cache::Bool`** is a boolean flag to use local cache. """ @@ -621,10 +622,10 @@ function Building( lon::Real, heat_resource::Resource, temp_to_demand::Function; - data::Vector{<:Data} = Data[], + data::Vector{<:ExtensionData} = ExtensionData[], data_path::String = joinpath(tempdir(), "building"), source::String = "NORA3", - reload::Bool = true, + reload_csv::Bool = true, save_csv::Bool = true, use_cache::Bool = true, ) @@ -636,7 +637,7 @@ function Building( temp_to_demand; data_path = data_path, source = source, - reload = reload, + reload_csv = reload_csv, save_csv = save_csv, use_cache = use_cache, ) @@ -667,7 +668,7 @@ and deficit. - **`penalty_deficit::Dict{<:Resource,<:TimeProfile}`** are the penalties for deficit. - **`input::Dict{<:Resource,<:Real}`** are the input [`Resource`](@extref EnergyModelsBase.Resource)s with conversion value `Real`. -- **`data::Vector{<:Data}`** 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 @@ -679,7 +680,7 @@ struct MultipleBuildingTypes <: AbstractBuildings penalty_surplus::Dict{<:Resource,<:TimeProfile} penalty_deficit::Dict{<:Resource,<:TimeProfile} input::Dict{<:Resource,<:Real} - data::Vector{<:Data} + data::Vector{<:ExtensionData} end function MultipleBuildingTypes( id::Any, @@ -688,7 +689,14 @@ function MultipleBuildingTypes( penalty_deficit::Dict{<:Resource,<:TimeProfile}, input::Dict{<:Resource,<:Real}, ) - return MultipleBuildingTypes(id, cap, penalty_surplus, penalty_deficit, input, Data[]) + return MultipleBuildingTypes( + id, + cap, + penalty_surplus, + penalty_deficit, + input, + ExtensionData[], + ) end """ @@ -702,7 +710,7 @@ end T::TimeStructure, penalty_surplus::Dict{<:Resource,<:TimeProfile}, penalty_deficit::Dict{<:Resource,<:TimeProfile}; - data::Vector{<:Data} = Data[], + data::Vector{<:ExtensionData} = ExtensionData[], data_location::String = joinpath(tempdir(), "buildings"), overwrite_saved_data::Bool = false, ) @@ -760,7 +768,7 @@ Constructs a `MultipleBuildingTypes` instance where the demand profiles are samp - **`penalty_deficit::Dict{<:Resource,<:TimeProfile}`** is the penalties for deficit. # Keyword arguments -- **`data::Vector{<:Data}`** is the additional data (*e.g.*, for investments). The default value is no `data`. +- **`data::Vector{<:ExtensionData}`** is the additional data (*e.g.*, for investments). The default value is no `data`. - **`data_location::String`** is the location where the data is saved. The default value is in the temporary directory. - **`overwrite_saved_data::Bool`** is a boolean that determines if the stored data should be @@ -795,7 +803,7 @@ function MultipleBuildingTypes( T::TimeStructure, penalty_surplus::Dict{<:Resource,<:TimeProfile}, penalty_deficit::Dict{<:Resource,<:TimeProfile}; - data::Vector{<:Data} = Data[], + data::Vector{<:ExtensionData} = ExtensionData[], data_location::String = joinpath(tempdir(), "buildings"), overwrite_saved_data::Bool = false, ) @@ -961,7 +969,7 @@ The capacity is hereby normalized to a conversion value of 1 in the fields `inpu value `Real`. - **`output::Dict{<:Resource,<:Real}`** are the generated [`Resource`](@extref EnergyModelsBase.Resource)s with conversion value `Real`. -- **`data::Vector{<:Data}`** 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 BioCHP <: NetworkNode @@ -972,7 +980,7 @@ struct BioCHP <: NetworkNode opex_fixed::TimeProfile input::Dict{<:ResourceBio,<:Real} output::Dict{<:Resource,<:Real} - data::Vector{<:Data} + data::Vector{<:ExtensionData} end function BioCHP( id, @@ -1002,7 +1010,7 @@ end mass_fractions::Dict{<:ResourceBio,<:Real}, heat_output_ratios::Dict{<:ResourceHeat,<:Real}, electricity_resource::Resource; - data::Vector{<:Data} = Data[], + data::Vector{<:ExtensionData} = ExtensionData[], libpath::String = joinpath( @__DIR__, "..", @@ -1029,7 +1037,7 @@ library file located at `libpath`. The BioCHP has electricity production of the - **`electricity_resource`** is the `Resource` for the electricity. # Keyword arguments -- **`data::Vector{<:Data}`** is the additional data (*e.g.*, for investments). +- **`data::Vector{<:ExtensionData}`** is the additional data (*e.g.*, for investments). - **`libpath`** is the absolute path of the `CHP_modelling` library file. !!! note "EmissionsEnergy" @@ -1044,7 +1052,7 @@ function BioCHP( mass_fractions::Dict{<:ResourceBio,<:Real}, heat_output_ratios::Dict{<:ResourceHeat,<:Real}, electricity_resource::Resource; - data::Vector{<:Data} = Data[], + data::Vector{<:ExtensionData} = ExtensionData[], libpath::String = joinpath( @__DIR__, "..", @@ -1080,7 +1088,7 @@ function BioCHP( mass_fractions::Dict{<:ResourceBio,<:Real}, heat_output_ratios::Dict{<:ResourceHeat,<:Real}, electricity_resource::Resource, - data::Vector{<:Data}, + data::Vector{<:ExtensionData}, libpath::String, ) # Get the capacity diff --git a/src/utils.jl b/src/utils.jl index 462f398..c094027 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -343,7 +343,7 @@ end data_path::String = "metocean_api_data", filename_hint::String = "", source::String = "NORA3", - reload::Bool = true, + reload_csv::Bool = true, save_csv::Bool = true, use_cache::Bool = true, ) @@ -367,7 +367,7 @@ caching, and storage. - **`data_path::String`**: Directory path to store or load temperature data (default: "metocean_api_data"). - **`filename_hint::String`**: Optional hint for naming the data file (default: ""). - **`source::String`**: Data source for temperature (default: "NORA3"). -- **`reload::Bool`**: If true, reloads CSV data even if cached data exists (default: true). +- **`reload_csv::Bool`**: If true, reloads CSV data if available (default: true). - **`save_csv::Bool`**: If true, saves the generated profile to a CSV file (default: true). - **`use_cache::Bool`**: If true, uses cached data if available (default: true). """ @@ -380,7 +380,7 @@ function heat_demand_profile( data_path::String = "metocean_api_data", filename_hint::String = "", source::String = "NORA3", - reload::Bool = true, + reload_csv::Bool = true, save_csv::Bool = true, use_cache::Bool = true, ) @@ -402,7 +402,7 @@ function heat_demand_profile( variables; data_path, filename_hint, - reload, + reload_csv, save_csv, use_cache, ) @@ -415,9 +415,19 @@ function heat_demand_profile( end """ - get_met_data(time_start::DateTime, time_end::DateTime, lat::Float64, lon::Float64, - product::String, variables::Vector{String}; data_path::String, - filename_hint::String, reload::Bool, save_csv::Bool, use_cache::Bool) + get_met_data( + time_start::DateTime, + time_end::DateTime, + lat::Float64, + lon::Float64, + product::String, + variables::Vector{String}; + data_path::String = "metocean_api_data", + filename_hint::String = "", + reload_csv::Bool = true, + save_csv::Bool = true, + use_cache::Bool = true, + ) Fetches meteorological data for a specified time range and geographic location. @@ -430,7 +440,7 @@ Fetches meteorological data for a specified time range and geographic location. - **`variables::Vector{String}`**: List of meteorological variables to retrieve. - **`data_path::String`**: Directory path where data files are stored or will be saved. - **`filename_hint::String`**: Hint for naming the output file. -- **`reload::Bool`**: If true, reloads CSV data even if cached data exists (default: true). +- **`reload_csv::Bool`**: If true, reloads CSV data if available (default: true). - **`save_csv::Bool`**: If `true`, saves the retrieved data as a CSV file (default: true). - **`use_cache::Bool`**: If `true`, uses cached data if available (default: true). @@ -452,7 +462,7 @@ function get_met_data( variables::Vector{String}; data_path::String = "metocean_api_data", filename_hint::String = "", - reload::Bool = true, + reload_csv::Bool = true, save_csv::Bool = true, use_cache::Bool = true, ) @@ -469,7 +479,7 @@ function get_met_data( string(lon) * filehint * ".csv", ) - if reload && isfile(csv_path) && filesize(csv_path) > 0 + if reload_csv && isfile(csv_path) && filesize(csv_path) > 0 df = CSV.read( csv_path, DataFrame; diff --git a/test/utils.jl b/test/utils.jl index 9c328b3..23a84dc 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -269,9 +269,6 @@ function simple_graph_building(; cap_p = nothing, temp_to_demand; data_path = joinpath(pkgdir(EMLI), "test", "data", "building"), source = "NORA3", - reload = true, - save_csv = true, - use_cache = true, ) else building = Building( From 5ecff5cfe62d5dc917faec9557c2253f52f227a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Sat, 25 Apr 2026 15:28:59 +0200 Subject: [PATCH 08/20] Adress Copilot sixth review [skip ci] --- docs/src/library/public.md | 34 +++++++++++++-------------- src/utils.jl | 48 +++++++++++++++++++------------------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/docs/src/library/public.md b/docs/src/library/public.md index a0ec62c..44d5460 100644 --- a/docs/src/library/public.md +++ b/docs/src/library/public.md @@ -3,30 +3,30 @@ ## [New resource types](@id lib-pub-resource_types) ```@docs -EnergyModelsLanguageInterfaces.ResourceBio +EMLI.ResourceBio ``` ## [New parameter types](@id lib-pub-parameter_types) ```@docs -EnergyModelsLanguageInterfaces.PVParameters +EMLI.PVParameters ``` ## [New nodal types](@id lib-pub-nodal_types) ```@docs -EnergyModelsLanguageInterfaces.WindPower -EnergyModelsLanguageInterfaces.PV -EnergyModelsLanguageInterfaces.CSPandPV -EnergyModelsLanguageInterfaces.Building -EnergyModelsLanguageInterfaces.MultipleBuildingTypes -EnergyModelsLanguageInterfaces.BioCHP +EMLI.WindPower +EMLI.PV +EMLI.CSPandPV +EMLI.Building +EMLI.MultipleBuildingTypes +EMLI.BioCHP ``` ## [Sampling constructors](@id lib-pub-sampling_constructors) ```@docs -EnergyModelsLanguageInterfaces.WindPower( +EMLI.WindPower( ::Any, ::TimeStruct.TimeProfile, ::Dict, @@ -36,7 +36,7 @@ EnergyModelsLanguageInterfaces.WindPower( ::TimeStruct.TimeProfile, ::Dict{<:EnergyModelsBase.Resource,<:Real}, ) -EnergyModelsLanguageInterfaces.PV( +EMLI.PV( ::Any, ::TimeProfile, ::TimeProfile, @@ -49,7 +49,7 @@ EnergyModelsLanguageInterfaces.PV( data_path::String = "pvgis_cache", filename_hint::String = "", ) -EnergyModelsLanguageInterfaces.CSPandPV( +EMLI.CSPandPV( ::Any, ::Dict, ::DateTime, @@ -60,7 +60,7 @@ EnergyModelsLanguageInterfaces.CSPandPV( data_path::String = "", source::String = "NORA3", ) -EnergyModelsLanguageInterfaces.Building( +EMLI.Building( ::Any, ::Dict{<:Resource,<:TimeProfile}, ::Dict{<:Resource,<:TimeProfile}, @@ -79,7 +79,7 @@ EnergyModelsLanguageInterfaces.Building( save_csv::Bool = true, use_cache::Bool = true, ) -EnergyModelsLanguageInterfaces.MultipleBuildingTypes( +EMLI.MultipleBuildingTypes( ::Any, ::Dict, ::DateTime, @@ -93,10 +93,10 @@ EnergyModelsLanguageInterfaces.MultipleBuildingTypes( data_location::String = joinpath(tempdir(), "buildings"), overwrite_saved_data::Bool = false, ) -EnergyModelsLanguageInterfaces.BioCHP( +EMLI.BioCHP( ::Any, ::TimeStruct.TimeProfile, - ::Dict{<:EnergyModelsLanguageInterfaces.ResourceBio,<:Real}, + ::Dict{<:EMLI.ResourceBio,<:Real}, ::Dict{<:EnergyModelsHeat.ResourceHeat,<:Real}, ::EnergyModelsBase.Resource; data::Vector{<:ExtensionData} = ExtensionData[], @@ -107,6 +107,6 @@ EnergyModelsLanguageInterfaces.BioCHP( ## [Utility functions](@id lib-pub-util_fun) ```@docs -EnergyModelsLanguageInterfaces.call_python_function -EnergyModelsLanguageInterfaces.fetch_element +EMLI.call_python_function +EMLI.fetch_element ``` diff --git a/src/utils.jl b/src/utils.jl index c094027..b31ec71 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -337,8 +337,8 @@ end heat_demand_profile( time_start::DateTime, time_end::DateTime, - lat::Float64, - lon::Float64, + lat::Real, + lon::Real, temp_to_demand::Function; data_path::String = "metocean_api_data", filename_hint::String = "", @@ -359,23 +359,23 @@ The data source is queried using the [`get_met_data`](@ref) function, which hand caching, and storage. # Arguments -- **`time_start::DateTime`**: Start of the time period for the demand profile. -- **`time_end::DateTime`**: End of the time period for the demand profile. -- **`lat::Float64`**: Latitude of the location. -- **`lon::Float64`**: Longitude of the location. -- **`temp_to_demand::Function`**: Function mapping temperature in Kelvin to demand. -- **`data_path::String`**: Directory path to store or load temperature data (default: "metocean_api_data"). -- **`filename_hint::String`**: Optional hint for naming the data file (default: ""). -- **`source::String`**: Data source for temperature (default: "NORA3"). -- **`reload_csv::Bool`**: If true, reloads CSV data if available (default: true). -- **`save_csv::Bool`**: If true, saves the generated profile to a CSV file (default: true). -- **`use_cache::Bool`**: If true, uses cached data if available (default: true). +- **`time_start::DateTime`** is the start of the time period for the demand profile. +- **`time_end::DateTime`** is the end of the time period for the demand profile. +- **`lat::Real`** is the latitude of the location. +- **`lon::Real`** is the longitude of the location. +- **`temp_to_demand::Function`** is a function mapping temperature in Kelvin to demand. +- **`data_path::String`** is the directory path to store or load temperature data (default: "metocean_api_data"). +- **`filename_hint::String`** is an optional hint for naming the data file (default: ""). +- **`source::String`** is the data source for temperature (default: "NORA3"). +- **`reload_csv::Bool`** is a flag indicating whether to reload CSV data if available (default: true). +- **`save_csv::Bool`** is a flag indicating whether to save the generated profile to a CSV file (default: true). +- **`use_cache::Bool`** is a flag indicating whether to use cached data if available (default: true). """ function heat_demand_profile( time_start::DateTime, time_end::DateTime, - lat::Float64, - lon::Float64, + lat::Real, + lon::Real, temp_to_demand::Function; data_path::String = "metocean_api_data", filename_hint::String = "", @@ -406,9 +406,9 @@ function heat_demand_profile( save_csv, use_cache, ) - temperature_column = Symbol(variables[1]) - if !hasproperty(df, temperature_column) - error("Temperature column $(variables[1]) not found in meteorological data.") + temperature_column = variables[1] + if !(temperature_column in names(df)) + error("Temperature column $temperature_column not found in meteorological data.") end df.heat_demand = temp_to_demand.(df[!, temperature_column]) return df @@ -418,8 +418,8 @@ end get_met_data( time_start::DateTime, time_end::DateTime, - lat::Float64, - lon::Float64, + lat::Real, + lon::Real, product::String, variables::Vector{String}; data_path::String = "metocean_api_data", @@ -434,8 +434,8 @@ Fetches meteorological data for a specified time range and geographic location. # Arguments - **`time_start::DateTime`**: Start of the time range for data retrieval. - **`time_end::DateTime`**: End of the time range for data retrieval. -- **`lat::Float64`**: Latitude of the location. -- **`lon::Float64`**: Longitude of the location. +- **`lat::Real`**: Latitude of the location. +- **`lon::Real`**: Longitude of the location. - **`product::String`**: Name of the meteorological data product to use. - **`variables::Vector{String}`**: List of meteorological variables to retrieve. - **`data_path::String`**: Directory path where data files are stored or will be saved. @@ -456,8 +456,8 @@ Fetches meteorological data for a specified time range and geographic location. function get_met_data( time_start::DateTime, time_end::DateTime, - lat::Float64, - lon::Float64, + lat::Real, + lon::Real, product::String, variables::Vector{String}; data_path::String = "metocean_api_data", From 0f391e8d5dc3dce03fc435591c063270c30a10eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Sat, 25 Apr 2026 15:44:06 +0200 Subject: [PATCH 09/20] Retrigger CI From 925aa8657e4a2829edb45fca5edf5ee80df00d6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Fri, 8 May 2026 10:22:59 +0200 Subject: [PATCH 10/20] Fix PyCall deprecation warning --- src/utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index b31ec71..4d8d8ae 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -59,7 +59,7 @@ function get_python_function(module_name::String, function_name::String) sub_names = split(function_name, ".") python_function = pyimport(module_name) for name โˆˆ sub_names - python_function = python_function[name] + python_function = getproperty(python_function, name) end return python_function end From 548b85e8bb96cd4366f383e19656e06739ea8db6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Fri, 8 May 2026 12:06:13 +0200 Subject: [PATCH 11/20] Add info statement for debugging purposes --- test/test_buildings.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_buildings.jl b/test/test_buildings.jl index 20668d0..09fd614 100644 --- a/test/test_buildings.jl +++ b/test/test_buildings.jl @@ -140,6 +140,7 @@ end 18.03, ]) + @info [value.(m[:flow_out][sink, t, HeatHT]) for t โˆˆ ๐’ฏ] @test all( isapprox( value.(m[:flow_out][sink, t, HeatHT]), From 3dc31a9cc42fb0ad9816d458d7404382c218370a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Fri, 8 May 2026 13:31:25 +0200 Subject: [PATCH 12/20] Try to fix CI --- test/test_buildings.jl | 4 ++++ test/utils.jl | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/test_buildings.jl b/test/test_buildings.jl index 09fd614..bdf8a58 100644 --- a/test/test_buildings.jl +++ b/test/test_buildings.jl @@ -140,7 +140,11 @@ end 18.03, ]) + @info "Flow out of the sink for HeatHT:" @info [value.(m[:flow_out][sink, t, HeatHT]) for t โˆˆ ๐’ฏ] + + @info "Reference values for the sink flow:" + @info building.cap[HeatHT].vals @test all( isapprox( value.(m[:flow_out][sink, t, HeatHT]), diff --git a/test/utils.jl b/test/utils.jl index 23a84dc..957c276 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -241,7 +241,7 @@ function simple_graph_building(; cap_p = nothing, sources = [ RefSource( "Source for " * resource.id, - FixedProfile(150e6), + FixedProfile(1e4), FixedProfile(120), FixedProfile(0), Dict(resource => 1.0), From bad3ec105210febe9c5b109404a261552eade83c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Fri, 8 May 2026 14:12:21 +0200 Subject: [PATCH 13/20] Try to fix CI --- src/datastructures.jl | 2 ++ test/test_buildings.jl | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/datastructures.jl b/src/datastructures.jl index e656a5d..17cbb22 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -649,6 +649,8 @@ function Building( # Copy to ensure we don't modify the original `cap` dictionary and widen key/value types cap_ext = Dict{Resource,TimeProfile}(resource => profile for (resource, profile) โˆˆ cap) cap_ext[heat_resource] = OperationalProfile(df.heat_demand) + @info "Generated heat demand profile for resource $(heat_resource) based on temperature data from $(source)." + @info df.heat_demand return Building(id, cap_ext, penalty_surplus, penalty_deficit, input, data) end diff --git a/test/test_buildings.jl b/test/test_buildings.jl index bdf8a58..8cefe25 100644 --- a/test/test_buildings.jl +++ b/test/test_buildings.jl @@ -144,7 +144,7 @@ end @info [value.(m[:flow_out][sink, t, HeatHT]) for t โˆˆ ๐’ฏ] @info "Reference values for the sink flow:" - @info building.cap[HeatHT].vals + @info [building.cap[HeatHT][t] for t โˆˆ ๐’ฏ] @test all( isapprox( value.(m[:flow_out][sink, t, HeatHT]), From 87c045c66cb299d1a3b616db047269485a2641fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Fri, 8 May 2026 15:22:29 +0200 Subject: [PATCH 14/20] Try to fix CI --- .github/workflows/ci.yml | 18 ++++++++-------- test/test_buildings.jl | 46 ++++++++++++++++++++-------------------- test/utils.jl | 2 +- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86a955a..eee525e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,18 +14,18 @@ jobs: fail-fast: false matrix: include: - - version: '1' # The latest point-release (Linux) - os: ubuntu-latest - arch: x64 + #- version: '1' # The latest point-release (Linux) + # os: ubuntu-latest + # arch: x64 - version: 'lts' # The long term stable version (Linux) os: ubuntu-latest arch: x64 - - version: '1' - os: windows-latest - arch: x64 - - version: 'lts' - os: windows-latest - arch: x64 + #- version: '1' + # os: windows-latest + # arch: x64 + #- version: 'lts' + # os: windows-latest + # arch: x64 steps: - uses: actions/checkout@v4 with: diff --git a/test/test_buildings.jl b/test/test_buildings.jl index 8cefe25..7b40ab9 100644 --- a/test/test_buildings.jl +++ b/test/test_buildings.jl @@ -114,30 +114,30 @@ end # Test that the source data is the same ref_values = OperationalProfile([ - 15.0, - 15.61, - 16.37, - 16.45, - 16.78, - 17.70, - 17.80, - 17.91, - 17.90, - 17.0, - 14.91, - 14.02, - 14.01, - 14.45, - 14.91, - 15.01, - 15.87, - 16.67, - 16.58, - 17.04, - 17.57, - 17.26, 17.76, - 18.03, + 18.01, + 18.47, + 18.01, + 16.88, + 15.18, + 14.84, + 14.95, + 15.98, + 15.73, + 15.8, + 15.82, + 15.02, + 15.84, + 15.34, + 15.76, + 16.18, + 16.9, + 16.79, + 16.99, + 17.13, + 17.47, + 17.64, + 17.8 ]) @info "Flow out of the sink for HeatHT:" diff --git a/test/utils.jl b/test/utils.jl index 957c276..02109e5 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -251,7 +251,7 @@ function simple_graph_building(; cap_p = nothing, # Example temp_to_demand function (replace with your actual function) temp_to_demand(temp) = max(0, 20 - (temp - 273.15)) # Example location (replace with actual values or make it an argument) - lat, lon = 59.91, 10.75 # Oslo coordinates as example + lat, lon = 63.4378306,10.3984953 # Trondheim, Norway cap = Dict( resource => FixedProfile(120) for resource โˆˆ building_res if resource != HeatHT ) From 77dddcff9a50de8ea9adc87aa2a376e0798fe2b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Fri, 8 May 2026 16:06:50 +0200 Subject: [PATCH 15/20] Try to fix CI --- src/utils.jl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/utils.jl b/src/utils.jl index 4d8d8ae..a78fbdd 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -512,6 +512,13 @@ function get_met_data( for col โˆˆ colnames df[!, col] = Float64.(df[!, col]) end + + @info("PyCall python", PyCall.pyversion, PyCall.libpython) + md = pyimport("importlib.metadata") + + met = pyimport("metocean_api") + @info("metocean_api version", md.version("metocean-api")) + return df end end From 5e6a20494552919534337507877d5a401961640e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Mon, 11 May 2026 09:02:57 +0200 Subject: [PATCH 16/20] Test metocean-api==1.1.9 --- .github/actions/before-script/action.yml | 1 + test/runtests.jl | 24 ++++++------- test/test_buildings.jl | 46 ++++++++++++------------ test/utils.jl | 2 +- 4 files changed, 37 insertions(+), 36 deletions(-) diff --git a/.github/actions/before-script/action.yml b/.github/actions/before-script/action.yml index ea412e0..d698d5d 100644 --- a/.github/actions/before-script/action.yml +++ b/.github/actions/before-script/action.yml @@ -24,6 +24,7 @@ runs: cd "$GITHUB_WORKSPACE/submodules/wind_power_timeseries" poetry install + pip install metocean-api==1.1.14 cd "$GITHUB_WORKSPACE" cd "$GITHUB_WORKSPACE/submodules/Tecnalia_Solar-Energy-Model" diff --git a/test/runtests.jl b/test/runtests.jl index 39e7f03..9bed887 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -22,21 +22,21 @@ include(joinpath(testdir, "utils.jl")) @testset "EnergyModelsLanguageInterfaces" begin ## Run all Aqua tests - include(joinpath(testdir, "Aqua.jl")) + #include(joinpath(testdir, "Aqua.jl")) - ## Check if there is need for formatting - include(joinpath(testdir, "JuliaFormatter.jl")) + ### Check if there is need for formatting + #include(joinpath(testdir, "JuliaFormatter.jl")) - ## Test sampling routines - include(joinpath(testdir, "test_sampling_routines.jl")) + ### Test sampling routines + #include(joinpath(testdir, "test_sampling_routines.jl")) - ## Test checks - include(joinpath(testdir, "test_checks.jl")) + ### Test checks + #include(joinpath(testdir, "test_checks.jl")) - ## Test nodes - include(joinpath(testdir, "test_windpower.jl")) - include(joinpath(testdir, "test_PV.jl")) + ### Test nodes + #include(joinpath(testdir, "test_windpower.jl")) + #include(joinpath(testdir, "test_PV.jl")) include(joinpath(testdir, "test_buildings.jl")) - include(joinpath(testdir, "test_CSPandPV.jl")) - include(joinpath(testdir, "test_bioCHP.jl")) + #include(joinpath(testdir, "test_CSPandPV.jl")) + #include(joinpath(testdir, "test_bioCHP.jl")) end diff --git a/test/test_buildings.jl b/test/test_buildings.jl index 7b40ab9..accad34 100644 --- a/test/test_buildings.jl +++ b/test/test_buildings.jl @@ -114,30 +114,30 @@ end # Test that the source data is the same ref_values = OperationalProfile([ + 15.00, + 15.61, + 16.37, + 16.67, + 16.78, + 17.70, + 17.80, + 17.91, + 17.90, + 16.98, + 14.91, + 14.02, + 14.01, + 14.45, + 14.91, + 15.45, + 15.87, + 16.67, + 16.58, + 17.04, + 17.57, + 17.95, 17.76, - 18.01, - 18.47, - 18.01, - 16.88, - 15.18, - 14.84, - 14.95, - 15.98, - 15.73, - 15.8, - 15.82, - 15.02, - 15.84, - 15.34, - 15.76, - 16.18, - 16.9, - 16.79, - 16.99, - 17.13, - 17.47, - 17.64, - 17.8 + 18.03, ]) @info "Flow out of the sink for HeatHT:" diff --git a/test/utils.jl b/test/utils.jl index 02109e5..957c276 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -251,7 +251,7 @@ function simple_graph_building(; cap_p = nothing, # Example temp_to_demand function (replace with your actual function) temp_to_demand(temp) = max(0, 20 - (temp - 273.15)) # Example location (replace with actual values or make it an argument) - lat, lon = 63.4378306,10.3984953 # Trondheim, Norway + lat, lon = 59.91, 10.75 # Oslo coordinates as example cap = Dict( resource => FixedProfile(120) for resource โˆˆ building_res if resource != HeatHT ) From 24cb4da91fdbfcaa2e1269ed8170087f451899a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Mon, 11 May 2026 09:21:41 +0200 Subject: [PATCH 17/20] Run full CI --- .github/workflows/ci.yml | 18 +++++++++--------- src/datastructures.jl | 2 -- src/utils.jl | 6 ------ test/runtests.jl | 26 +++++++++++++------------- test/test_buildings.jl | 5 ----- 5 files changed, 22 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eee525e..86a955a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,18 +14,18 @@ jobs: fail-fast: false matrix: include: - #- version: '1' # The latest point-release (Linux) - # os: ubuntu-latest - # arch: x64 + - version: '1' # The latest point-release (Linux) + os: ubuntu-latest + arch: x64 - version: 'lts' # The long term stable version (Linux) os: ubuntu-latest arch: x64 - #- version: '1' - # os: windows-latest - # arch: x64 - #- version: 'lts' - # os: windows-latest - # arch: x64 + - version: '1' + os: windows-latest + arch: x64 + - version: 'lts' + os: windows-latest + arch: x64 steps: - uses: actions/checkout@v4 with: diff --git a/src/datastructures.jl b/src/datastructures.jl index 17cbb22..e656a5d 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -649,8 +649,6 @@ function Building( # Copy to ensure we don't modify the original `cap` dictionary and widen key/value types cap_ext = Dict{Resource,TimeProfile}(resource => profile for (resource, profile) โˆˆ cap) cap_ext[heat_resource] = OperationalProfile(df.heat_demand) - @info "Generated heat demand profile for resource $(heat_resource) based on temperature data from $(source)." - @info df.heat_demand return Building(id, cap_ext, penalty_surplus, penalty_deficit, input, data) end diff --git a/src/utils.jl b/src/utils.jl index a78fbdd..0c1728b 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -512,12 +512,6 @@ function get_met_data( for col โˆˆ colnames df[!, col] = Float64.(df[!, col]) end - - @info("PyCall python", PyCall.pyversion, PyCall.libpython) - md = pyimport("importlib.metadata") - - met = pyimport("metocean_api") - @info("metocean_api version", md.version("metocean-api")) return df end diff --git a/test/runtests.jl b/test/runtests.jl index 9bed887..31f72f3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -21,22 +21,22 @@ testdir = joinpath(pkg_dir, "test") include(joinpath(testdir, "utils.jl")) @testset "EnergyModelsLanguageInterfaces" begin - ## Run all Aqua tests - #include(joinpath(testdir, "Aqua.jl")) + # Run all Aqua tests + include(joinpath(testdir, "Aqua.jl")) - ### Check if there is need for formatting - #include(joinpath(testdir, "JuliaFormatter.jl")) + # Check if there is need for formatting + include(joinpath(testdir, "JuliaFormatter.jl")) - ### Test sampling routines - #include(joinpath(testdir, "test_sampling_routines.jl")) + # Test sampling routines + include(joinpath(testdir, "test_sampling_routines.jl")) - ### Test checks - #include(joinpath(testdir, "test_checks.jl")) + # Test checks + include(joinpath(testdir, "test_checks.jl")) - ### Test nodes - #include(joinpath(testdir, "test_windpower.jl")) - #include(joinpath(testdir, "test_PV.jl")) + # Test nodes + include(joinpath(testdir, "test_windpower.jl")) + include(joinpath(testdir, "test_PV.jl")) include(joinpath(testdir, "test_buildings.jl")) - #include(joinpath(testdir, "test_CSPandPV.jl")) - #include(joinpath(testdir, "test_bioCHP.jl")) + include(joinpath(testdir, "test_CSPandPV.jl")) + include(joinpath(testdir, "test_bioCHP.jl")) end diff --git a/test/test_buildings.jl b/test/test_buildings.jl index accad34..ebe35af 100644 --- a/test/test_buildings.jl +++ b/test/test_buildings.jl @@ -140,11 +140,6 @@ end 18.03, ]) - @info "Flow out of the sink for HeatHT:" - @info [value.(m[:flow_out][sink, t, HeatHT]) for t โˆˆ ๐’ฏ] - - @info "Reference values for the sink flow:" - @info [building.cap[HeatHT][t] for t โˆˆ ๐’ฏ] @test all( isapprox( value.(m[:flow_out][sink, t, HeatHT]), From da0799a1248028646d0e6d92971cdb10148c7150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Fri, 15 May 2026 11:38:32 +0200 Subject: [PATCH 18/20] Apply suggestions from code review Co-authored-by: Julian Straus <104911227+JulStraus@users.noreply.github.com> --- docs/src/nodes/multiplebuildingtypes.md | 4 ++-- src/utils.jl | 14 ++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/docs/src/nodes/multiplebuildingtypes.md b/docs/src/nodes/multiplebuildingtypes.md index cb95a9c..8a89915 100644 --- a/docs/src/nodes/multiplebuildingtypes.md +++ b/docs/src/nodes/multiplebuildingtypes.md @@ -1,6 +1,6 @@ # [Building nodes](@id nodes-AbstractBuildings) -The [`EMLI.AbstractBuildings`](@ref) node types create sinks for all demand resources with penalties for both surplus and deficit. +The [`AbstractBuildings`](@ref EMLI.AbstractBuildings) node types create sinks for all demand resources with penalties for both surplus and deficit. The implementation uses `Dict` structures for the fields `cap`, `penalty_surplus`, and `penalty_deficit` to facilitate multiple [Resource](@extref EnergyModelsBase.Resource)s. This approach allows modeling building demands with flexible penalty mechanisms for over- and under-supply. @@ -31,7 +31,7 @@ the temperature at a single location and a user-defined temperature-to-demand fu ## [Introduced type and its field](@id nodes-AbstractBuildings-fields) -[`EMLI.AbstractBuildings`](@ref) nodes are a subtype of [`Sink`](@extref EnergyModelsBase.Sink) and are implemented as specialized sink nodes. +[`EMLI.AbstractBuildings`](@ref) nodes are subtypes of [`Sink`](@extref EnergyModelsBase.Sink), and hence, correspond to specialized sink nodes. Hence, they utilize the same functions declared in `EnergyModelsBase`. ### [Standard fields](@id nodes-AbstractBuildings-fields-stand) diff --git a/src/utils.jl b/src/utils.jl index 0c1728b..aeaa5c3 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -444,14 +444,12 @@ Fetches meteorological data for a specified time range and geographic location. - **`save_csv::Bool`**: If `true`, saves the retrieved data as a CSV file (default: true). - **`use_cache::Bool`**: If `true`, uses cached data if available (default: true). -# Notes -- The function may download data from remote sources if not available locally. -- If `save_csv` is enabled, the data will be saved to a CSV file in the specified `data_path`. -- Caching behavior is controlled by the `use_cache` parameter. - -!!! note "Usage of the ERA5 data source" - For use of the "ERA5" data source, the user needs to register and obtain a CDS API key. - - Perform step 1: https://cds.climate.copernicus.eu/how-to-api +!!! note "Usage of the function" + * The function may download data from remote sources if not available locally. + * If `save_csv` is enabled, the data will be saved to a CSV file in the specified `data_path`. + * Caching behavior is controlled by the `use_cache` parameter. + * For use of the "ERA5" data source, the user needs to register and obtain a CDS API key. + This can be achieved by performing step 1: https://cds.climate.copernicus.eu/how-to-api """ function get_met_data( time_start::DateTime, From e176ccc753bda9f8e298190c1077dd28d9c87dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Fri, 15 May 2026 14:12:38 +0200 Subject: [PATCH 19/20] Add suggestions from review --- .github/workflows/ci.yml | 6 + docs/src/nodes/multiplebuildingtypes.md | 55 ++-- src/datastructures.jl | 4 - src/utils.jl | 29 +- test/JuliaFormatter.jl | 6 +- test/runtests.jl | 3 + test/test_buildings.jl | 348 ++++++++++++++---------- test/test_checks.jl | 5 + test/test_utils.jl | 72 +++++ test/utils.jl | 19 +- 10 files changed, 353 insertions(+), 194 deletions(-) create mode 100644 test/test_utils.jl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86a955a..c308792 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,9 @@ jobs: Add-Content $env:GITHUB_ENV "SSL_CERT_FILE=$ca" - name: Run Julia tests inside the testenv-enviornment in Miniconda (Linux) if: runner.os == 'Linux' + env: + CDSAPI_URL: https://cds.climate.copernicus.eu/api + CDSAPI_KEY: ${{ secrets.CDSAPI_KEY }} run: | source "$HOME/miniconda/etc/profile.d/conda.sh" conda init bash @@ -75,6 +78,9 @@ jobs: shell: bash - name: Run Julia tests inside the testenv-environment in Miniconda (Windows) if: runner.os == 'Windows' + env: + CDSAPI_URL: https://cds.climate.copernicus.eu/api + CDSAPI_KEY: ${{ secrets.CDSAPI_KEY }} run: | $conda = Join-Path $env:USERPROFILE "miniconda\Scripts\conda.exe" & $conda "shell.powershell" "hook" | Out-String | Invoke-Expression diff --git a/docs/src/nodes/multiplebuildingtypes.md b/docs/src/nodes/multiplebuildingtypes.md index 8a89915..47fb4c8 100644 --- a/docs/src/nodes/multiplebuildingtypes.md +++ b/docs/src/nodes/multiplebuildingtypes.md @@ -1,42 +1,38 @@ # [Building nodes](@id nodes-AbstractBuildings) The [`AbstractBuildings`](@ref EMLI.AbstractBuildings) node types create sinks for all demand resources with penalties for both surplus and deficit. -The implementation uses `Dict` structures for the fields `cap`, `penalty_surplus`, and `penalty_deficit` to facilitate -multiple [Resource](@extref EnergyModelsBase.Resource)s. +The implementation uses `Dict` structures for the fields `cap`, `penalty_surplus`, and `penalty_deficit` to facilitate multiple [Resource](@extref EnergyModelsBase.Resource)s. This approach allows modeling building demands with flexible penalty mechanisms for over- and under-supply. -The subtype [`MultipleBuildingTypes`](@ref) is used to enable a specialized constructor that samples -the [`Tecnalia_Building-Stock-Energy-Model`](https://github.com/iDesignRES/Tecnalia_Building-Stock-Energy-Model) module. +!!! danger + Investments are currently not available for these nodes. + +## [Introduced types and their fields](@id nodes-AbstractBuildings-fields) -The subtype [`Building`](@ref) can be used to model a simple building demand where the heat demand is calculated from -the temperature at a single location and a user-defined temperature-to-demand function. See the [constructor](@ref lib-pub-sampling_constructors) for more details. +[AbstractBuildings](@ref EMLI.AbstractBuildings) nodes are subtypes of [`Sink`](@extref EnergyModelsBase.Sink), and hence, correspond to specialized sink nodes. + +The subtype [`MultipleBuildingTypes`](@ref) is used to enable a specialized constructor that samples the [`Tecnalia_Building-Stock-Energy-Model`](https://github.com/iDesignRES/Tecnalia_Building-Stock-Energy-Model) module. + +The subtype [`Building`](@ref) can be used to model a simple building demand where the heat demand is calculated from the temperature at a single location and a user-defined temperature-to-demand function. +See the [constructor](@ref lib-pub-sampling_constructors) for more details. !!! note "Sampling Tecnalia_Building-Stock-Energy-Model module" To use the [constructor](@ref lib-pub-sampling_constructors) for [`MultipleBuildingTypes`](@ref) that samples the [`Tecnalia_Building-Stock-Energy-Model`](https://github.com/iDesignRES/Tecnalia_Building-Stock-Energy-Model) module, follow the installation in the [Use nodes](@ref how_to-utilize-use_nodes) section. !!! note "Using the sampling constructor for `Building` nodes" - The sampling [constructor](@ref lib-pub-sampling_constructors) for [`Building`](@ref) node requires the installation of the python package `metocean_api` which can be done with + The sampling [constructor](@ref lib-pub-sampling_constructors) for [`Building`](@ref) node requires the installation of the python package `metocean_api` which can be done with (there is a breaking change in version 1.1.14 which is currently not available in Conda, so we recommend installing it with pip): ```bash - pip install metocean_api + pip install metocean-api ``` Enable this package in julia by following the instructions in the [Enable python modules](@ref how_to-utilize-use_nodes-enable_python_modules) section. Note that this package is already available if the module wind_power_timeseries is installed as described in the [Install python modules](@ref how_to-utilize-use_nodes) section. - -!!! danger - Investments are currently not available for these nodes. - -## [Introduced type and its field](@id nodes-AbstractBuildings-fields) - -[`EMLI.AbstractBuildings`](@ref) nodes are subtypes of [`Sink`](@extref EnergyModelsBase.Sink), and hence, correspond to specialized sink nodes. -Hence, they utilize the same functions declared in `EnergyModelsBase`. - ### [Standard fields](@id nodes-AbstractBuildings-fields-stand) -Standard fields of [`EMLI.AbstractBuildings`](@ref) nodes are given as: +Standard fields of [AbstractBuildings](@ref EMLI.AbstractBuildings) nodes are given as: - **`id`**:\ The field `id` is only used for providing a name to the node. @@ -46,9 +42,10 @@ Standard fields of [`EMLI.AbstractBuildings`](@ref) nodes are given as: All values have to be non-negative. - **`data::Vector{<:ExtensionData}`**:\ An entry for providing additional data to the model. - In the current version, it is not applicable. We intend to change this in future releases to enable investments. + In the current version, it is not applicable. + We intend to change this in future releases to enable investments. - !!! note "Constructor for `EMLI.AbstractBuildings` nodes" + !!! note "Constructor for `AbstractBuildings` nodes" The field `data` is not required as we include a constructor when the value is excluded. !!! danger "Using `CaptureData`" @@ -57,7 +54,7 @@ Standard fields of [`EMLI.AbstractBuildings`](@ref) nodes are given as: ### [Additional fields](@id nodes-AbstractBuildings-fields-new) -[`EMLI.AbstractBuildings`](@ref) nodes introduce additional fields for demand and penalty specifications: +[AbstractBuildings](@ref EMLI.AbstractBuildings) nodes introduce additional fields for demand and penalty specifications: - **`cap::Dict{<:Resource,<:TimeProfile}`**:\ The demand capacity for each of the input resources. @@ -88,7 +85,7 @@ with parentheses. #### [Standard variables](@id nodes-AbstractBuildings-math-var-stand) -[`EMLI.AbstractBuildings`](@ref) nodes utilize standard variables from the [`Sink`](@extref EnergyModelsBase.Sink) node type and includes: +[AbstractBuildings](@ref EMLI.AbstractBuildings) nodes utilize standard variables from the [`Sink`](@extref EnergyModelsBase.Sink) node type and includes: - [``\texttt{opex\_var}``](@extref EnergyModelsBase man-opt_var-opex) - [``\texttt{opex\_fixed}``](@extref EnergyModelsBase man-opt_var-opex) @@ -98,12 +95,12 @@ with parentheses. - [``\texttt{emissions\_node}``](@extref EnergyModelsBase man-opt_var-emissions) if `EmissionsData` is added to the field `data` !!! note "cap\_use and cap\_inst" - [`EMLI.AbstractBuildings`](@ref) nodes have an individual capacity for all their resources, that is each `Resource` has its own capacity which must be satisfied. - As a consequence, the standard variables [``\texttt{cap\_use}``](@extref EnergyModelsBase man-opt_var-cap) and [``\texttt{cap\_inst}``](@extref EnergyModelsBase man-opt_var-cap) are not defined for [`EMLI.AbstractBuildings`](@ref) nodes through a new method for the function [`has_capacity`](@ref EnergyModelsBase.capacity). + [AbstractBuildings](@ref EMLI.AbstractBuildings) nodes have an individual capacity for all their resources, that is each `Resource` has its own capacity which must be satisfied. + As a consequence, the standard variables [``\texttt{cap\_use}``](@extref EnergyModelsBase man-opt_var-cap) and [``\texttt{cap\_inst}``](@extref EnergyModelsBase man-opt_var-cap) are not defined for [AbstractBuildings](@ref EMLI.AbstractBuildings) nodes through a new method for the function [`has_capacity`](@ref EnergyModelsBase.capacity). #### [Additional variables](@id nodes-AbstractBuildings-math-add) -[`EMLI.AbstractBuildings`](@ref) nodes introduce the following variables: +[AbstractBuildings](@ref EMLI.AbstractBuildings) nodes introduce the following variables: - ``\texttt{buildings\_surplus}[n, t, p]``: Surplus (over-supply) for node ``n`` in operational period ``t`` for resource ``p``. - ``\texttt{buildings\_deficit}[n, t, p]``: Deficit (under-supply) for node ``n`` in operational period ``t`` for resource ``p``. @@ -112,14 +109,14 @@ with parentheses. ### [Constraints](@id nodes-AbstractBuildings-math-con) -The following sections omit the direct inclusion of the vector of [`EMLI.AbstractBuildings`](@ref) nodes. -Instead, it is implicitly assumed that the constraints are valid ``\forall n โˆˆ N^{\text{EMLI.AbstractBuildings}}`` if not stated differently. +The following sections omit the direct inclusion of the vector of [AbstractBuildings](@ref EMLI.AbstractBuildings) nodes. +Instead, it is implicitly assumed that the constraints are valid ``\forall n โˆˆ N^{\text{AbstractBuildings}}`` if not stated differently. In addition, all constraints are valid ``\forall t \in T`` (that is in all operational periods) or ``\forall t_{inv} \in T^{Inv}`` (that is in all strategic periods). Finally, all constraints are valid ``\forall p \in inputs(n)`` (that is in all input resources). #### [Standard constraints](@id nodes-AbstractBuildings-math-con-stand) -[`EMLI.AbstractBuildings`](@ref) nodes utilize the following standard constraint functions: +[AbstractBuildings](@ref EMLI.AbstractBuildings) nodes utilize the following standard constraint functions: - `constraints_opex_fixed`:\ The current implementation fixes the fixed operating expenses of a sink to 0. @@ -169,4 +166,4 @@ The function `constraints_opex_var` is extended with a new method to allow for i #### [Additional constraints](@id nodes-AbstractBuildings-math-con-add) -[`EMLI.AbstractBuildings`](@ref) nodes do not add additional constraints. +[AbstractBuildings](@ref EMLI.AbstractBuildings) nodes do not add additional constraints. diff --git a/src/datastructures.jl b/src/datastructures.jl index e656a5d..90caa50 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -581,7 +581,6 @@ end source::String = "NORA3", reload_csv::Bool = true, save_csv::Bool = true, - use_cache::Bool = true, ) Constructs a [`Building`](@ref) instance where the heat demand profile is generated from temperature data @@ -608,7 +607,6 @@ The temperature-to-demand mapping is provided by `temp_to_demand`. - **`source::String`** is the data source, e.g., "NORA3" or "ERA5". - **`reload_csv::Bool`** is a boolean flag to reload data from local CSV files if available (default: true). - **`save_csv::Bool`** is a boolean flag to save data to CSV files. -- **`use_cache::Bool`** is a boolean flag to use local cache. """ function Building( id::Any, @@ -627,7 +625,6 @@ function Building( source::String = "NORA3", reload_csv::Bool = true, save_csv::Bool = true, - use_cache::Bool = true, ) df = heat_demand_profile( time_start, @@ -639,7 +636,6 @@ function Building( source = source, reload_csv = reload_csv, save_csv = save_csv, - use_cache = use_cache, ) if heat_resource โˆˆ keys(cap) @warn "The provided capacity dictionary already contains a profile for the `heat_resource`. " * diff --git a/src/utils.jl b/src/utils.jl index aeaa5c3..5a1f800 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -345,7 +345,6 @@ end source::String = "NORA3", reload_csv::Bool = true, save_csv::Bool = true, - use_cache::Bool = true, ) Generates a heat demand profile for a specified location and time period using temperature data @@ -369,7 +368,6 @@ caching, and storage. - **`source::String`** is the data source for temperature (default: "NORA3"). - **`reload_csv::Bool`** is a flag indicating whether to reload CSV data if available (default: true). - **`save_csv::Bool`** is a flag indicating whether to save the generated profile to a CSV file (default: true). -- **`use_cache::Bool`** is a flag indicating whether to use cached data if available (default: true). """ function heat_demand_profile( time_start::DateTime, @@ -382,7 +380,6 @@ function heat_demand_profile( source::String = "NORA3", reload_csv::Bool = true, save_csv::Bool = true, - use_cache::Bool = true, ) if source == "NORA3" product = "NORA3_atm_sub" @@ -391,7 +388,11 @@ function heat_demand_profile( product = "ERA5" variables = ["2m_temperature"] else - error("Unsupported data source: $source. Use 'NORA3' or 'ERA5'.") + throw( + ArgumentError( + "Unsupported data source: $source. Supported data sources are 'NORA3' or 'ERA5'.", + ), + ) end df = get_met_data( time_start, @@ -404,13 +405,8 @@ function heat_demand_profile( filename_hint, reload_csv, save_csv, - use_cache, ) - temperature_column = variables[1] - if !(temperature_column in names(df)) - error("Temperature column $temperature_column not found in meteorological data.") - end - df.heat_demand = temp_to_demand.(df[!, temperature_column]) + df.heat_demand = temp_to_demand.(df[!, variables[1]]) return df end @@ -426,7 +422,6 @@ end filename_hint::String = "", reload_csv::Bool = true, save_csv::Bool = true, - use_cache::Bool = true, ) Fetches meteorological data for a specified time range and geographic location. @@ -442,12 +437,10 @@ Fetches meteorological data for a specified time range and geographic location. - **`filename_hint::String`**: Hint for naming the output file. - **`reload_csv::Bool`**: If true, reloads CSV data if available (default: true). - **`save_csv::Bool`**: If `true`, saves the retrieved data as a CSV file (default: true). -- **`use_cache::Bool`**: If `true`, uses cached data if available (default: true). !!! note "Usage of the function" * The function may download data from remote sources if not available locally. * If `save_csv` is enabled, the data will be saved to a CSV file in the specified `data_path`. - * Caching behavior is controlled by the `use_cache` parameter. * For use of the "ERA5" data source, the user needs to register and obtain a CDS API key. This can be achieved by performing step 1: https://cds.climate.copernicus.eu/how-to-api """ @@ -462,7 +455,6 @@ function get_met_data( filename_hint::String = "", reload_csv::Bool = true, save_csv::Bool = true, - use_cache::Bool = true, ) # Ensure the cache directory exists isdir(data_path) || mkpath(data_path) @@ -486,7 +478,6 @@ function get_met_data( types = Dict(:time => DateTime), ) rename!(df, :time => :time_utc) - return df else ts = pyimport("metocean_api.ts") ts_data = ts.TimeSeries( @@ -499,7 +490,7 @@ function get_met_data( datafile = nothing, ) ts_data.datafile = csv_path - ts_data.import_data(save_csv = save_csv, save_nc = false, use_cache = use_cache) + ts_data.import_data(save_csv = save_csv, save_nc = false, use_cache = true) idx_np = ts_data.data.index.to_numpy(copy = true) time = DateTime(1970, 1, 1) .+ Nanosecond.(idx_np.astype("int64")) data = ts_data.data.to_numpy(copy = true) @@ -510,7 +501,9 @@ function get_met_data( for col โˆˆ colnames df[!, col] = Float64.(df[!, col]) end - - return df end + if product == "ERA5" + rename!(df, "t2m" => "2m_temperature") + end + return df end diff --git a/test/JuliaFormatter.jl b/test/JuliaFormatter.jl index 8fd04a5..8348dd7 100644 --- a/test/JuliaFormatter.jl +++ b/test/JuliaFormatter.jl @@ -1,7 +1,7 @@ using JuliaFormatter @testset "JuliaFormatter.jl" begin - @test format(joinpath(@__DIR__, "..", "src")) - @test format(joinpath(@__DIR__, "..", "test")) - @test format(joinpath(@__DIR__, "..", "ext")) + @test format(joinpath(testdir, "..", "src")) + @test format(joinpath(testdir, "..", "test")) + @test format(joinpath(testdir, "..", "ext")) end diff --git a/test/runtests.jl b/test/runtests.jl index 31f72f3..19dca59 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -33,6 +33,9 @@ include(joinpath(testdir, "utils.jl")) # Test checks include(joinpath(testdir, "test_checks.jl")) + # Test utils + include(joinpath(testdir, "test_utils.jl")) + # Test nodes include(joinpath(testdir, "test_windpower.jl")) include(joinpath(testdir, "test_PV.jl")) diff --git a/test/test_buildings.jl b/test/test_buildings.jl index ebe35af..f55fa87 100644 --- a/test/test_buildings.jl +++ b/test/test_buildings.jl @@ -1,159 +1,231 @@ @testset "MultipleBuildingTypes" begin - for _ โˆˆ 1:2 # Run the test two times to also test running from stored files (from first run) - case, modeltype = simple_graph_buildings() + @testset "Utilities" begin + # Create the general data for the building node + ๐’ฏ = TwoLevel(2, 1, SimpleTimes(4, 1)) + heat = ResourceCarrier("Heat", 0.0) + power = ResourceCarrier("Power", 0.0) + building_node = MultipleBuildingTypes( + "Building", + Dict(heat => FixedProfile(100), power => FixedProfile(50)), + Dict(heat => FixedProfile(10), power => FixedProfile(5)), + Dict(heat => FixedProfile(20), power => FixedProfile(10)), + Dict(heat => 1.0, power => 0.5), + ) - buildings = get_node(case, "Buildings") # The MultipleBuildingTypes node - products = get_products(case) - building_res = products[1:(end-1)] # All resources except CO2 - CO2 = products[end] # The CO2 resource + # Test the EMB utility functions + @test EMB.capacity(building_node) == + Dict(heat => FixedProfile(100), power => FixedProfile(50)) + @test EMB.capacity(building_node, heat) == FixedProfile(100) + @test EMB.capacity(building_node, power) == FixedProfile(50) + @test EMB.capacity(building_node, first(๐’ฏ), heat) == 100 + @test EMB.capacity(building_node, first(๐’ฏ), power) == 50 + @test EMB.surplus_penalty(building_node) == + Dict(heat => FixedProfile(10), power => FixedProfile(5)) + @test EMB.surplus_penalty(building_node, heat) == FixedProfile(10) + @test EMB.surplus_penalty(building_node, power) == FixedProfile(5) + @test EMB.surplus_penalty(building_node, first(๐’ฏ), heat) == 10 + @test EMB.surplus_penalty(building_node, first(๐’ฏ), power) == 5 + @test EMB.deficit_penalty(building_node) == + Dict(heat => FixedProfile(20), power => FixedProfile(10)) + @test EMB.deficit_penalty(building_node, heat) == FixedProfile(20) + @test EMB.deficit_penalty(building_node, power) == FixedProfile(10) + @test EMB.deficit_penalty(building_node, first(๐’ฏ), heat) == 20 + @test EMB.deficit_penalty(building_node, first(๐’ฏ), power) == 10 + @test heat โˆˆ EMB.inputs(building_node) + @test power โˆˆ EMB.inputs(building_node) + @test EMB.has_capacity(building_node) == false + end - # Run the model - m = EMB.run_model(case, modeltype, OPTIMIZER) + @testset "Mathematical formulation" begin + for _ โˆˆ 1:2 # Run the test two times to also test running from stored files (from first run) + case, modeltype = simple_graph_buildings() - # Extraction of the time structure - ๐’ฏ = get_time_struct(case) + buildings = get_node(case, "Buildings") # The MultipleBuildingTypes node + products = get_products(case) + building_res = products[1:(end-1)] # All resources except CO2 + CO2 = products[end] # The CO2 resource - # Run of the general tests - general_tests(m) + # Run the model + m = EMB.run_model(case, modeltype, OPTIMIZER) - @test all( - value.(m[:buildings_surplus][buildings, t, p]) == 0.0 for - t โˆˆ ๐’ฏ, p โˆˆ building_res - ) - @test all( - value.(m[:buildings_deficit][buildings, t, p]) == 0.0 for - t โˆˆ ๐’ฏ, p โˆˆ building_res - ) - @test all(value.(m[:emissions_total][t, CO2]) > 1e3 for t โˆˆ ๐’ฏ) + # Extraction of the time structure + ๐’ฏ = get_time_struct(case) - # Test that the EMB function has_capacity is false for the MultipleBuildingTypes node. - @test !EMB.has_capacity(buildings) + # Run of the general tests + general_tests(m) - # Test constraints from EMB.constraints_capacity - @test all( - value.(m[:flow_in][buildings, t, p]) / inputs(buildings, p) + - value.(m[:buildings_deficit][buildings, t, p]) == - EMB.capacity(buildings, t, p) + value.(m[:buildings_surplus][buildings, t, p]) - for t โˆˆ ๐’ฏ, p โˆˆ inputs(buildings) - ) + @test all( + value.(m[:buildings_surplus][buildings, t, p]) == 0.0 for + t โˆˆ ๐’ฏ, p โˆˆ building_res + ) + @test all( + value.(m[:buildings_deficit][buildings, t, p]) == 0.0 for + t โˆˆ ๐’ฏ, p โˆˆ building_res + ) + @test all(value.(m[:emissions_total][t, CO2]) > 1e3 for t โˆˆ ๐’ฏ) + + # Test that the EMB function has_capacity is false for the MultipleBuildingTypes node. + @test !EMB.has_capacity(buildings) + + # Test constraints from EMB.constraints_capacity + @test all( + value.(m[:flow_in][buildings, t, p]) / inputs(buildings, p) + + value.(m[:buildings_deficit][buildings, t, p]) == + EMB.capacity(buildings, t, p) + + value.(m[:buildings_surplus][buildings, t, p]) + for t โˆˆ ๐’ฏ, p โˆˆ inputs(buildings) + ) - @test all( - sum(value.(m[:buildings_deficit][buildings, t, p]) for p โˆˆ inputs(buildings)) == - value.(m[:sink_deficit][buildings, t]) - for t โˆˆ ๐’ฏ - ) + @test all( + sum( + value.(m[:buildings_deficit][buildings, t, p]) for p โˆˆ inputs(buildings) + ) == + value.(m[:sink_deficit][buildings, t]) + for t โˆˆ ๐’ฏ + ) - @test all( - sum(value.(m[:buildings_surplus][buildings, t, p]) for p โˆˆ inputs(buildings)) == - value.(m[:sink_surplus][buildings, t]) - for t โˆˆ ๐’ฏ - ) + @test all( + sum( + value.(m[:buildings_surplus][buildings, t, p]) for p โˆˆ inputs(buildings) + ) == + value.(m[:sink_surplus][buildings, t]) + for t โˆˆ ๐’ฏ + ) - # Test constraints from EMB.constraints_opex_var - ๐’ฏแดตโฟแต› = strategic_periods(๐’ฏ) - @test all( - value.(m[:opex_var][buildings, t_inv]) == - sum( - ( - value.(m[:buildings_surplus][buildings, t, p]) * - EMB.surplus_penalty(buildings, t, p) + - value.(m[:buildings_deficit][buildings, t, p]) * - EMB.deficit_penalty(buildings, t, p) - ) * scale_op_sp(t_inv, t) for t โˆˆ t_inv, p โˆˆ inputs(buildings) + # Test constraints from EMB.constraints_opex_var + ๐’ฏแดตโฟแต› = strategic_periods(๐’ฏ) + @test all( + value.(m[:opex_var][buildings, t_inv]) == + sum( + ( + value.(m[:buildings_surplus][buildings, t, p]) * + EMB.surplus_penalty(buildings, t, p) + + value.(m[:buildings_deficit][buildings, t, p]) * + EMB.deficit_penalty(buildings, t, p) + ) * scale_op_sp(t_inv, t) for t โˆˆ t_inv, p โˆˆ inputs(buildings) + ) + for t_inv โˆˆ ๐’ฏแดตโฟแต› ) - for t_inv โˆˆ ๐’ฏแดตโฟแต› - ) - # Test the utility functions for MultipleBuildingTypes - for p โˆˆ building_res - # Capacity - @test EMB.capacity(buildings) isa Dict - @test EMB.capacity(buildings, p) isa TimeProfile - @test EMB.capacity(buildings)[p] == EMB.capacity(buildings, p) - @test EMB.capacity(buildings, first(๐’ฏ), p) == - EMB.capacity(buildings, p)[first(๐’ฏ)] - - # Surplus penalty - @test EMB.surplus_penalty(buildings) isa Dict - @test EMB.surplus_penalty(buildings, p) isa TimeProfile - @test EMB.surplus_penalty(buildings)[p] == EMB.surplus_penalty(buildings, p) - @test EMB.surplus_penalty(buildings, first(๐’ฏ), p) == - EMB.surplus_penalty(buildings, p)[first(๐’ฏ)] - - # Deficit penalty - @test EMB.deficit_penalty(buildings) isa Dict - @test EMB.deficit_penalty(buildings, p) isa TimeProfile - @test EMB.deficit_penalty(buildings)[p] == EMB.deficit_penalty(buildings, p) - @test EMB.deficit_penalty(buildings, first(๐’ฏ), p) == - EMB.deficit_penalty(buildings, p)[first(๐’ฏ)] + # Test the utility functions for MultipleBuildingTypes + for p โˆˆ building_res + # Capacity + @test EMB.capacity(buildings) isa Dict + @test EMB.capacity(buildings, p) isa TimeProfile + @test EMB.capacity(buildings)[p] == EMB.capacity(buildings, p) + @test EMB.capacity(buildings, first(๐’ฏ), p) == + EMB.capacity(buildings, p)[first(๐’ฏ)] + + # Surplus penalty + @test EMB.surplus_penalty(buildings) isa Dict + @test EMB.surplus_penalty(buildings, p) isa TimeProfile + @test EMB.surplus_penalty(buildings)[p] == EMB.surplus_penalty(buildings, p) + @test EMB.surplus_penalty(buildings, first(๐’ฏ), p) == + EMB.surplus_penalty(buildings, p)[first(๐’ฏ)] + + # Deficit penalty + @test EMB.deficit_penalty(buildings) isa Dict + @test EMB.deficit_penalty(buildings, p) isa TimeProfile + @test EMB.deficit_penalty(buildings)[p] == EMB.deficit_penalty(buildings, p) + @test EMB.deficit_penalty(buildings, first(๐’ฏ), p) == + EMB.deficit_penalty(buildings, p)[first(๐’ฏ)] + end + + # Test has_capacity utility function + @test EMB.has_capacity(buildings) == false end - - # Test has_capacity utility function - @test EMB.has_capacity(buildings) == false end end @testset "Building" begin - for _ โˆˆ 1:2 # Run the test two times to also test running from stored files (from first run) - case, modeltype, m = simple_graph_building() - - # Run the model - m = EMB.run_model(case, modeltype, OPTIMIZER) - - building = get_node(case, "Building") - sink = get_node(case, "Source for HeatHT") - products = get_products(case) - building_res = products[1:(end-1)] # All resources except CO2 - HeatHT = fetch_element(products, "HeatHT") - - # Extraction of the time structure - ๐’ฏ = get_time_struct(case) - - # Run of the general tests - general_tests(m) - - # Test that the source data is the same - ref_values = OperationalProfile([ - 15.00, - 15.61, - 16.37, - 16.67, - 16.78, - 17.70, - 17.80, - 17.91, - 17.90, - 16.98, - 14.91, - 14.02, - 14.01, - 14.45, - 14.91, - 15.45, - 15.87, - 16.67, - 16.58, - 17.04, - 17.57, - 17.95, - 17.76, - 18.03, - ]) - - @test all( - isapprox( - value.(m[:flow_out][sink, t, HeatHT]), - ref_values[t]; - atol = TEST_ATOL, - ) for t โˆˆ ๐’ฏ - ) - @test all( - value.(m[:buildings_surplus][building, t, p]) == 0.0 for - t โˆˆ ๐’ฏ, p โˆˆ building_res - ) - @test all( - value.(m[:buildings_deficit][building, t, p]) == 0.0 for - t โˆˆ ๐’ฏ, p โˆˆ building_res + @testset "Utilities" begin + # Create the general data for the building node + ๐’ฏ = TwoLevel(2, 1, SimpleTimes(4, 1)) + heat = ResourceCarrier("Heat", 0.0) + building_node = Building( + "Building", + Dict(heat => FixedProfile(100)), + Dict(heat => FixedProfile(10)), + Dict(heat => FixedProfile(20)), + Dict(heat => 1.0), ) + + # Test the EMB utility functions + @test EMB.capacity(building_node) == Dict(heat => FixedProfile(100)) + @test EMB.capacity(building_node, heat) == FixedProfile(100) + @test EMB.capacity(building_node, first(๐’ฏ), heat) == 100 + @test EMB.surplus_penalty(building_node) == Dict(heat => FixedProfile(10)) + @test EMB.surplus_penalty(building_node, heat) == FixedProfile(10) + @test EMB.surplus_penalty(building_node, first(๐’ฏ), heat) == 10 + @test EMB.deficit_penalty(building_node) == Dict(heat => FixedProfile(20)) + @test EMB.deficit_penalty(building_node, heat) == FixedProfile(20) + @test EMB.deficit_penalty(building_node, first(๐’ฏ), heat) == 20 + @test EMB.inputs(building_node) == [heat] + @test EMB.has_capacity(building_node) == false + end + + @testset "Mathematical formulation" begin + for _ โˆˆ 1:2 # Run the test two times to also test running from stored files (from first run) + case, modeltype, m = simple_graph_building() + + # Run the model + m = EMB.run_model(case, modeltype, OPTIMIZER) + + building = get_node(case, "Building") + sink = get_node(case, "Source for HeatHT") + products = get_products(case) + building_res = products[1:(end-1)] # All resources except CO2 + HeatHT = fetch_element(products, "HeatHT") + + # Extraction of the time structure + ๐’ฏ = get_time_struct(case) + + # Run of the general tests + general_tests(m) + + # Test that the source data is the same + ref_values = OperationalProfile([ + 15.00, + 15.61, + 16.37, + 16.67, + 16.78, + 17.70, + 17.80, + 17.91, + 17.90, + 16.98, + 14.91, + 14.02, + 14.01, + 14.45, + 14.91, + 15.45, + 15.87, + 16.67, + 16.58, + 17.04, + 17.57, + 17.95, + 17.76, + 18.03, + ]) + + @test all( + isapprox( + value.(m[:flow_out][sink, t, HeatHT]), + ref_values[t]; + atol = TEST_ATOL, + ) for t โˆˆ ๐’ฏ + ) + @test all( + value.(m[:buildings_surplus][building, t, p]) == 0.0 for + t โˆˆ ๐’ฏ, p โˆˆ building_res + ) + @test all( + value.(m[:buildings_deficit][building, t, p]) == 0.0 for + t โˆˆ ๐’ฏ, p โˆˆ building_res + ) + end end end diff --git a/test/test_checks.jl b/test/test_checks.jl index c413717..51cc96e 100644 --- a/test/test_checks.jl +++ b/test/test_checks.jl @@ -72,6 +72,11 @@ end ) end +@testset "Test checks - Building" begin + # Test that a unsupported source is caught by the checks + @test_throws ArgumentError simple_graph_building(; source = "unsupported_source") +end + @testset "Test checks - CSPandPV" begin # Test missing resource in cap @test_throws AssertionError simple_graph_csp_pv(; diff --git a/test/test_utils.jl b/test/test_utils.jl new file mode 100644 index 0000000..ce36584 --- /dev/null +++ b/test/test_utils.jl @@ -0,0 +1,72 @@ +@testset "heat_demand_profile utility function" begin + for _ โˆˆ 1:2 # Run the test two times to also test running from stored files (from first run) + profiles = [] + for source โˆˆ ["NORA3", "ERA5"] + df = get_heat_demand_profile(; source) + push!(profiles, df.heat_demand) + end + + ref_values = [ + [ + 12.37, + 12.26, + 12.09, + 12.08, + 11.92, + 12.74, + 13.71, + 13.81, + 13.91, + 13.77, + 13.79, + 13.75, + 14.29, + 14.65, + 14.98, + 16.54, + 15.52, + 15.87, + 16.14, + 16.16, + 16.18, + 16.17, + 16.07, + 16.53, + ], + [ + 12.01, + 11.91, + 11.76, + 11.61, + 11.49, + 12.22, + 13.07, + 13.11, + 13.14, + 13.14, + 13.27, + 13.26, + 13.87, + 14.51, + 14.6, + 14.7, + 14.57, + 14.98, + 15.19, + 15.26, + 15.09, + 15.12, + 14.87, + 15.05, + ], + ] + + for (ref_value, profile) โˆˆ zip(ref_values, profiles) + @test isapprox( + ref_value, + profile; + atol = TEST_ATOL, + ) + end + end +end diff --git a/test/utils.jl b/test/utils.jl index 957c276..c6757df 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -221,7 +221,7 @@ 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)) + 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" @@ -268,7 +268,7 @@ function simple_graph_building(; cap_p = nothing, HeatHT, temp_to_demand; data_path = joinpath(pkgdir(EMLI), "test", "data", "building"), - source = "NORA3", + source, ) else building = Building( @@ -567,3 +567,18 @@ function get_node(case::Case, id) elements = get_nodes(case) return EMLI.fetch_element(elements, id) end + +function get_heat_demand_profile(; source = "NORA3") + return EMLI.heat_demand_profile( + DateTime("2019-01-01T00:00:00"), + DateTime("2019-01-01T23:00:00"), + 55, # lat + 9, # lon + temp -> max(0, 20 - (temp - 273.15)); + data_path = joinpath(testdir, "data", "heat_demand_profile_test", source), + filename_hint = "Denmark", + source = source, + reload_csv = true, + save_csv = true, + ) +end From 15582ee4259436cbeb9beb3faf4a61d6ce7e5a57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Fri, 15 May 2026 17:50:41 +0200 Subject: [PATCH 20/20] Add suggestions from copilot review --- .github/actions/before-script/action.yml | 1 + docs/src/library/public.md | 1 - src/utils.jl | 2 +- test/utils.jl | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/before-script/action.yml b/.github/actions/before-script/action.yml index d698d5d..5e56526 100644 --- a/.github/actions/before-script/action.yml +++ b/.github/actions/before-script/action.yml @@ -71,6 +71,7 @@ runs: poetry install Set-Location (Join-Path $env:GITHUB_WORKSPACE "submodules\wind_power_timeseries") poetry install + pip install metocean-api==1.1.14 Set-Location (Join-Path $env:GITHUB_WORKSPACE "submodules\Tecnalia_Solar-Energy-Model") poetry install Set-Location (Join-Path $env:GITHUB_WORKSPACE "submodules\Tecnalia_Building-Stock-Energy-Model") diff --git a/docs/src/library/public.md b/docs/src/library/public.md index 44d5460..cf233c1 100644 --- a/docs/src/library/public.md +++ b/docs/src/library/public.md @@ -77,7 +77,6 @@ EMLI.Building( source::String = "NORA3", reload_csv::Bool = true, save_csv::Bool = true, - use_cache::Bool = true, ) EMLI.MultipleBuildingTypes( ::Any, diff --git a/src/utils.jl b/src/utils.jl index 5a1f800..74c7fe8 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -59,7 +59,7 @@ function get_python_function(module_name::String, function_name::String) sub_names = split(function_name, ".") python_function = pyimport(module_name) for name โˆˆ sub_names - python_function = getproperty(python_function, name) + python_function = getproperty(python_function, Symbol(name)) end return python_function end diff --git a/test/utils.jl b/test/utils.jl index c6757df..9da999d 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -82,7 +82,7 @@ function simple_graph_wind(; "shape" => missing, "turbine_height" => 150, ) - data_path = mkpath(joinpath(@__DIR__, "downloaded_nora3")) + data_path = mkpath(joinpath(testdir, "data", "WindPower")) if isnothing(profile) wind = WindPower( "Windfarm", # Node id