From 18f229e9ad6c865d40792007dfcaf7a651756241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Tue, 25 Nov 2025 14:38:30 +0100 Subject: [PATCH 1/6] Fix missing distinction between different `TransmissionMode`s in the "data"-menu by including the `id` field in parantheses. Also, improve the info-box. --- test/EMI_geography_2.jl | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/EMI_geography_2.jl b/test/EMI_geography_2.jl index 1eeca5d..de9b12c 100644 --- a/test/EMI_geography_2.jl +++ b/test/EMI_geography_2.jl @@ -180,6 +180,17 @@ function generate_example_data_geo_all_resources() 2, ) + H2_Transport_50MW_OT = RefDynamic( + "H2_Transport_50_OT", + H2, + FixedProfile(50.0), + FixedProfile(0.05), + FixedProfile(0), + FixedProfile(0), + 2, + [], + ) + Waste_Transport_50MW_OT = RefDynamic( "Waste_Transport_50_OT", Waste, From 88f3a132217190809f14213137af92cc4a1737a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Wed, 26 Nov 2025 15:10:23 +0100 Subject: [PATCH 2/6] Use EMG v0.11.3 to resolve a bug resolved in its PR http://energymodelsx/EnergyModelsGeography.jl#45 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 95a5d58..7f9ab39 100644 --- a/Project.toml +++ b/Project.toml @@ -41,7 +41,7 @@ Colors = "0.13" DataFrames = "1" Dates = "1" EnergyModelsBase = "0.9" -EnergyModelsGeography = "0.11" +EnergyModelsGeography = "0.11.3" EnergyModelsInvestments = "0.8" FileIO = "1" GLMakie = "0.13" From 562d3f18efbaa2ac03a1036a027bc59d9197541b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Sun, 14 Dec 2025 08:16:56 +0100 Subject: [PATCH 3/6] Enabled exporting topo axis to bmp, tif, tiff, jpg, jpeg, and png file format. --- src/utils_gen/export_utils.jl | 15 ++++++--------- test/test_interactivity.jl | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/utils_gen/export_utils.jl b/src/utils_gen/export_utils.jl index b2a4d9e..777e19f 100644 --- a/src/utils_gen/export_utils.jl +++ b/src/utils_gen/export_utils.jl @@ -197,7 +197,7 @@ function export_to_file(gui::GUI) valid_combinations = Dict( "All" => ["jpg", "jpeg", "svg", "xlsx", "png"], "Plots" => ["bmp", "tif", "tiff", "jpg", "jpeg", "svg", "xlsx", "png"], - "Topo" => ["svg"], + "Topo" => ["bmp", "tif", "tiff", "jpg", "jpeg", "svg", "png"], ) if !(file_ending ∈ valid_combinations[axes_str]) @info "Exporting $axes_str to a $file_ending file is not supported" @@ -220,17 +220,14 @@ function export_to_file(gui::GUI) elseif axes_str == "Topo" ax_sym = :topo end + ax = get_ax(gui, ax_sym) if file_ending == "svg" if axes_str == "Plots" - flag = export_svg( - get_ax(gui, ax_sym), filename; legend = get_results_legend(gui), - ) + flag = export_svg(ax, filename; legend = get_results_legend(gui)) elseif axes_str == "Topo" - flag = export_svg( - get_ax(gui, ax_sym), filename; legend = get_topo_legend(gui), - ) + flag = export_svg(ax, filename; legend = get_topo_legend(gui)) else - flag = export_svg(get_ax(gui, ax_sym), filename) + flag = export_svg(ax, filename) end elseif file_ending == "xlsx" if axes_str == "Plots" @@ -247,7 +244,7 @@ function export_to_file(gui::GUI) end else try - save(filename, colorbuffer(get_ax(gui, ax_sym))) + save(filename, colorbuffer(ax.scene)) flag = 0 catch flag = 2 diff --git a/test/test_interactivity.jl b/test/test_interactivity.jl index 793d3aa..6ac3d2d 100644 --- a/test/test_interactivity.jl +++ b/test/test_interactivity.jl @@ -370,7 +370,7 @@ simplified_toggle = get_toggle(gui, :simplified) "All" => ["jpg", "jpeg", "svg", "xlsx", "png", "lp", "mps"], "Plots" => ["bmp", "tif", "tiff", "jpg", "jpeg", "svg", "xlsx", "png", "lp", "mps"], - "Topo" => ["svg", "lp", "mps"], + "Topo" => ["bmp", "tif", "tiff", "jpg", "jpeg", "svg", "png", "lp", "mps"], ) @testset "get_button(gui,:export).clicks" begin tmpdir = mktempdir(testdir; prefix = "exported_files_") From ce9e1c131fe6e7d5343f7441b2cdeb67fdcfc61f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Sun, 14 Dec 2025 08:18:48 +0100 Subject: [PATCH 4/6] Adjusted `descriptive_names` to be more robust to available packages. Introduced module names as intermediate `Dict`s in the `structures` field of `descriptive_names` (this is a breaking change). --- NEWS.md | 14 ++ Project.toml | 4 +- examples/generate_examples.jl | 2 - src/EnergyModelsGUI.jl | 1 + src/descriptive_names.yml | 249 ++++++++++++++-------------- src/setup_GUI.jl | 3 +- src/setup_topology.jl | 3 +- src/utils_GUI/GUI_utils.jl | 77 +++++++-- src/utils_GUI/results_axis_utils.jl | 3 +- src/utils_gen/structures_utils.jl | 10 -- src/utils_gen/utils.jl | 59 +++---- test/EMI_geography_2.jl | 11 -- test/test_descriptive_names.jl | 67 ++++++-- 13 files changed, 285 insertions(+), 218 deletions(-) diff --git a/NEWS.md b/NEWS.md index 15e7f4b..d5ed67b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,19 @@ # Release notes +## Version 0.6.0 (2025-12-15) + +### Bugfix + +* Added missing descriptive names for the variables `emissions_link`, `link_opex_var`, `link_opex_fixed` and `emissions_trans`. + +### Enhancements + +* Enabled exporting topo axis to bmp, tif, tiff, jpg, jpeg, and png file format. + +### Adjustments + +* Adjusted `descriptive_names` to be more robust to available packages. Introduced module names as intermediate `Dict`s in the `structures` field of `descriptive_names` (this is a breaking change). + ## Version 0.5.18 (2025-11-24) ### Bugfix diff --git a/Project.toml b/Project.toml index 7f9ab39..39a053d 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "EnergyModelsGUI" uuid = "737a7361-d3b7-40e9-b1ac-59bee4c5ea2d" -version = "0.5.18" -authors = ["Jon Vegard Venås ", "Magnus Askeland ", "Shweta Tiwari "] +version = "0.6.0" +authors = ["Jon Vegard Venås ", "Dimitri Pinel ", "Magnus Askeland ", "Shweta Tiwari "] [deps] CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" diff --git a/examples/generate_examples.jl b/examples/generate_examples.jl index e18a414..f7169b8 100644 --- a/examples/generate_examples.jl +++ b/examples/generate_examples.jl @@ -483,8 +483,6 @@ function get_sub_system_data( Dict(CO2 => 1, Power => 0.02), # Input resource with input ratio # Line above: This implies that storing CO2 requires Power Dict(CO2 => 1), # Output from the node with output ratio - # In practice, for CO₂ storage, this is never used. - Data[], ), RefSink( j + 7, # Node id diff --git a/src/EnergyModelsGUI.jl b/src/EnergyModelsGUI.jl index 8aa66ee..3c65f8d 100644 --- a/src/EnergyModelsGUI.jl +++ b/src/EnergyModelsGUI.jl @@ -87,5 +87,6 @@ export EnergySystemDesign export set_colors export set_icons export save_results +export create_descriptive_names, get_descriptive_names end # module diff --git a/src/descriptive_names.yml b/src/descriptive_names.yml index 05a34de..584d52e 100644 --- a/src/descriptive_names.yml +++ b/src/descriptive_names.yml @@ -1,132 +1,122 @@ # This file contains description of EMX element fields (and potential sub-fields) and variables -# with fields of type TimeStruct.TimeProfile +# with fields of type TimeStruct.TimeProfile and fields that cannot be inherited +# from supertypes. structures: - # EnergyModelsBase - ## node.jl - AbstractStorageParameters: - capacity: "Installed capacity" - opex_var: "Relative variable operating expense per energy unit" - opex_fixed: "Relative fixed operating expense per installed capacity" - - Node: - cap: "Installed capacity" - opex_var: "Relative variable operating expense per energy unit produced" - opex_fixed: "Relative fixed operating expense per installed capacity" - - Sink: - cap: "Demand" - penalty: - surplus: "Penalties for surplus" - deficit: "Penalties for deficits" - - # EnergyModelsGeography - ## mode.jl - TransmissionMode: - trans_cap: "Capacity of the transmission mode" - trans_loss: "Relative loss of the transported resource during transmission" - opex_var: "Relative variable operating expense per energy unit transported" - opex_fixed: "Relative fixed operating expense per installed capacity" - consumption_rate: "Rate at which the resource is consumed, as a ratio of the volume of the resource going into the inlet" - - # EnergyModelsInvestment + EnergyModelsBase: + ## node.jl + AbstractStorageParameters: + capacity: "Installed capacity" + opex_var: "Relative variable operating expense per energy unit" + opex_fixed: "Relative fixed operating expense per installed capacity" + + Node: + cap: "Installed capacity" + opex_var: "Relative variable operating expense per energy unit produced" + opex_fixed: "Relative fixed operating expense per installed capacity" + + Sink: + cap: "Demand" + penalty: + surplus: "Penalties for surplus" + deficit: "Penalties for deficits" + + ## data.jl + EmissionsData: + emissions: "The process emissions per capacity usage through the variable `:cap_use`" + + EnergyModelsGeography: + TransmissionMode: + ## mode.jl + trans_cap: "Capacity of the transmission mode" + trans_loss: "Relative loss of the transported resource during transmission" + opex_var: "Relative variable operating expense per energy unit transported" + opex_fixed: "Relative fixed operating expense per installed capacity" + consumption_rate: "Rate at which the resource is consumed, as a ratio of the volume of the resource going into the inlet" + + EnergyModelsInvestments: ## investment_data.jl - AbstractInvData: - capex: "Capital costs for investing in a capacity" - max_inst: "Maximum installed capacity in a strategic period" - initial: "Initial capacity in the first strategic period" - - ## investment_mode.jl - Investment: - min_add: "Minimum added capacity in a strategic period" - max_add: "Maximum added capacity in a strategic period" - increment: "Used increment for discrete investments" - capex_offset: "Offset for the CAPEX in a strategic period" - FixedInvestment: - cap: "Capacity used for the fixed investments" - - BinaryInvestment: - cap: "Capacity used for the binary investments" - - ## lifetime_mode.jl - LifetimeMode: - lifetime: "Chosen lifetime of the technology" - - # EnergyModelsRenewableProducers - ## datastructures.jl - AbstractNonDisRES: - profile: "Power production profile as a ratio of installed capacity" - - HydroStorage: - level_init: "Initial stored energy in the dam" - level_inflow: "Inflow of power per operational period" - level_min: "Minimum fraction of the reservoir capacity required" - - ScheduleConstraint: - value: "The constraint value not to be violated" - flag: "Boolean value indicating if the constraint is active" - penalty: "Penalty for violating the constraint" - - HydroReservoir: - vol_inflow: "Water inflow to the reservoir" - - HydroGate: - cap: "Installed discharge capacity" - opex_var: "Variable operational costs per water flow" - opex_fixed: "Fixed operational costs" - - HydroUnit: - opex_var: "Variable operational costs per energy unit produced" - opex_fixed: "Fixed operational costs" - - HydroGenerator: - cap: "Installed discharge or power capacity" - - HydroPump: - cap: "Installed pumping capacity" - - AbstractBatteryLife: - stack_cost: "Relative cost for replacing a battery stack" - - # EnergyModelsHeat - ## link.jl - DHPipe: - cap: "Heat transport capacity of the pipe" - t_ground: "Ground temperature in °C" - - ## node.jl - HeatPump: - t_source: "Heat source temperature" - t_sink: "Heat sink temperature" - eff_carnot: "Carnot Efficiency" - - ## resource.jl - ResourceHeat: - t_supply: "Supply temperature in °C" - t_return: "Return temperature in °C" - - # EnergyModelsHydrogen - ## node.jl - AbstractElectrolyzer: - opex_var: "Variable operating expense per capacity used" - stack_replacement_cost: "Replacement cost of electrolyzer stacks" - - CommitParameters: - opex: "Operating cost per installed capacity and operational duration" - time: "Minimum time node must remain in a state before transitioning" - - AbstractRampParameters: - up: "Maximum positive rate of change of a node" - down: "Maximum negative rate of change of a node" - - # EnergyModelsCO2 - CO2Source: - opex_var: "Variable operating expense per energy unit produced" - - RefNetworkNodeRetrofit: - opex_var: "Variable operating expense per energy unit produced" - - CCSRetroFit: - opex_var: "Variable operating expense per unit of CO₂ captured" + AbstractInvData: + capex: "Capital costs for investing in a capacity" + max_inst: "Maximum installed capacity in a strategic period" + initial: "Initial capacity in the first strategic period" + + ## investment_mode.jl + Investment: + min_add: "Minimum added capacity in a strategic period" + max_add: "Maximum added capacity in a strategic period" + increment: "Used increment for discrete investments" + capex_offset: "Offset for the CAPEX in a strategic period" + + FixedInvestment: + cap: "Capacity used for the fixed investments" + + BinaryInvestment: + cap: "Capacity used for the binary investments" + + ## lifetime_mode.jl + LifetimeMode: + lifetime: "Chosen lifetime of the technology" + + EnergyModelsRenewableProducers: + ## datastructures.jl + AbstractNonDisRES: + profile: "Power production profile as a ratio of installed capacity" + + HydroStorage: + level_init: "Initial stored energy in the dam" + level_inflow: "Inflow of power per operational period" + level_min: "Minimum fraction of the reservoir capacity required" + + ScheduleConstraint: + value: "The constraint value not to be violated" + flag: "Boolean value indicating if the constraint is active" + penalty: "Penalty for violating the constraint" + + HydroReservoir: + vol_inflow: "Water inflow to the reservoir" + + HydroGate: + cap: "Installed discharge capacity" + opex_var: "Variable operational costs per water flow" + opex_fixed: "Fixed operational costs" + + HydroUnit: + opex_var: "Variable operational costs per energy unit produced" + opex_fixed: "Fixed operational costs" + + HydroGenerator: + cap: "Installed discharge or power capacity" + + HydroPump: + cap: "Installed pumping capacity" + + AbstractBatteryLife: + stack_cost: "Relative cost for replacing a battery stack" + + EnergyModelsHydrogen: + ## node.jl + AbstractElectrolyzer: + opex_var: "Variable operating expense per capacity used" + stack_replacement_cost: "Replacement cost of electrolyzer stacks" + + CommitParameters: + opex: "Operating cost per installed capacity and operational duration" + time: "Minimum time node must remain in a state before transitioning" + + AbstractRampParameters: + up: "Maximum positive rate of change of a node" + down: "Maximum negative rate of change of a node" + + EnergyModelsCO2: + ## datastructures.jl + CO2Source: + opex_var: "Variable operating expense per energy unit produced" + + RefNetworkNodeRetrofit: + opex_var: "Variable operating expense per energy unit produced" + + CCSRetroFit: + opex_var: "Variable operating expense per unit of CO₂ captured" variables: @@ -148,11 +138,14 @@ variables: emissions_node: "Emission of a node" emissions_total: "Total strategic emissions" emissions_strategic: "Total emissions" + emissions_link: "Emissions of a link" opex_var: "Absolute variable operating expenses" opex_fixed: "Absolute fixed operating expenses" sink_surplus: "Surplus delivered to a sink, i.e., oversatisfied demand" sink_deficit: "Deficit in a sink, i.e., not satisfied demand" link_cap_inst: "Installed capacity" + link_opex_var: "Variable operating expenses" + link_opex_fixed: "Fixed operating expenses" # EnergyModelsGeography area_exchange: "Area exchange" @@ -165,6 +158,7 @@ variables: trans_neg: "Negative loss during transmission" trans_pos: "Positive loss during transmission" linepack_stor_level: "Storage level in linepack" + emissions_trans: "Emissions of a transmission mode" # EnergyModelsInvestment cap_capex: "Absolute CAPEX for investments in the capacity of a technology" @@ -216,9 +210,6 @@ variables: bat_res_up: "Upwards reserve of battery storage" bat_res_down: "Downwards reserve of battery storage" - # EnergyModelsHeat - dh_pipe_loss: "Heat losses in DH pipes" - # EnergyModelsHydrogen ref_off_b: "Binary variable indicating if the reformer is in the 'off' state" ref_start_b: "Binary variable indicating if the reformer is in the 'start-up' state" diff --git a/src/setup_GUI.jl b/src/setup_GUI.jl index 6fd8851..d48ff25 100644 --- a/src/setup_GUI.jl +++ b/src/setup_GUI.jl @@ -152,7 +152,6 @@ function GUI( vars[:box_text_sep_px] = 5 # Separation between rectangles for colors and text vars[:taskbar_height] = 30 - vars[:descriptive_names] = Dict() vars[:path_to_descriptive_names] = path_to_descriptive_names vars[:descriptive_names_dict] = descriptive_names_dict @@ -230,7 +229,7 @@ function GUI( ) # Create complete Dict of descriptive names - update_descriptive_names!(gui) + gui.vars[:descriptive_names] = create_descriptive_names(get_vars(gui)) # Pre calculate the available fields for each node initialize_available_data!(gui) diff --git a/src/setup_topology.jl b/src/setup_topology.jl index 1778b43..c7af852 100644 --- a/src/setup_topology.jl +++ b/src/setup_topology.jl @@ -1,5 +1,6 @@ """ - EnergySystemDesign(system::AbstractSystem) + EnergySystemDesign(system::AbstractSystem; kwargs...) + EnergySystemDesign(case::Case; kwargs...) Create and initialize an instance of the `EnergySystemDesign` struct, representing energy system designs. If the argument is a `Case` instance, the function converts the case to a diff --git a/src/utils_GUI/GUI_utils.jl b/src/utils_GUI/GUI_utils.jl index 292bf86..e1b7ff9 100644 --- a/src/utils_GUI/GUI_utils.jl +++ b/src/utils_GUI/GUI_utils.jl @@ -390,7 +390,8 @@ function initialize_available_data!(gui) field = getfield(element, field_name) structure = String(nameof(typeof(element))) name = "$field_name" - key_str = "structures.$structure.$name" + parent_module = String(nameof(parentmodule(typeof(element)))) + key_str = "structures.$parent_module.$structure.$name" selection = Vector{Any}([element]) add_description!(field, name, key_str, "", selection, available_data, gui) end @@ -584,32 +585,32 @@ function update_available_data_menu!(gui::GUI, element) end """ - update_descriptive_names!(gui::GUI) + create_descriptive_names(vars::Dict{Symbol,Any}) -Update the dictionary of `descriptive_names` where the Dict is appended/overwritten in the +Return the dictionary of `descriptive_names` where the `Dict` is appended/overwritten in the following order: - The default descriptive names found in `src/descriptive_names.yml`. -- Any descriptive_names.yml file found in the ext/EMGUIExt folder of any other EMX package. +- Any descriptive_names.yml file found in the ext/EMGUIExt folder of available EMX packages. - Descriptive names from a user defined file (from the GUI input argument `path_to_descriptive_names`). - Descriptive names from a user defined Dict (from the GUI input argument `descriptive_names_dict`). """ -function update_descriptive_names!(gui::GUI) +function create_descriptive_names(vars::Dict{Symbol,Any}) descriptive_names = YAML.load_file( joinpath(@__DIR__, "..", "descriptive_names.yml"); dicttype = Dict{Symbol,Any}, ) # Get a dictionary of loaded packages - loaded_packages = loaded() + loaded_packages = collect(values(Base.loaded_modules)) # Filter packages with names matching the pattern "EnergyModels*" - emx_packages = filter(pkg -> occursin(r"EnergyModels", pkg), loaded_packages) + emx_packages = filter(pkg -> occursin(r"EnergyModels", string(pkg)), loaded_packages) # apply inheritances for fetching descriptive names # create a dictionary were the keys are all the types defined in emx_packages and the values are the types they inherit from emx_supertypes_dict = get_supertypes(emx_packages) inherit_descriptive_names_from_supertypes!(descriptive_names, emx_supertypes_dict) for package ∈ emx_packages - package_path::Union{String,Nothing} = dirname(dirname(Base.find_package(package))) + package_path::Union{String,Nothing} = dirname(dirname(Base.pathof(package))) if !isnothing(package_path) # check for presence of file to extend descriptive names path_to_descriptive_names_ext = joinpath( @@ -627,7 +628,7 @@ function update_descriptive_names!(gui::GUI) end # Update the Dict of descriptive names with user defined names from file - path_to_descriptive_names = get_var(gui, :path_to_descriptive_names) + path_to_descriptive_names = vars[:path_to_descriptive_names] if !isempty(path_to_descriptive_names) descriptive_names_dict_user_file = YAML.load_file( path_to_descriptive_names; dicttype = Dict{Symbol,Any}, @@ -636,11 +637,65 @@ function update_descriptive_names!(gui::GUI) end # Update the Dict of descriptive names with user defined names from Dict - descriptive_names_dict = get_var(gui, :descriptive_names_dict) + descriptive_names_dict = vars[:descriptive_names_dict] if !isempty(descriptive_names_dict) descriptive_names = merge_dicts(descriptive_names, descriptive_names_dict) end - get_vars(gui)[:descriptive_names] = descriptive_names + return descriptive_names +end +create_descriptive_names() = create_descriptive_names( + Dict{Symbol,Any}( + :path_to_descriptive_names => "", + :descriptive_names_dict => Dict{Symbol,Any}(), + ), +) + +""" + get_descriptive_names(modules::Union{Module, Vector{Module}}, descriptive_names::Dict{Symbol,Any}) + get_descriptive_names(model::Model, descriptive_names::Dict{Symbol,Any}) + +Get a mapping between EMX types and their descriptive names for fields of type `TimeProfile`. +""" +function get_descriptive_names( + modules::Union{Module,Vector{Module}}, + descriptive_names::Dict{Symbol,Any}, +) + emx_supertypes_dict = get_supertypes(modules) + + descriptive_names_map = Dict{Type,Dict{Symbol,Any}}() + for (_, emx_supertypes) ∈ emx_supertypes_dict + emx_type = emx_supertypes[1] + if !has_fields(emx_type) + continue + end + if !any(fieldtypes(emx_type) .<: TimeProfile) + continue + end + structure = String(nameof(emx_type)) + descriptive_names_map[emx_type] = Dict{Symbol,Any}() + for (field_name, field_type) ∈ zip(fieldnames(emx_type), fieldtypes(emx_type)) + if field_type <: TimeProfile + parent_module = String(nameof(parentmodule(emx_type))) + name = "$field_name" + key_str = "structures.$parent_module.$structure.$name" + descriptive_names_map[emx_type][field_name] = + get_nested_value(descriptive_names, key_str) + end + end + end + return descriptive_names_map +end +function get_descriptive_names(model::Model, descriptive_names::Dict{Symbol,Any}) + descriptive_names_map = Dict{Symbol,String}() + ignore_names = Symbol.(descriptive_names[:ignore]) + for sym ∈ collect(keys(object_dictionary(model))) + if sym ∈ ignore_names + continue + end + key_str = "variables.$(String(sym))" + descriptive_names_map[sym] = get_nested_value(descriptive_names, key_str) + end + return descriptive_names_map end """ diff --git a/src/utils_GUI/results_axis_utils.jl b/src/utils_GUI/results_axis_utils.jl index ecd889a..44c1e2d 100644 --- a/src/utils_GUI/results_axis_utils.jl +++ b/src/utils_GUI/results_axis_utils.jl @@ -158,7 +158,8 @@ function add_description!( name_field_type = nameof(field_type) name_field = "$name.$sub_field_name" pre_desc_sub = "$pre_desc$name_field_type: " - key_str = "structures.$name_field_type.$sub_field_name" + parent_module = String(nameof(parentmodule(field_type))) + key_str = "structures.$parent_module.$name_field_type.$sub_field_name" add_description!( sub_field, name_field, key_str, pre_desc_sub, selection, available_data, gui, ) diff --git a/src/utils_gen/structures_utils.jl b/src/utils_gen/structures_utils.jl index 9cef445..3d0f12f 100644 --- a/src/utils_gen/structures_utils.jl +++ b/src/utils_gen/structures_utils.jl @@ -14,16 +14,6 @@ function installed() return installs end -""" - function loaded() - -Get a list of loaded packages. -""" -loaded() = [ - String(n) for n ∈ names(Main, imported = true) if - isdefined(Main, n) && getfield(Main, n) isa Module -] - """ place_nodes_in_circle(total_nodes::Int64, current_node::Int64, r::Float32, c::Point2f) diff --git a/src/utils_gen/utils.jl b/src/utils_gen/utils.jl index 14dfd71..de950b2 100644 --- a/src/utils_gen/utils.jl +++ b/src/utils_gen/utils.jl @@ -89,6 +89,8 @@ function get_nested_value(dict::Dict, keys_str::String) for key ∈ keys if haskey(current_value, Symbol(key)) current_value = current_value[Symbol(key)] + else + error("Key $(key) not found in dictionary (full key: $(keys_str)).") end end return current_value @@ -340,12 +342,6 @@ Retrieves the supertypes of all defined types from modules or packages. - `get_supertypes(moduls::Vector{Module})`: Merges and returns supertypes from multiple modules. -- `get_supertypes(pkg::Union{String, Symbol})`: - Converts the package name to a module (via `Main`) and returns its type supertypes. - -- `get_supertypes(pkgs::Union{Vector{<:Union{String, Symbol}}, Set{<:Union{String, Symbol}}})`: - Merges and returns supertypes from multiple packages via their names. - # Arguments - `input`: Can be a single module, a vector of modules, a single package name (as `String` or `Symbol`), or a collection of package names. @@ -373,9 +369,6 @@ end get_supertypes(moduls::Vector{Module}) = merge!(Dict(), (get_supertypes(m) for m ∈ moduls)...) -get_supertypes(pkg::Union{String,Symbol}) = get_supertypes(getfield(Main, Symbol(pkg))) -get_supertypes(pkgs::Union{Vector{<:Union{String,Symbol}},Set{<:Union{String,Symbol}}}) = - merge!(Dict(), (get_supertypes(pkg) for pkg ∈ pkgs)...) """ has_fields(type::Type) -> Bool @@ -484,40 +477,42 @@ If a descriptive name exists for a field in a supertype but not in the subtype, - Updates `descriptive_names` in-place by inheriting missing descriptive names from supertypes. """ function inherit_descriptive_names_from_supertypes!(descriptive_names, emx_supertypes_dict) + structures = descriptive_names[:structures] for (emx_type_id, emx_supertypes) ∈ emx_supertypes_dict emx_type = emx_supertypes[1] # check if emx_type has field names and if so retrieve them, otherwise continue if !has_fields(emx_type) continue end + + # If parent module does not exist in structures, create it + emx_type_parent_module_sym = Symbol(parentmodule(emx_type)) + if !haskey(structures, emx_type_parent_module_sym) + structures[emx_type_parent_module_sym] = Dict{Symbol,Any}() + end + + emx_type_parent_module_dict = structures[emx_type_parent_module_sym] emx_type_fieldnames = fieldnames(emx_type) for fname ∈ emx_type_fieldnames for emx_supertype ∈ emx_supertypes[2:end] # skip first element as it is the type itself - #check if the supertype has an entry in descriptive names for fname + # check if the supertype has an entry in descriptive names for fname # Extract only what is after the dot in emx_supertype, if any - supertype_str = string(emx_supertype) - supertype_key = - occursin(r"\.", supertype_str) ? - match(r"\.([^.]+)$", supertype_str).captures[1] : supertype_str - if haskey(descriptive_names[:structures], Symbol(supertype_key)) && - haskey( - descriptive_names[:structures][Symbol(supertype_key)], - Symbol(fname), - ) - # if so, and if the emx_type does not have an entry for fname, copy it - if !haskey(descriptive_names[:structures], Symbol(emx_type)) || - !haskey( - descriptive_names[:structures][Symbol(emx_type)], - Symbol(fname), - ) - if !haskey(descriptive_names[:structures], Symbol(emx_type)) - descriptive_names[:structures][Symbol(emx_type)] = - Dict{Symbol,Any}() + parent_module_str = Symbol(parentmodule(emx_supertype)) + supertype_key = nameof(emx_supertype) + if haskey(structures, parent_module_str) + module_dict = structures[parent_module_str] + if haskey(module_dict, supertype_key) && + haskey(module_dict[supertype_key], fname) + # if so, and if the emx_type does not have an entry for fname, copy it + if !haskey(emx_type_parent_module_dict, emx_type_id) || + !haskey(emx_type_parent_module_dict[emx_type_id], fname) + if !haskey(emx_type_parent_module_dict, emx_type_id) + emx_type_parent_module_dict[emx_type_id] = + Dict{Symbol,Any}() + end + emx_type_parent_module_dict[emx_type_id][fname] = + module_dict[supertype_key][fname] end - descriptive_names[:structures][Symbol(emx_type)][Symbol(fname)] = - descriptive_names[:structures][Symbol(supertype_key)][Symbol( - fname, - )] end end end diff --git a/test/EMI_geography_2.jl b/test/EMI_geography_2.jl index de9b12c..1eeca5d 100644 --- a/test/EMI_geography_2.jl +++ b/test/EMI_geography_2.jl @@ -180,17 +180,6 @@ function generate_example_data_geo_all_resources() 2, ) - H2_Transport_50MW_OT = RefDynamic( - "H2_Transport_50_OT", - H2, - FixedProfile(50.0), - FixedProfile(0.05), - FixedProfile(0), - FixedProfile(0), - 2, - [], - ) - Waste_Transport_50MW_OT = RefDynamic( "Waste_Transport_50_OT", Waste, diff --git a/test/test_descriptive_names.jl b/test/test_descriptive_names.jl index d8ac05d..3ebd6af 100644 --- a/test/test_descriptive_names.jl +++ b/test/test_descriptive_names.jl @@ -1,3 +1,8 @@ +const EMB = EnergyModelsBase +const EMI = EnergyModelsInvestments +const EMG = EnergyModelsGeography +const EMRP = EnergyModelsRenewableProducers + case, model, m, gui = run_case() # Test specific miscellaneous descriptive names @@ -11,9 +16,11 @@ case, model, m, gui = run_case() str5 = "" str6 = "" descriptive_names_dict = Dict( - :structures => Dict( # Input parameter from the case Dict - :RefStatic => Dict(:trans_cap => str1, :opex_fixed => str2), - :RefDynamic => Dict(:opex_var => str3, :directions => str4), + :structures => Dict( + :EnergyModelsGeography => Dict( # Input parameter from the case Dict + :RefStatic => Dict(:trans_cap => str1, :opex_fixed => str2), + :RefDynamic => Dict(:opex_var => str3, :directions => str4), + ), ), :variables => Dict( # variables from the JuMP model :stor_discharge_use => str5, @@ -26,10 +33,11 @@ case, model, m, gui = run_case() descriptive_names_dict = descriptive_names_dict, ) descriptive_names = EMGUI.get_var(gui2, :descriptive_names) - @test descriptive_names[:structures][:RefStatic][:trans_cap] == str1 - @test descriptive_names[:structures][:RefStatic][:opex_fixed] == str2 - @test descriptive_names[:structures][:RefDynamic][:opex_var] == str3 - @test descriptive_names[:structures][:RefDynamic][:directions] == str4 + descriptive_names_EMG = descriptive_names[:structures][:EnergyModelsGeography] + @test descriptive_names_EMG[:RefStatic][:trans_cap] == str1 + @test descriptive_names_EMG[:RefStatic][:opex_fixed] == str2 + @test descriptive_names_EMG[:RefDynamic][:opex_var] == str3 + @test descriptive_names_EMG[:RefDynamic][:directions] == str4 @test descriptive_names[:variables][:stor_discharge_use] == str5 @test descriptive_names[:variables][:trans_cap_rem] == str6 EMGUI.close(gui2) @@ -46,21 +54,46 @@ case, model, m, gui = run_case() path_to_descriptive_names = path_to_descriptive_names, ) - @test descriptive_names_raw[:structures][:Node][:opex_fixed] == str1 - @test :StorCapOpexFixed ∉ keys(descriptive_names_raw[:structures]) - @test :RefNetworkNode ∉ keys(descriptive_names_raw[:structures]) + desc_raw_EMB = descriptive_names_raw[:structures][:EnergyModelsBase] + @test desc_raw_EMB[:Node][:opex_fixed] == str1 + @test :StorCapOpexFixed ∉ keys(desc_raw_EMB) + @test :RefNetworkNode ∉ keys(desc_raw_EMB) - @test descriptive_names_raw[:structures][:HydroStorage][:level_init] == str2 - @test :HydroStor ∉ keys(descriptive_names_raw[:structures]) - @test :PumpedHydroStor ∉ keys(descriptive_names_raw[:structures]) + desc_raw_EMRP = descriptive_names_raw[:structures][:EnergyModelsRenewableProducers] + @test desc_raw_EMRP[:HydroStorage][:level_init] == str2 + @test :HydroStor ∉ keys(desc_raw_EMRP) + @test :PumpedHydroStor ∉ keys(desc_raw_EMRP) descriptive_names = EMGUI.get_var(gui3, :descriptive_names) - @test descriptive_names[:structures][:StorCapOpexFixed][:opex_fixed] == str1 - @test descriptive_names[:structures][:RefNetworkNode][:opex_fixed] == str1 + desc_EMB = descriptive_names[:structures][:EnergyModelsBase] + @test desc_EMB[:StorCapOpexFixed][:opex_fixed] == str1 + @test desc_EMB[:RefNetworkNode][:opex_fixed] == str1 - @test descriptive_names[:structures][:HydroStor][:level_init] == str2 - @test descriptive_names[:structures][:PumpedHydroStor][:level_init] == str2 + desc_EMRP = descriptive_names[:structures][:EnergyModelsRenewableProducers] + @test desc_EMRP[:HydroStorage][:level_init] == str2 + @test desc_EMRP[:PumpedHydroStor][:level_init] == str2 EMGUI.close(gui3) end + + @testset "Test existence of descriptive names for all available EMX-packages" begin + # Check that no descriptive names are empty for types + descriptive_names = create_descriptive_names() + types_map = get_descriptive_names([EMB, EMI, EMG, EMRP], descriptive_names) + @test !any(any(isempty.(values(a))) for a ∈ values(types_map)) + + case, model = generate_example_data_geo() + m = create_model(case, model) + + # Check that no descriptive names are empty for variables + variables_map = get_descriptive_names(m, descriptive_names) + @test !any(any(isempty.(values(a))) for a ∈ values(variables_map)) + + case, model = generate_example_hp() + m = create_model(case, model) + + # Check that no descriptive names are empty for variables for EMRP + variables_map_EMRP = get_descriptive_names(m, descriptive_names) + @test !any(any(isempty.(values(a))) for a ∈ values(variables_map_EMRP)) + end end EMGUI.close(gui) From 4beb9a4676580df5f362df9d9f889c8726c75d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Sun, 14 Dec 2025 15:14:10 +0100 Subject: [PATCH 5/6] Enable custom boundary background map instead of the default coastlines (through a `.geojson` file location provided by the keyword argument `map_boundary_file` in the `GUI`-function). --- NEWS.md | 1 + src/setup_GUI.jl | 52 +++++++++++++++++++++++++++++------------------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/NEWS.md b/NEWS.md index d5ed67b..691c0d6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -9,6 +9,7 @@ ### Enhancements * Enabled exporting topo axis to bmp, tif, tiff, jpg, jpeg, and png file format. +* Enable custom boundary background map instead of the default coastlines (through a `.geojson` file location provided by the keyword argument `map_boundary_file` in the `GUI`-function). ### Adjustments diff --git a/src/setup_GUI.jl b/src/setup_GUI.jl index d48ff25..15d2137 100644 --- a/src/setup_GUI.jl +++ b/src/setup_GUI.jl @@ -51,6 +51,8 @@ to the old EnergyModelsX `case` dictionary. (instead of multiple lines for multiple transmission modes) in the topology design. - **`simplify_all_levels::Bool=false`** toggles whether or not to use simplified connection plotting for all hierarchical levels. +- **`map_boundary_file::String=""`** is the path to a .geojson file containing + geographical boundary data for plotting to be used instead of the default coastlines. !!! warning "Reading model results from CSV-files" Reading model results from a directory (*i.e.*, `model::String` implying that the results @@ -84,6 +86,7 @@ function GUI( pre_plot_sub_components::Bool = true, simplified_connection_plotting::Bool = false, simplify_all_levels::Bool = false, + map_boundary_file::String = "", ) # Generate the system topology: @info raw"Setting up the topology design structure" @@ -130,6 +133,7 @@ function GUI( :simplified_connection_plotting => simplified_connection_plotting, :simplify_all_levels => simplify_all_levels, :marker_to_box_ratio => 0.4, # Ratio between marker size and `Node` box size + :map_boundary_file => map_boundary_file, :autolimits => Dict( :results_op => true, :results_sc => true, @@ -325,28 +329,34 @@ function create_makie_objects(vars::Dict, design::EnergySystemDesign) alignmode = Inside(), ) - if vars[:coarse_coast_lines] # Use low resolution coast lines - countries = GeoMakie.land() - else # Use high resolution coast lines - # Define the URL and the local file path - resolution::String = "10m" # "10m", "50m", "110m" - url::String = "https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_$(resolution)_land.geojson" - temp_dir::String = tempdir() # Get the system's temporary directory - filename_countries::String = "EnergyModelsGUI_countries.geojson" - local_file_path::String = joinpath(temp_dir, filename_countries) - - # Download the file if it doesn't exist in the temporary directory - if !isfile(local_file_path) - HTTP.download(url, local_file_path) + if isempty(vars[:map_boundary_file]) + # Plot coast lines + if vars[:coarse_coast_lines] # Use low resolution coast lines + boundary = GeoMakie.land() + else # Use high resolution coast lines + # Define the URL and the local file path + resolution::String = "10m" # "10m", "50m", "110m" + url::String = "https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_$(resolution)_land.geojson" + temp_dir::String = tempdir() # Get the system's temporary directory + filename_countries::String = "EnergyModelsGUI_countries.geojson" + local_file_path::String = joinpath(temp_dir, filename_countries) + + # Download the file if it doesn't exist in the temporary directory + if !isfile(local_file_path) + HTTP.download(url, local_file_path) + end + + # Now read the data from the file + boundary_geo_json = GeoJSON.read(read(local_file_path, String)) + + # Create GeoMakie plotable object + boundary = GeoMakie.to_multipoly(boundary_geo_json.geometry) end - - # Now read the data from the file - countries_geo_json = GeoJSON.read(read(local_file_path, String)) - - # Create GeoMakie plotable object - countries = GeoMakie.to_multipoly(countries_geo_json.geometry) + else + boundary_geo_json = GeoJSON.read(read(vars[:map_boundary_file], String)) + boundary = GeoMakie.to_multipoly(boundary_geo_json.geometry) end - countries_plot = poly!( + poly!( ax, countries; color = :honeydew, @@ -358,7 +368,7 @@ function create_makie_objects(vars::Dict, design::EnergySystemDesign) stroke_depth_shift = 1.0f0 - 3.0f-5, ) ocean_coords = [(180, -90), (-180, -90), (-180, 90), (180, 90)] - ocean = poly!( + poly!( ax, ocean_coords, color = :lightblue1, From 630d4adfd73f109296567fbc6be307e49b2aeca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Mon, 15 Dec 2025 10:41:51 +0100 Subject: [PATCH 6/6] Fix introduced bug --- docs/src/library/public.md | 2 ++ src/setup_GUI.jl | 2 +- src/utils_GUI/GUI_utils.jl | 4 +--- src/utils_GUI/results_axis_utils.jl | 8 ++++++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/src/library/public.md b/docs/src/library/public.md index 457ce11..134d761 100644 --- a/docs/src/library/public.md +++ b/docs/src/library/public.md @@ -20,4 +20,6 @@ GUI save_results set_colors set_icons +create_descriptive_names +get_descriptive_names ``` diff --git a/src/setup_GUI.jl b/src/setup_GUI.jl index 15d2137..ffb2237 100644 --- a/src/setup_GUI.jl +++ b/src/setup_GUI.jl @@ -358,7 +358,7 @@ function create_makie_objects(vars::Dict, design::EnergySystemDesign) end poly!( ax, - countries; + boundary; color = :honeydew, colormap = :dense, strokecolor = :gray50, diff --git a/src/utils_GUI/GUI_utils.jl b/src/utils_GUI/GUI_utils.jl index e1b7ff9..fef0a31 100644 --- a/src/utils_GUI/GUI_utils.jl +++ b/src/utils_GUI/GUI_utils.jl @@ -579,9 +579,7 @@ function update_available_data_menu!(gui::GUI, element) available_data = get_available_data(gui) container = available_data[element] container_strings = create_label.(container) - if !isempty(container) # needed to resolve bug introduced in Makie - get_menu(gui, :available_data).options = zip(container_strings, container) - end + get_menu(gui, :available_data).options = zip(container_strings, container) end """ diff --git a/src/utils_GUI/results_axis_utils.jl b/src/utils_GUI/results_axis_utils.jl index 44c1e2d..2e9b791 100644 --- a/src/utils_GUI/results_axis_utils.jl +++ b/src/utils_GUI/results_axis_utils.jl @@ -119,8 +119,12 @@ function add_description!( data_type = nameof(typeof(data)) name_field = "$name.$data_type" key_str_field = "$key_str.$data_type" + ext_selection = deepcopy(selection) + if isa(data, Resource) + push!(ext_selection, data) + end add_description!( - data, name_field, key_str_field, pre_desc, selection, available_data, gui, + data, name_field, key_str_field, pre_desc, ext_selection, available_data, gui, ) end end @@ -148,7 +152,7 @@ function add_description!( available_data::Vector{PlotContainer}, gui::GUI, ) - structure = get_nth_field(key_str, '.', 3) + structure = get_nth_field(key_str, '.', 4) if structure == "to" || structure == "from" # don't add `to` and `from` fields return nothing end