diff --git a/NEWS.md b/NEWS.md index c0ba14f..2739258 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,19 @@ # Release notes +## Unversioned updates + +### Enhancements + +* Added the option `pre_plot_sub_components` to the `GUI`-constructor to skip preplotting hidden sub components of an area (the option is by default `true`). The components of an `Area` are then plotted on demand (on the `open` functionality). This greatly enhances performance for large cases. +* Improved general performance. + +### Adjustments + +* Adjusted the calculation of `Connection` plots. +* Added the field `visible` to `EnergySystemDesign` and `Connection` on which plots can directly rely on for visibility +* Adjust behaviour of `Base.show` on the types `EnergySystemDesign`, `Connection` and `AbstractSystem` to correspond to `Base.show` of their corresponding `AbstractElement`. +* Change tests of toggling of colors to be based on a new case having more transmission modes (the case in the new `test/EMI_geography_2.jl` file). + ## Version 0.5.17 (2025-11-19) ### Bugfix diff --git a/Project.toml b/Project.toml index 46123e8..95a5d58 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "EnergyModelsGUI" uuid = "737a7361-d3b7-40e9-b1ac-59bee4c5ea2d" -version = "0.5.17" +version = "0.5.18" authors = ["Jon Vegard Venås ", "Magnus Askeland ", "Shweta Tiwari "] [deps] diff --git a/docs/src/figures/EMI_geography.png b/docs/src/figures/EMI_geography.png index 2543623..2837ba5 100644 Binary files a/docs/src/figures/EMI_geography.png and b/docs/src/figures/EMI_geography.png differ diff --git a/docs/src/figures/EMI_geography_Oslo.png b/docs/src/figures/EMI_geography_Oslo.png index fef7f91..a652c2c 100644 Binary files a/docs/src/figures/EMI_geography_Oslo.png and b/docs/src/figures/EMI_geography_Oslo.png differ diff --git a/examples/EMI_geography.jl b/examples/EMI_geography.jl index 6d41cd8..a8c5199 100644 --- a/examples/EMI_geography.jl +++ b/examples/EMI_geography.jl @@ -26,4 +26,5 @@ gui = GUI( coarse_coast_lines = false, scale_tot_opex = true, scale_tot_capex = false, + pre_plot_sub_components = false, ) diff --git a/examples/generate_examples.jl b/examples/generate_examples.jl index ed197d3..a7458b0 100644 --- a/examples/generate_examples.jl +++ b/examples/generate_examples.jl @@ -520,9 +520,7 @@ technologies. The example is partly based on the provided example `network.jl` in `EnergyModelsGeography`. It will be repalced in the near future with a simplified example. """ - function generate_example_data_geo() - @debug "Generate case data" @info "Generate data coded dummy model for now (Investment Model)" # Retrieve the products diff --git a/ext/EMGExt/EMGExt.jl b/ext/EMGExt/EMGExt.jl index eaf26bb..ea8485e 100644 --- a/ext/EMGExt/EMGExt.jl +++ b/ext/EMGExt/EMGExt.jl @@ -4,14 +4,8 @@ using TimeStruct using EnergyModelsBase using EnergyModelsInvestments using EnergyModelsGeography - -# Use GLMakie to get the GridLayout type -using GLMakie - using EnergyModelsGUI -using DataFrames - const TS = TimeStruct const EMG = EnergyModelsGeography const EMB = EnergyModelsBase @@ -38,7 +32,7 @@ EMG.get_transmissions(system::EMGUI.SystemGeo) = EMGUI.get_connections(system) Get all transmission modes of a `SystemGeo` `system`. """ function get_modes(system::EMGUI.SystemGeo) - transmission_modes = Vector{TransmissionMode}() + transmission_modes = TransmissionMode[] for t ∈ get_transmissions(system) append!(transmission_modes, modes(t)) end @@ -70,7 +64,6 @@ Initialize a `SystemGeo` from a `Case`. """ function EMGUI.SystemGeo(case::Case) areas = get_areas(case) - ref_element = areas[1] return EMGUI.SystemGeo( get_time_struct(case), get_products(case), @@ -78,7 +71,7 @@ function EMGUI.SystemGeo(case::Case) areas, get_transmissions(case), EMGUI.NothingElement(), - ref_element, + areas[1], ) end diff --git a/src/datastructures.jl b/src/datastructures.jl index edba484..5a53811 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -135,9 +135,10 @@ energy system designs in Julia. - **`wall::Observable{Symbol}`** represents an aspect of the system's state, observed for changes and represented as a Symbol. - **`file::String`** is the filename or path associated with the `EnergySystemDesign`. -- **`plots::Vector{Any}`** is a vector with all Makie object associated with this object. - The value does not have to be provided. -- **`invest_data::ProcInvData`** stores processed investment data. +- **`visible::Observable{Bool}`** indicates whether the system is visible, observed for changes. +- **`inv_data::ProcInvData`** stores processed investment data. +- **`plots::Vector{Makie.AbstractPlot}`** is a vector with all Makie object associated with + this object. """ mutable struct EnergySystemDesign <: AbstractGUIObj system::AbstractSystem @@ -151,8 +152,9 @@ mutable struct EnergySystemDesign <: AbstractGUIObj color::Observable{RGBA{Float32}} wall::Observable{Symbol} file::String + visible::Observable{Bool} inv_data::ProcInvData - plots::Vector{Any} + plots::Vector{Makie.AbstractPlot} end function EnergySystemDesign( system::AbstractSystem, @@ -166,6 +168,7 @@ function EnergySystemDesign( color::Observable{RGBA{Float32}}, wall::Observable{Symbol}, file::String, + visible::Observable{Bool}, ) return EnergySystemDesign( system, @@ -179,6 +182,7 @@ function EnergySystemDesign( color, wall, file, + visible, ProcInvData(), Any[], ) @@ -198,25 +202,29 @@ Mutable type for providing a flexible data structure for connections between linked to. - **`connection::AbstractElement`** is the EMX connection structure. - **`colors::Vector{RGBA{Float32}}`** is the associated colors of the connection. -- **`plots::Vector{Any}`** is a vector with all Makie object associated with this object. -- **`invest_data::ProcInvData`** stores processed investment data. +- **`visible::Observable{Bool}`** indicates whether the system is visible, observed for changes. +- **`inv_data::ProcInvData`** stores processed investment data. +- **`plots::Vector{Makie.AbstractPlot}`** is a vector with all Makie object associated with + this object. """ mutable struct Connection <: AbstractGUIObj from::EnergySystemDesign to::EnergySystemDesign connection::AbstractElement colors::Vector{RGBA{Float32}} + visible::Observable{Bool} inv_data::ProcInvData - plots::Vector{Any} + plots::Vector{Makie.AbstractPlot} end function Connection( from::EnergySystemDesign, to::EnergySystemDesign, connection::AbstractElement, id_to_color_map::Dict{Any,Any}, + visible::Observable{Bool}, ) colors::Vector{RGBA{Float32}} = get_resource_colors(connection, id_to_color_map) - return Connection(from, to, connection, colors, ProcInvData(), Any[]) + return Connection(from, to, connection, colors, visible, ProcInvData(), Any[]) end """ @@ -308,10 +316,10 @@ const YELLOW = RGBA{Float32}(1.0, 1.0, 0.0, 1.0) const MAGENTA = RGBA{Float32}(1.0, 0.0, 1.0, 1.0) const CYAN = RGBA{Float32}(0.0, 1.0, 1.0, 1.0) -Base.show(io::IO, obj::AbstractGUIObj) = dump(io, obj; maxdepth = 1) +Base.show(io::IO, obj::AbstractGUIObj) = Base.show(io, get_element(obj)) Base.show(io::IO, ::NothingDesign) = print(io, "NothingDesign()") Base.show(io::IO, obj::ProcInvData) = dump(io, obj; maxdepth = 1) -Base.show(io::IO, system::AbstractSystem) = dump(io, system; maxdepth = 1) +Base.show(io::IO, system::AbstractSystem) = Base.show(io, get_element(system)) Base.show(io::IO, gui::GUI) = dump(io, gui; maxdepth = 1) Base.show(io::IO, ::NothingElement) = print(io, "top_level") @@ -418,11 +426,11 @@ Returns the nodes of a `AbstractSystem` `system`. EMB.get_nodes(system::AbstractSystem) = get_children(system) """ - get_element(system::System) + get_element(system::AbstractSystem) -Returns the `element` assosiated of a `System` `system`. +Returns the `element` assosiated of a `AbstractSystem` `system`. """ -get_element(system::System) = get_parent(system) +get_element(system::AbstractSystem) = get_parent(system) """ get_plotables(system::System) @@ -600,6 +608,13 @@ Returns the `plots` field of a `AbstractGUIObj` `obj`. """ get_plots(obj::AbstractGUIObj) = obj.plots +""" + get_visible(obj::AbstractGUIObj) + +Returns the `visible` field of a `AbstractGUIObj` `obj`. +""" +get_visible(obj::AbstractGUIObj) = obj.visible + """ get_fig(gui::GUI) diff --git a/src/setup_GUI.jl b/src/setup_GUI.jl index 396f0f4..26fca5c 100644 --- a/src/setup_GUI.jl +++ b/src/setup_GUI.jl @@ -42,6 +42,10 @@ to the old EnergyModelsX `case` dictionary. hovering objects to show information. - **`use_geomakie::Bool=true`** toggles the use of GeoMakie for plotting geographical designs when the `case` contains geographical information. +- **`pre_plot_sub_components::Bool=true`** toggles whether or not to pre-plot all + sub-components of areas in the topology design. Setting this to `false` greatly + enhances performance for large cases, as the components of an `Area` are then + plotted on demand (on the `open` functionality). !!! warning "Reading model results from CSV-files" Reading model results from a directory (*i.e.*, `model::String` implying that the results @@ -72,6 +76,7 @@ function GUI( tol::Float64 = 1e-8, enable_data_inspector::Bool = true, use_geomakie::Bool = true, + pre_plot_sub_components::Bool = true, ) # Generate the system topology: @info raw"Setting up the topology design structure" @@ -82,6 +87,11 @@ function GUI( @info raw"Setting up the GUI" design::EnergySystemDesign = root_design # variable to store current system (inkluding sub systems) + if expand_all && !pre_plot_sub_components + expand_all = false + @warn "Incompatible EMGUI settings: `expand_all` is set to true but " * + "`pre_plot_sub_components` is set to false. Setting `expand_all` to false." + end # Set variables vars::Dict{Symbol,Any} = Dict( :title => Observable("top_level"), @@ -111,6 +121,7 @@ function GUI( :colormap => colormap, :tol => tol, :use_geomakie => use_geomakie, + :pre_plot_sub_components => pre_plot_sub_components, :autolimits => Dict( :results_op => true, :results_sc => true, @@ -137,7 +148,7 @@ function GUI( vars[:path_to_descriptive_names] = path_to_descriptive_names vars[:descriptive_names_dict] = descriptive_names_dict - vars[:plot_widths] = plot_widths + vars[:plot_widths] = Vec{2,Int64}(plot_widths) vars[:hide_topo_ax_decorations] = hide_topo_ax_decorations vars[:expand_all] = expand_all @@ -149,30 +160,42 @@ function GUI( # Create iterables for plotting objects in layers (z-direction) such that nodes are # neatly placed on top of each other and lines are beneath nodes - vars[:z_translate_lines] = 10.0f0 - vars[:z_translate_components] = 50.0f0 + vars[:depth_shift_lines] = 0.006f0 + vars[:depth_shift_components] = 0.002f0 vars[:selected_systems] = [] # Default text for the text area - vars[:default_text] = string( - "Tips:\n", - "Keyboard shortcuts:\n", - "\tctrl+left-click: Select multiple nodes.\n", - "\tright-click and drag: to pan\n", - "\tscroll wheel: zoom in or out\n", - "\tspace: Enter the selected system\n", - "\tctrl+s: Save\n", - "\tctrl+r: Reset view\n", - "\tctrl+w: Close window\n", - "\tEsc (or MouseButton4): Exit the current system and into the parent system\n", - "\tholding x while scrolling over plots will zoom in/out in the x-direction.\n", - "\tholding y while scrolling over plots will zoom in/out in the y-direction.\n\n", - "Left-clicking a component will put information about this component here.\n\n", - "Clicking a plot below enables you to pin this plot (hitting the `pin\n", - "current plot` button) for comparison with other plots.\n", - "Use the `Delete` button to unpin a selected plot.", - ) + io = IOBuffer() + println(io, "Tips:") + println(io, "Keyboard shortcuts:") + println(io, "\tctrl+left-click: Select multiple nodes.") + println(io, "\tright-click and drag: to pan") + println(io, "\tscroll wheel: zoom in or out") + println(io, "\tspace: Enter the selected system") + println(io, "\tctrl+s: Save") + println(io, "\tctrl+r: Reset view") + println(io, "\tctrl+w: Close window") + println( + io, + "\tEsc (or MouseButton4): Exit the current system and into the parent system", + ) + println( + io, + "\tholding x while scrolling over plots will zoom in/out in the x-direction.", + ) + println( + io, + "\tholding y while scrolling over plots will zoom in/out in the y-direction.\n", + ) + println( + io, + "Left-clicking a component will put information about this component here.\n", + ) + println(io, "Clicking a plot below enables you to pin this plot (hitting the `pin") + println(io, "current plot` button) for comparison with other plots.") + print(io, "Use the `Delete` button to unpin a selected plot.") + vars[:default_text] = String(take!(io)) vars[:info_text] = Observable(vars[:default_text]) vars[:summary_text] = Observable("No model results") vars[:dragging] = Ref(false) @@ -219,7 +242,9 @@ function GUI( notify(axes[:topo].finallimits) # make sure all graphics is adapted to the spawned figure sizes - notify(get_toggle(gui, :expand_all).active) + if get_var(gui, :expand_all) + notify(get_toggle(gui, :expand_all).active) + end # Enable inspector (such that hovering objects shows information) # Linewidth set to zero as this boundary is slightly laggy on movement @@ -305,7 +330,6 @@ function create_makie_objects(vars::Dict, design::EnergySystemDesign) # Download the file if it doesn't exist in the temporary directory if !isfile(local_file_path) - @debug "Trying to download file $url to $local_file_path" HTTP.download(url, local_file_path) end @@ -323,8 +347,9 @@ function create_makie_objects(vars::Dict, design::EnergySystemDesign) strokecolor = :gray50, strokewidth = 0.5, inspectable = false, + depth_shift = 1.0f0 - 2.0f-5, + stroke_depth_shift = 1.0f0 - 3.0f-5, ) - Makie.translate!(countries_plot, 0, 0, -1) ocean_coords = [(180, -90), (-180, -90), (-180, 90), (180, 90)] ocean = poly!( ax, @@ -333,8 +358,9 @@ function create_makie_objects(vars::Dict, design::EnergySystemDesign) strokewidth = 0.5, strokecolor = :gray50, inspectable = false, + depth_shift = 1.0f0, + stroke_depth_shift = 1.0f0 - 1.0f-5, ) - Makie.translate!(ocean, 0, 0, -2) else # The design does not use the EnergyModelsGeography package: Create a simple Makie axis ax = Axis( gridlayout_topology_ax[1, 1]; @@ -411,7 +437,6 @@ function create_makie_objects(vars::Dict, design::EnergySystemDesign) labelsize = vars[:fontsize], titlesize = vars[:fontsize], ) - Makie.translate!(topo_legend.blockscene, 0, 0, vars[:z_translate_components] + 999) # Initiate an axis for displaying information about the selected node ax_info::Makie.Axis = Axis( @@ -638,11 +663,6 @@ function create_makie_objects(vars::Dict, design::EnergySystemDesign) :results => nothing, :topo => topo_legend, ) - # Ensure that menus are on top - for menu ∈ values(menus) - translate!(menu.blockscene, 0.0f0, 0.0f0, vars[:z_translate_components] + 2000.0f0) - end - # Collect all toggles into a dictionary toggles::Dict{Symbol,Makie.Toggle} = Dict(:expand_all => expand_all_toggle) @@ -652,18 +672,13 @@ function create_makie_objects(vars::Dict, design::EnergySystemDesign) ) # Update the title of the figure - topo_title_obj = text!( + text!( ax, vars[:topo_title_loc_x], vars[:topo_title_loc_y]; text = vars[:title], fontsize = vars[:fontsize], - ) - Makie.translate!( - topo_title_obj, - 0.0f0, - 0.0f0, - vars[:z_translate_components] + 999.0f0, + depth_shift = -1.0f0, ) return fig, buttons, menus, toggles, axes, legends diff --git a/src/setup_topology.jl b/src/setup_topology.jl index 895a5a0..0874630 100644 --- a/src/setup_topology.jl +++ b/src/setup_topology.jl @@ -11,11 +11,11 @@ the function initializes the `EnergySystemDesign`. - **`design_path::String=""`** is a file path or identifier related to the design. - **`id_to_color_map::Dict`** is a dictionary of resources and their assigned colors. - **`id_to_icon_map::Dict`** is a dictionary of nodes and their assigned icons. -- **`x::Float32=0.0f0`** is the initial x-coordinate of the system. -- **`y::Float32=0.0f0`** is the initial y-coordinate of the system. +- **`xy_parent::Point2f=Point2f(0.0f0, 0.0f0)`** is the parent coordinate of the system. - **`icon::String=""`** is the optional (path to) icons associated with the system, stored as a string. - **`parent::AbstractGUIObj=NothingDesign()`** is a parent EnergySystemDesign object. +- **`level::Int64=0`** indicates the hierarchical level of the design in the system. The function reads system configuration data from a TOML file specified by `design_path` (if it exists), initializes various internal fields, and processes connections and wall values. @@ -27,10 +27,10 @@ function EnergySystemDesign( design_path::String = "", id_to_color_map::Dict = Dict(), id_to_icon_map::Dict = Dict(), - x::Float32 = 0.0f0, - y::Float32 = 0.0f0, + xy_parent::Point2f = Point2f(0.0f0, 0.0f0), icon::String = "", parent::AbstractGUIObj = NothingDesign(), + level::Int64 = 0, ) # Create the path to the file where existing design is stored (if any) file::String = design_file(system, design_path) @@ -49,16 +49,13 @@ function EnergySystemDesign( end # Initialize components and connections - components::Vector{EnergySystemDesign} = EnergySystemDesign[] - connections::Vector{Connection} = Connection[] - - # Create an observable for the coordinate xy that can be inherited as the coordinate - # parent_xy - xy::Observable{Point2f} = Observable(Point2f(x, y)) + components = EnergySystemDesign[] + connections = Connection[] # Create an iterator for the current system elements = get_children(system) - parent_x, parent_y = xy[] # extract parent coordinates + + visible::Observable{Bool} = Observable(level <= 1) design = EnergySystemDesign( system, @@ -67,18 +64,19 @@ function EnergySystemDesign( components, connections, parent, - xy, + Observable(xy_parent), icon, Observable(BLACK), Observable(:E), file, + visible, ) # If system contains any components (i.e. !isnothing(elements)) add all components # (constructed as an EnergySystemDesign) to `components` if !isnothing(elements) current_node::Int64 = 1 - nodes_count::Int64 = length(get_children(system)) + nodes_count = length(get_children(system)) # Loop through all components of `system` for element ∈ elements @@ -88,22 +86,20 @@ function EnergySystemDesign( # Extract x and y coordinates from file, or from structure or add defaults if haskey(system_info, "x") && haskey(system_info, "y") - x = Float32(system_info["x"]) - y = Float32(system_info["y"]) + xy = Point2f(system_info["x"], system_info["y"]) elseif isa(system, SystemGeo) if hasproperty(element, :lon) && hasproperty(element, :lat) # assigning longitude and latitude - x = Float32(element.lon) - y = Float32(element.lat) + xy = Point2f(element.lon, element.lat) else @error "Missing lon and/or lat coordinates" end else if element == get_ref_element(system) - x, y = parent_x, parent_y + xy = xy_parent else # place nodes in a circle around the parents availability node - x, y = place_nodes_in_circle( - nodes_count, current_node, 1.0f0, parent_x, parent_y, + xy = place_nodes_in_circle( + nodes_count, current_node, 1.0f0, xy_parent, ) current_node += 1 end @@ -120,16 +116,16 @@ function EnergySystemDesign( design_path, id_to_color_map, id_to_icon_map, - x, - y, + xy_parent = xy, icon = find_icon(this_sys, id_to_icon_map), parent = design, + level = level + 1, ), ) end end - # Add `Transmission`s and `Link`s to `connections` as a `Connection` + # Add `Transmission`s and `Link`s to `connections` as a `Connection` elements = get_connections(system) if !isnothing(elements) for element ∈ elements @@ -141,7 +137,10 @@ function EnergySystemDesign( # If `EnergySystemDesign`s found, create a new `Connection` if !isnothing(from) && !isnothing(to) - push!(design.connections, Connection(from, to, element, id_to_color_map)) + push!( + design.connections, + Connection(from, to, element, id_to_color_map, Observable(level == 0)), + ) end end end diff --git a/src/utils_GUI/GUI_utils.jl b/src/utils_GUI/GUI_utils.jl index a24c8a1..575a931 100644 --- a/src/utils_GUI/GUI_utils.jl +++ b/src/utils_GUI/GUI_utils.jl @@ -10,28 +10,26 @@ function toggle_selection_color!(gui::GUI, selection::EnergySystemDesign, select selection.color[] = selected ? get_selection_color(gui) : BLACK end function toggle_selection_color!(gui::GUI, selection::Connection, selected::Bool) - plots = selection.plots + plots = get_plots(selection) if selected for plot ∈ plots selection_color = get_selection_color(gui) - if isa(plot, Makie.AbstractPlot) + if isa(plot.color[], Vector) plot.color = fill(selection_color, length(plot.color[])) else - for plot_sub ∈ plot - plot_sub.color = selection_color - end + plot.color = selection_color end end else - colors::Vector{RGBA{Float32}} = selection.colors - no_colors::Int64 = length(colors) + colors = get_colors(selection) + no_colors = length(colors) + i::Int64 = 1 for plot ∈ plots - if isa(plot, Makie.AbstractPlot) + if isa(plot.color[], Vector) plot.color = colors else - for (i, plot_sub) ∈ enumerate(plot) - plot_sub.color = colors[((i-1)%no_colors)+1] - end + plot.color = colors[((i-1)%no_colors)+1] + i += 1 end end end @@ -49,22 +47,22 @@ structure of Makie, we must iteratively look through up to three nested layers t this object is stored. """ function get_EMGUI_obj(plt) - if isa(plt, AbstractPlot) - if haskey(plt.kw, :EMGUI_obj) - return plt.kw[:EMGUI_obj] - elseif isa(plt.parent, AbstractPlot) - if haskey(plt.parent.kw, :EMGUI_obj) - return plt.parent.kw[:EMGUI_obj] - elseif isa(plt.parent.parent, AbstractPlot) - if haskey(plt.parent.parent.kw, :EMGUI_obj) - return plt.parent.parent.kw[:EMGUI_obj] - elseif isa(plt.parent.parent.parent, AbstractPlot) && - haskey(plt.parent.parent.parent.kw, :EMGUI_obj) - return plt.parent.parent.parent.kw[:EMGUI_obj] - end - end - end + !isa(plt, AbstractPlot) && return nothing + + # Check current level + obj = get(plt.kw, :EMGUI_obj, nothing) + !isnothing(obj) && return obj + + # Check parent levels (up to 3 levels deep) + current = plt.parent + for _ ∈ 1:3 + !isa(current, AbstractPlot) && break + obj = get(current.kw, :EMGUI_obj, nothing) + !isnothing(obj) && return obj + current = current.parent end + + return nothing end """ @@ -95,16 +93,16 @@ function pick_component!(gui::GUI, element::Dict, ::Symbol) toggle_selection_color!(gui, element, true) end function pick_component!(gui::GUI, ::Nothing, ax_type::Symbol) - clear_selection(gui, ax_type) + clear_selection!(gui, ax_type) end """ - clear_selection(gui::GUI, ax_type::Symbol) + clear_selection!(gui::GUI, ax_type::Symbol) Clear the color selection of the topology axis if `ax_type = :topo`, and of the results axis if `ax_type = :results`. """ -function clear_selection(gui::GUI, ax_type::Symbol) +function clear_selection!(gui::GUI, ax_type::Symbol) if ax_type == :topo selected_systems = get_selected_systems(gui) for selection ∈ selected_systems @@ -119,6 +117,7 @@ function clear_selection(gui::GUI, ax_type::Symbol) toggle_selection_color!(gui, selection, false) end end + return nothing end """ @@ -342,27 +341,29 @@ function initialize_available_data!(gui) # Create investment overview in the information box total_opex = sum(tot_opex_unscaled .* sp_dur) total_capex = sum(tot_capex_unscaled) - investment_overview = "Result summary:\n\n" - investment_overview *= "Objective value: $(format_number(get_obj_value(model)))\n\n" - investment_overview *= "Investment summary (no values discounted):\n\n" - investment_overview *= "Total operational cost: $(format_number(total_opex))\n" - investment_overview *= "Total investment cost: $(format_number(total_capex))\n\n" - inv_overview_components = "" + io = IOBuffer() + println(io, "Result summary:\n") + println(io, "Objective value: $(format_number(get_obj_value(model)))\n") + println(io, "Investment summary (no values discounted):\n") + println(io, "Total operational cost: $(format_number(total_opex))") + println(io, "Total investment cost: $(format_number(total_capex))\n") + has_investments::Bool = false for obj ∈ design inv_times = get_inv_times(obj) if !isempty(inv_times) + if !has_investments + println(io, "Investment overview (CAPEX):") + has_investments = true + end capex = get_capex(obj) label = get_element_label(obj) - inv_overview_components *= "\t$label:\n" + println(io, "\t", label, ":") for (t, capex) ∈ zip(inv_times, capex) - inv_overview_components *= "\t\t$t: $(format_number(capex))\n" + println(io, "\t\t", t, ": ", format_number(capex)) end end end - if !isempty(inv_overview_components) - investment_overview *= "Investment overview (CAPEX):\n" - investment_overview *= inv_overview_components - end + investment_overview = String(take!(io)) summary_text = get_var(gui, :summary_text) summary_text[] = investment_overview else @@ -375,7 +376,7 @@ function initialize_available_data!(gui) for element ∈ plotables # Add timedependent input data (if available) if !isnothing(element) - available_data = Vector{PlotContainer}(undef, 0) + available_data = PlotContainer[] for field_name ∈ fieldnames(typeof(element)) field = getfield(element, field_name) structure = String(nameof(typeof(element))) @@ -506,8 +507,8 @@ function get_investment_times(gui::GUI, max_inst::Float64) model = get_model(gui) for component ∈ get_root_design(gui) element = get_element(component) - investment_times::Vector{String} = Vector{String}[] - investment_capex::Vector{Float64} = Vector{Float64}[] + investment_times = String[] + investment_capex = Float64[] for (i, t) ∈ enumerate(𝒯ᴵⁿᵛ) for investment_indicator ∈ investment_indicators # important not to use shorthand loop syntax here due to the break command (exiting both loops in that case) sym = Symbol(investment_indicator) @@ -684,13 +685,13 @@ function get_all_periods!(vec::Vector, ts::TwoLevel) end end function get_all_periods!(vec::Vector, ts::RepresentativePeriods) - append!(vec, repr_periods(T)) + append!(vec, repr_periods(ts)) for t ∈ ts.rep_periods get_all_periods!(vec, t) end end function get_all_periods!(vec::Vector, ts::OperationalScenarios) - append!(vec, opscenarios(T)) + append!(vec, opscenarios(ts)) for t ∈ ts.scenarios get_all_periods!(vec, t) end @@ -731,7 +732,16 @@ function transfer_model(model::String, system::AbstractSystem) metadata_path = joinpath(model, "metadata.yaml") data[:metadata] = YAML.load_file(metadata_path; dicttype = Dict{Symbol,Any}) 𝒯 = get_time_struct(system) - Threads.@threads for file ∈ files + + results = Vector{Pair{Symbol,DataFrame}}(undef, length(files)) + all_periods = Union{TS.TimePeriod,TS.TimeStructure}[] + get_all_periods!(all_periods, 𝒯) + periods_dict = get_repr_dict(unique(all_periods)) + products_dict = get_repr_dict(get_products(system)) + plotables_dict = get_repr_dict(get_plotables(system)) + + Threads.@threads for i ∈ eachindex(files) + file = files[i] varname = Symbol(basename(file)[1:(end-4)]) df = CSV.read(file, DataFrame) @@ -745,26 +755,32 @@ function transfer_model(model::String, system::AbstractSystem) end col_names = names(df) - all_periods = [] - get_all_periods!(all_periods, 𝒯) - df[!, :t] = convert_array(df[!, :t], get_repr_dict(unique(all_periods))) + df[!, :t] = convert_array(df[!, :t], periods_dict) if "res" ∈ col_names - df[!, :res] = convert_array( - df[!, :res], - get_repr_dict(get_products(system)), - ) + df[!, :res] = convert_array(df[!, :res], products_dict) end if "element" ∈ col_names - df[!, :element] = convert_array( - df[!, :element], - get_repr_dict(get_plotables(system)), - ) + df[!, :element] = convert_array(df[!, :element], plotables_dict) end - data[varname] = df + results[i] = varname => df + end + for (k, v) ∈ results + data[k] = v end else @warn "The model must be a directory containing the results. No results loaded." end return data end + +""" + sub_plots_empty(component::EnergySystemDesign) + +Check if any sub-component of `component` is missing plots. +""" +function sub_plots_empty(component::EnergySystemDesign) + return any( + isempty(get_plots(sub_component)) for sub_component ∈ get_components(component) + ) +end diff --git a/src/utils_GUI/event_functions.jl b/src/utils_GUI/event_functions.jl index 50a98f9..b576e69 100644 --- a/src/utils_GUI/event_functions.jl +++ b/src/utils_GUI/event_functions.jl @@ -9,9 +9,11 @@ function define_event_functions(gui::GUI) ax_info = get_ax(gui, :info) ax_summary = get_ax(gui, :summary) + expand_all_toggle = get_toggle(gui, :expand_all) + expand_all = get_var(gui, :expand_all) + # On zooming, make sure all graphics are adjusted acordingly on(ax_topo.finallimits; priority = 10) do finallimits - @debug "Changes in finallimits" widths::Vec = finallimits.widths origin::Vec = finallimits.origin get_vars(gui)[:xlimits] = [origin[1], origin[1] + widths[1]] @@ -33,9 +35,8 @@ function define_event_functions(gui::GUI) # If the window is resized, make sure all graphics are adjusted acordingly fig = get_fig(gui) - on(fig.scene.events.window_area; priority = 3) do val - @debug "Changes in window_area" - get_vars(gui)[:plot_widths] = Tuple(fig.scene.viewport.val.widths) + on(fig.scene.events.window_area; priority = 3) do _ + get_vars(gui)[:plot_widths] = fig.scene.viewport.val.widths get_vars(gui)[:ax_aspect_ratio] = get_var(gui, :plot_widths)[1] / (get_var(gui, :plot_widths)[2] - get_var(gui, :taskbar_height)) / 2 @@ -83,7 +84,7 @@ function define_event_functions(gui::GUI) end elseif Int(event.key) == 87 # ctrl+w: Close if ctrl_is_pressed - Threads.@spawn GLMakie.closeall() + GLMakie.closeall() end #elseif Int(event.key) == 340 # Shift #elseif Int(event.key) == 342 # Alt @@ -133,7 +134,7 @@ function define_event_functions(gui::GUI) ctrl_is_pressed = get_var(gui, :ctrl_is_pressed)[] if mouse_within_axis(ax_topo, mouse_pos) if !ctrl_is_pressed && !isempty(get_selected_systems(gui)) - clear_selection(gui, :topo) + clear_selection!(gui, :topo) end pick_component!(gui, :topo) @@ -150,7 +151,7 @@ function define_event_functions(gui::GUI) if mouse_within_axis(ax_results, mouse_pos) time_axis = time_menu.selection[] if !ctrl_is_pressed && !isempty(get_selected_plots(gui, time_axis)) - clear_selection(gui, :results) + clear_selection!(gui, :results) end pick_component!(gui, :results) gui.vars[:autolimits][time_axis] = false @@ -240,17 +241,20 @@ function define_event_functions(gui::GUI) gui, get_design(gui); visible = false, - expand_all = get_var(gui, :expand_all), + expand_all, ) + if isa(component, EnergySystemDesign) && sub_plots_empty(component) + initialize_plot!(gui, component) + end gui.design = component plot_design!( gui, get_design(gui); visible = true, - expand_all = get_var(gui, :expand_all), + expand_all, ) update_title!(gui) - clear_selection(gui, :topo) + clear_selection!(gui, :topo) notify(get_button(gui, :reset_view).clicks) end end @@ -260,18 +264,10 @@ function define_event_functions(gui::GUI) # Navigate up button: Handle click on the navigate up button (go back to the root_design) on(get_button(gui, :up).clicks; priority = 10) do clicks if !isa(get_parent(get_system(gui)), NothingElement) - get_vars(gui)[:expand_all] = get_toggle(gui, :expand_all).active[] - plot_design!( - gui, get_design(gui); visible = false, - expand_all = get_var(gui, :expand_all), - ) + get_vars(gui)[:expand_all] = expand_all_toggle.active[] + plot_design!(gui, get_design(gui); visible = false, expand_all) gui.design = get_root_design(gui) - plot_design!( - gui, get_design(gui); visible = true, expand_all = get_var( - gui, - :expand_all, - ), - ) + plot_design!(gui, get_design(gui); visible = true, expand_all) update_title!(gui) adjust_limits!(gui) notify(get_button(gui, :reset_view).clicks) @@ -334,15 +330,22 @@ function define_event_functions(gui::GUI) selection[:visible] = false selection[:pinned] = false end - clear_selection(gui, :results) + clear_selection!(gui, :results) update_legend!(gui) return Consume(false) end # Toggle expansion of all systems - on(get_toggle(gui, :expand_all).active; priority = 10) do val + on(expand_all_toggle.active; priority = 10) do val # Plot the topology get_vars(gui)[:expand_all] = val + if !get_vars(gui)[:pre_plot_sub_components] + for component ∈ get_components(get_design(gui)) + if sub_plots_empty(component) + initialize_plot!(gui, component) + end + end + end plot_design!(gui, get_design(gui); expand_all = val) update_distances!(gui) return Consume(false) diff --git a/src/utils_GUI/info_axis_utils.jl b/src/utils_GUI/info_axis_utils.jl index dfb9063..bd8b475 100644 --- a/src/utils_GUI/info_axis_utils.jl +++ b/src/utils_GUI/info_axis_utils.jl @@ -9,37 +9,40 @@ function update_info_box!(gui::GUI, element) info_text[] = get_var(gui, :default_text) return nothing end - info_text[] = print_nested_structure!( + io = IOBuffer() + print_nested_structure!( element, - ""; + io; vector_limit = 5, show_the_n_last_elements = 1, ) + info_text[] = String(take!(io)) end """ print_nested_structure!( element, - output::Observable; + io::IOBuffer; indent::Int64=0, vector_limit::Int64=typemax(Int64), ) -Appends the nested structure of element in a nice format to the output[] string. The +Appends the nested structure of element in a nice format to the io buffer. The parameter `vector_limit` is used to truncate large vectors. """ function print_nested_structure!( element, - output::String; + io::IOBuffer; indent::Int64 = 0, vector_limit::Int64 = typemax(Int64), show_the_n_last_elements::Int64 = 3, ) if indent == 0 + type = typeof(element) if isa(element, Dict) || isa(element, Vector) - output *= "$(typeof(element))\n" + println(io, type) else - output *= "$element ($(typeof(element)))\n" + println(io, element, " (", type, ")") end end indent += 1 @@ -61,62 +64,60 @@ function print_nested_structure!( if eltype(element) <: expandable for (i, field1) ∈ enumerate(element) if i == vector_limit + 1 - output *= indent_str * "...\n" + println(io, indent_str, "...") continue end if i <= vector_limit || i > length(element) - show_the_n_last_elements + type = typeof(field1) if isa(field1, expandable) - output *= indent_str * "$i ($(typeof(field1))):\n" - output = - print_nested_structure!(field1, output; indent, vector_limit) + println(io, indent_str, i, " (", type, "):") + print_nested_structure!(field1, io; indent, vector_limit) else - output *= indent_str * "$i: $(typeof(field1))($field1)\n" + println(io, indent_str, i, ": ", type, "(", field1, ")") end end end else - output *= indent_str * "[" + print(io, indent_str, "[") for (i, field1) ∈ enumerate(element) if i == vector_limit + 1 - output *= " ... " + print(io, " ... ") continue end if i <= vector_limit || i > length(element) - show_the_n_last_elements - output *= "$field1" + print(io, field1) if i != length(element) - output *= ", " + print(io, ", ") end end end - output *= "]\n" + println(io, "]") end elseif isa(element, Dict) for field1 ∈ keys(element) if isa(element[field1], expandable) - output *= indent_str * "$field1 ($(typeof(element[field1]))):\n" - output = - print_nested_structure!(element[field1], output; indent, vector_limit) + println(io, indent_str, field1, " (", typeof(element[field1]), "):") + print_nested_structure!(element[field1], io; indent, vector_limit) else - output *= indent_str * "$field1 => $(element[field1])\n" + println(io, indent_str, field1, " => ", element[field1]) end end else for field1 ∈ fieldnames(typeof(element)) value1 = getfield(element, field1) if isa(value1, expandable) - output *= indent_str * "$(field1) ($(typeof(value1))):\n" - output = print_nested_structure!(value1, output; indent, vector_limit) + println(io, indent_str, field1, " (", typeof(value1), "):") + print_nested_structure!(value1, io; indent, vector_limit) else if isa(value1, OperationalProfile) && !isa(value1, FixedProfile) && length(value1.vals) > vector_limit # Truncate large vectors - output *= indent_str * "$(field1): $(typeof(value1))\n" + println(io, indent_str, field1, ": ", typeof(value1)) else - output *= indent_str * "$(field1): $value1\n" + println(io, indent_str, field1, ": ", value1) end end end end - return output end diff --git a/src/utils_GUI/results_axis_utils.jl b/src/utils_GUI/results_axis_utils.jl index 6427bfb..8228787 100644 --- a/src/utils_GUI/results_axis_utils.jl +++ b/src/utils_GUI/results_axis_utils.jl @@ -300,29 +300,32 @@ end Return a label for a given `selection` to be used in the get_menus(gui)[:available_data] menu. """ function create_label(selection::PlotContainer) - label::String = isa(selection, CaseDataContainer) ? "Case data: " : "" + io = IOBuffer() + if isa(selection, CaseDataContainer) + print(io, "Case data: ") + end if isempty(get_name(selection)) - label *= get_description(selection) + print(io, get_description(selection)) else - label *= get_description(selection) * " ($(get_name(selection)))" + print(io, get_description(selection), " (", get_name(selection), ")") end otherRes::Bool = false for select ∈ get_selection(selection) if isa(select, Resource) if !otherRes - label *= " (" + print(io, " (") otherRes = true end - label *= "$(select)" + print(io, string(select)) if select != get_selection(selection)[end] - label *= ", " + print(io, ", ") end end end if otherRes - label *= ")" + print(io, ")") end - return label + return String(take!(io)) end """ @@ -582,6 +585,7 @@ function update_legend!(gui::GUI) ) legend.entrygroups[] = entry_groups end + return nothing end """ diff --git a/src/utils_GUI/topo_axis_utils.jl b/src/utils_GUI/topo_axis_utils.jl index 2d38c72..a698493 100644 --- a/src/utils_GUI/topo_axis_utils.jl +++ b/src/utils_GUI/topo_axis_utils.jl @@ -27,13 +27,13 @@ Find the minimum distance between the elements in the design object `gui` and up that neighbouring icons do not overlap. """ function update_distances!(gui::GUI) - min_d::Float32 = Inf + min_d::Float32 = Inf32 design = get_design(gui) components = get_components(design) if length(components) > 1 for component ∈ components d::Float32 = minimum([ - l2_norm(collect(get_xy(component)[] .- get_xy(component2)[])) for + l2_norm(get_xy(component)[] .- get_xy(component2)[]) for component2 ∈ components if component != component2 ]) if d < min_d @@ -90,7 +90,7 @@ function align(gui::GUI, align::Symbol) ys::Vector{Float32} = Float32[] for sub_design ∈ get_selected_systems(gui) if isa(sub_design, EnergySystemDesign) - x, y = sub_design.xy[] + x, y = get_xy(sub_design)[] push!(xs, x) push!(ys, y) end @@ -105,12 +105,13 @@ function align(gui::GUI, align::Symbol) for sub_design ∈ get_selected_systems(gui) if isa(sub_design, EnergySystemDesign) - x, y = sub_design.xy[] + xy = get_xy(sub_design) + x, y = xy[] if align == :horizontal - sub_design.xy[] = (x, z) + xy[] = (x, z) elseif align == :vertical - sub_design.xy[] = (z, y) + xy[] = (z, y) end end end @@ -124,7 +125,9 @@ Initialize the plot of the topology of design object `gui` given an EnergySystem """ function initialize_plot!(gui::GUI, design::EnergySystemDesign) for component ∈ get_components(design) - initialize_plot!(gui, component) + if get_var(gui, :pre_plot_sub_components) + initialize_plot!(gui, component) + end add_component!(gui, component) end return connect!(gui, design) @@ -148,17 +151,9 @@ function plot_design!( if get_design(gui) == design update_distances!(gui) end - for component ∈ get_components(design), plot ∈ get_plots(component) - plot.visible = visible - end - for connection ∈ get_connections(design), plots ∈ get_plots(connection) - if isa(plots, Makie.AbstractPlot) # handle the arrowheads (scatter! object) - plots.visible = visible - else # handle the lines (vector of line! objects) - for plot_sub ∈ plots - plot_sub.visible = visible - end - end + components_and_connections = vcat(get_components(design), get_connections(design)) + for component ∈ components_and_connections + component.visible[] = visible end end @@ -192,8 +187,8 @@ end When a boolean argument `two_way` is specified, draw the lines in both directions. """ function connect!(gui::GUI, connection::Connection, two_way::Bool) - colors::Vector{RGB} = get_colors(connection) - no_colors::Int64 = length(colors) + colors = get_colors(connection) + no_colors = length(colors) # Create an arrow to highlight the direction of the energy flow l::Float32 = 1.0f0 # length of the arrow @@ -221,81 +216,78 @@ function connect!(gui::GUI, connection::Connection, two_way::Bool) Δh = get_var(gui, :Δh) triple = @lift begin - xy_midpoints = Vector{Point2f}(fill(Point2f(0.0f0, 0.0f0), no_colors)) - θs = Vector{Float32}(fill(0.0f0, no_colors)) - pts_lines = - Vector{Vector{Point2f}}(fill(fill(Point2f(0.0f0, 0.0f0), 2), no_colors)) - for j ∈ 1:no_colors - lines_shift::Point2f = - pixel_to_data(gui, linewidth) .+ - pixel_to_data(gui, line_sep_px) - two_way_sep::Point2f = pixel_to_data(gui, two_way_sep_px) - markersize_lengths::Point2f = pixel_to_data(gui, markersize) - - xy_1::Point2f = $from_xy - xy_2::Point2f = $to_xy - - θ::Float32 = atan(xy_2[2] - xy_1[2], xy_2[1] - xy_1[1]) - cosθ::Float32 = cos(θ) - sinθ::Float32 = sin(θ) - cosϕ::Float32 = -sinθ # where ϕ = θ+π/2 - sinϕ::Float32 = cosθ - - # Create directional vectors in the direction of θ and ϕ - dirϕ::Point2f = Point2f(cosϕ, sinϕ) - dirθ::Point2f = Point2f(cosθ, sinθ) - - Δ::Float32 = $Δh / 2 # half width of a box - if !isempty(get_components(connection.from)) - Δ *= parent_scaling - end + pts_lines = Vector{Vector{Point2f}}(fill(fill(Point2f(0.0f0, 0.0f0), 2), no_colors)) + markersize_length::Float32 = l2_norm(pixel_to_data(gui, markersize)) + linewidth_length::Float32 = l2_norm(pixel_to_data(gui, linewidth)) + + xy_1::Point2f = $from_xy + xy_2::Point2f = $to_xy + + θ::Float32 = atan(xy_2[2] - xy_1[2], xy_2[1] - xy_1[1]) + cosθ::Float32 = cos(θ) + sinθ::Float32 = sin(θ) + cosϕ::Float32 = -sinθ # where ϕ = θ+π/2 + sinϕ::Float32 = cosθ + + # Create directional vectors in the direction of θ and ϕ + dirϕ::Point2f = Point2f(cosϕ, sinϕ) + dirθ::Point2f = Point2f(cosθ, sinθ) + + Δ::Float32 = $Δh / 2 # half width of a box + if !isempty(get_components(connection.from)) + Δ *= parent_scaling + end + Δ += linewidth_length / 2 + lines_shift::Point2f = + (linewidth_length + l2_norm(pixel_to_data(gui, line_sep_px))) * dirϕ + two_way_sep::Point2f = l2_norm(pixel_to_data(gui, two_way_sep_px)) * dirϕ + xy_midpoint::Point2f = xy_2 + (no_colors - 1) / 2 * lines_shift + if two_way # separate the opposite directed lines by two_way_sep + xy_midpoint += two_way_sep / 2 + end + xy_midpoint = square_intersection(xy_2, xy_midpoint, θ + π32, Δ) - xy_start::Point2f = xy_1 + (j - 1) * lines_shift .* dirϕ - xy_end::Point2f = xy_2 + (j - 1) * lines_shift .* dirϕ - xy_midpoint::Point2f = xy_2 + (no_colors - 1) / 2 * lines_shift .* dirϕ # The midpoint of the end of all lines (for arrow head) + for j ∈ 1:no_colors + xy_start::Point2f = xy_1 + (j - 1) * lines_shift + xy_end::Point2f = xy_2 + (j - 1) * lines_shift if two_way # separate the opposite directed lines by two_way_sep - xy_start += two_way_sep / 2 .* dirϕ - xy_end += two_way_sep / 2 .* dirϕ - xy_midpoint += two_way_sep / 2 .* dirϕ + xy_start += two_way_sep / 2 + xy_end += two_way_sep / 2 end xy_start = square_intersection(xy_1, xy_start, θ, Δ) - xy_end = square_intersection(xy_2, xy_end, θ + π, Δ) - xy_midpoint = square_intersection(xy_2, xy_midpoint, θ + π, Δ) + xy_end = square_intersection(xy_2, xy_end, θ + π32, Δ) parm::Float32 = -xy_start[1] * cosθ - xy_start[2] * sinθ + xy_midpoint[1] * cosθ + - xy_midpoint[2] * sinθ - minimum(markersize_lengths) + xy_midpoint[2] * sinθ - markersize_length / 2 - xy_midpoints[j] = xy_midpoint - θs[j] = θ pts_lines[j] = Point2f[xy_start, parm*dirθ+xy_start] end # return the objects into a tuple Observable - (xy_midpoints, θs, pts_lines) + (xy_midpoint, θ, pts_lines) end # Extract observables from the tuple - xy_midpoints = @lift $triple[1] - θs = @lift $triple[2] + xy_midpoints = @lift fill($triple[1], no_colors) + θs = @lift fill($triple[2], no_colors) ax = get_ax(gui, :topo) sctr = scatter!( ax, xy_midpoints; marker = arrow_parts, - markersize = get_var(gui, :markersize), + markersize = markersize, rotation = θs, color = colors, inspectable = false, + depth_shift = get_var(gui, :depth_shift_lines), + visible = get_visible(connection), ) - Makie.translate!(sctr, 0, 0, get_var(gui, :z_translate_lines)) sctr.kw[:EMGUI_obj] = connection push!(get_plots(connection), sctr) - lns_arr::Vector{Makie.AbstractPlot} = Makie.AbstractPlot[] # to store the line plots - for j ∈ 1:no_colors pts_lines = @lift $triple[3][j] lns = lines!( @@ -306,14 +298,14 @@ function connect!(gui::GUI, connection::Connection, two_way::Bool) linestyle = linestyle[j], inspector_label = (self, i, p) -> get_hover_string(connection), inspectable = true, + depth_shift = get_var(gui, :depth_shift_lines), + visible = get_visible(connection), ) - Makie.translate!(lns, 0, 0, get_var(gui, :z_translate_lines)) lns.kw[:EMGUI_obj] = connection - push!(lns_arr, lns) + push!(get_plots(connection), lns) end - push!(get_plots(connection), lns_arr) - get_vars(gui)[:z_translate_lines] += 0.0001f0 + get_vars(gui)[:depth_shift_lines] -= 1.0f-5 end """ @@ -385,7 +377,7 @@ function draw_box!(gui::GUI, design::EnergySystemDesign) linestyle::Union{Symbol,Makie.Linestyle} = get_linestyle(gui, design) Δh = get_var(gui, :Δh) - xy = design.xy + xy = get_xy(design) # if the design has components, draw an enlarged box around it. if !isempty(get_components(design)) @@ -403,13 +395,15 @@ function draw_box!(gui::GUI, design::EnergySystemDesign) strokewidth = get_var(gui, :linewidth), strokecolor = design.color, linestyle = linestyle, + depth_shift = get_var(gui, :depth_shift_components), + stroke_depth_shift = get_var(gui, :depth_shift_components) - 1.0f-5, + visible = get_visible(design), ) # Create a white background rectangle to hide lines from connections add_inspector_to_poly!(white_rect2, (self, i, p) -> get_hover_string(design)) - Makie.translate!(white_rect2, 0.0f0, 0.0f0, get_var(gui, :z_translate_components)) - get_vars(gui)[:z_translate_components] += 0.0001f0 - push!(design.plots, white_rect2) white_rect2.kw[:EMGUI_obj] = design + push!(get_plots(design), white_rect2) + get_vars(gui)[:depth_shift_components] -= 2.0f-5 end # Build the rectangle path from the observables @@ -423,14 +417,16 @@ function draw_box!(gui::GUI, design::EnergySystemDesign) strokewidth = get_var(gui, :linewidth), strokecolor = design.color, linestyle = linestyle, + depth_shift = get_var(gui, :depth_shift_components), + stroke_depth_shift = get_var(gui, :depth_shift_components) - 1.0f-5, + visible = get_visible(design), ) # Create a white background rectangle to hide lines from connections add_inspector_to_poly!(white_rect, (self, i, p) -> get_hover_string(design)) - Makie.translate!(white_rect, 0.0f0, 0.0f0, get_var(gui, :z_translate_components)) - get_vars(gui)[:z_translate_components] += 0.0001f0 - push!(design.plots, white_rect) + push!(get_plots(design), white_rect) white_rect.kw[:EMGUI_obj] = design + get_vars(gui)[:depth_shift_components] -= 2.0f-5 end """ @@ -440,6 +436,7 @@ Draw an icon for EnergySystemDesign `design`. """ function draw_icon!(gui::GUI, design::EnergySystemDesign) ax = get_axes(gui)[:topo] + xy = get_xy(design) if isempty(design.icon) # No path to an icon has been found node::EMB.Node = get_ref_element(design) @@ -459,8 +456,7 @@ function draw_icon!(gui::GUI, design::EnergySystemDesign) (:triangle, 4) end - no_polygons::Int64 = length(all_colors) - xy = design.xy + no_polygons = length(all_colors) Δh = get_var(gui, :Δh) icon_scale = get_var(gui, :icon_scale) node_isa_networknode::Bool = isa(node, NetworkNode) @@ -479,11 +475,11 @@ function draw_icon!(gui::GUI, design::EnergySystemDesign) # Check if node is a NetworkNode (if so, devide disc into two where # left side is for input and right side is for output) if node_isa_networknode - θᵢ = (-1)^(j + 1) * π / 2 + π * (i - 1) / no_colors - θᵢ₊₁ = (-1)^(j + 1) * π / 2 + π * i / no_colors + θᵢ = (-1)^(j + 1) * π32 / 2 + π32 * (i - 1) / no_colors + θᵢ₊₁ = (-1)^(j + 1) * π32 / 2 + π32 * i / no_colors else - θᵢ = 2π * (i - 1) / no_colors - θᵢ₊₁ = 2π * i / no_colors + θᵢ = 2π32 * (i - 1) / no_colors + θᵢ₊₁ = 2π32 * i / no_colors end poly_points[idx] = get_sector_points(; c = $xy, @@ -499,21 +495,22 @@ function draw_icon!(gui::GUI, design::EnergySystemDesign) poly_points end - polys = poly!(ax, poly_points_obs; color = all_colors, inspectable = true) - add_inspector_to_poly!(polys, (self, i, p) -> get_hover_string(design)) - Makie.translate!( - polys, - 0.0f0, - 0.0f0, - get_var(gui, :z_translate_components), + polys = poly!( + ax, + poly_points_obs; + color = all_colors, + inspectable = true, + depth_shift = get_var(gui, :depth_shift_components), + stroke_depth_shift = get_var(gui, :depth_shift_components) - 1.0f-5, + visible = get_visible(design), ) + add_inspector_to_poly!(polys, (self, i, p) -> get_hover_string(design)) polys.kw[:EMGUI_obj] = design - push!(design.plots, polys) + push!(get_plots(design), polys) if node_isa_networknode # Add a center box to separate input resources from output resources Δh = get_var(gui, :Δh) - xy = design.xy box = @lift begin Δ = $Δh * get_var(gui, :icon_scale) / 4 @@ -526,24 +523,20 @@ function draw_icon!(gui::GUI, design::EnergySystemDesign) color = WHITE, inspectable = true, strokewidth = get_var(gui, :linewidth), + depth_shift = get_var(gui, :depth_shift_components), + stroke_depth_shift = get_var(gui, :depth_shift_components) - 1.0f-5, + visible = get_visible(design), ) add_inspector_to_poly!( center_box, (self, i, p) -> get_hover_string(design), ) - Makie.translate!( - center_box, - 0.0f0, - 0.0f0, - get_var(gui, :z_translate_components), - ) center_box.kw[:EMGUI_obj] = design - push!(design.plots, center_box) + push!(get_plots(design), center_box) end else Δh = get_var(gui, :Δh) - xy = design.xy scale = get_var(gui, :icon_scale) xo_image = @lift ($xy[1] - $Δh * scale / 2) .. ($xy[1] + $Δh * scale / 2) yo_image = @lift ($xy[2] - $Δh * scale / 2) .. ($xy[2] + $Δh * scale / 2) @@ -555,12 +548,13 @@ function draw_icon!(gui::GUI, design::EnergySystemDesign) rotr90(FileIO.load(design.icon)); inspectable = true, inspector_label = (self, i, p) -> get_hover_string(design), + depth_shift = get_var(gui, :depth_shift_components), + visible = get_visible(design), ) - Makie.translate!(icon_image, 0.0f0, 0.0f0, get_var(gui, :z_translate_components)) icon_image.kw[:EMGUI_obj] = design - push!(design.plots, icon_image) + push!(get_plots(design), icon_image) end - get_vars(gui)[:z_translate_components] += 0.0001f0 + get_vars(gui)[:depth_shift_components] -= 2.0f-5 end """ @@ -597,10 +591,10 @@ function draw_label!(gui::GUI, component::EnergySystemDesign) linked_component ∈ linked_from_component ], ) - min_angle_diff::Vector{Float32} = fill(Inf, 4) + min_angle_diff::Vector{Float32} = fill(Inf32, 4) for i ∈ eachindex(min_angle_diff) for angle ∈ angles - Δθ::Float32 = angle_difference(angle, (i - 1) * Float32(π) / 2) + Δθ::Float32 = angle_difference(angle, (i - 1) * π32 / 2) if min_angle_diff[i] > Δθ min_angle_diff[i] = Δθ end @@ -635,11 +629,12 @@ function draw_label!(gui::GUI, component::EnergySystemDesign) fontsize = get_var(gui, :fontsize), inspectable = false, color = has_invested(component) ? RED : BLACK, + depth_shift = get_var(gui, :depth_shift_components), + visible = get_visible(component), ) - Makie.translate!(label_text, 0.0f0, 0.0f0, get_var(gui, :z_translate_components)) - get_vars(gui)[:z_translate_components] += 0.0001f0 label_text.kw[:EMGUI_obj] = component push!(get_plots(component), label_text) + get_vars(gui)[:depth_shift_components] -= 2.0f-5 end """ @@ -696,8 +691,7 @@ function adjust_limits!(gui::GUI) end vars[:xlimits] = [min_x, max_x] vars[:ylimits] = [min_y, max_y] - ax = get_ax(gui, :topo) - limits!(ax, vars[:xlimits], vars[:ylimits]) + limits!(get_ax(gui, :topo), vars[:xlimits], vars[:ylimits]) end """ @@ -724,13 +718,13 @@ function get_hover_string(obj::AbstractGUIObj) element = get_element(obj) label = get_element_label(element) inv_times = get_inv_times(obj) - inv_str = "$label ($(nameof(typeof(element))))" + io = IOBuffer() + print(io, "$label ($(nameof(typeof(element))))") if !isempty(inv_times) capex = get_capex(obj) - label = get_element_label(obj) - for (t, capex) ∈ zip(inv_times, capex) - inv_str *= "\n\t$t: $(format_number(capex))" + for (t, c) ∈ zip(inv_times, capex) + print(io, "\n\t", t, ": ", format_number(c)) end end - return inv_str + return String(take!(io)) end diff --git a/src/utils_gen/export_utils.jl b/src/utils_gen/export_utils.jl index bbe0659..b2a4d9e 100644 --- a/src/utils_gen/export_utils.jl +++ b/src/utils_gen/export_utils.jl @@ -96,7 +96,7 @@ function export_xlsx(plots::Vector, filename::String, xlabel::Symbol) end labels::Vector{String} = [plot[:name] for plot ∈ plots] - headers::Vector{Any} = vcat(xlabel, labels) + headers::Vector{String} = vcat(string(xlabel), labels) #XLSX.rename!(sheet, "My Data Sheet") XLSX.writetable!(sheet, data, headers) @@ -281,8 +281,8 @@ function export_to_repl(gui::GUI) data = Matrix{Any}(undef, length(t), length(vis_plots) + 1) data[:, 1] = t header = [ - Vector{Any}(undef, length(vis_plots) + 1), - Vector{Any}(undef, length(vis_plots) + 1), + Vector{String}(undef, length(vis_plots) + 1), + Vector{String}(undef, length(vis_plots) + 1), ] header[1][1] = "t" header[2][1] = "(" * string(nameof(eltype(t))) * ")" diff --git a/src/utils_gen/structures_utils.jl b/src/utils_gen/structures_utils.jl index 5fa8809..9d610ba 100644 --- a/src/utils_gen/structures_utils.jl +++ b/src/utils_gen/structures_utils.jl @@ -25,16 +25,14 @@ loaded() = [ ] """ - place_nodes_in_circle(total_nodes::Int64, current_node::Int64, r::Float32, xₒ::Float32, yₒ::Float32) + place_nodes_in_circle(total_nodes::Int64, current_node::Int64, r::Float32, c::Point2f) Return coordinate for point number `i` of a total of `n` points evenly distributed around -a circle of radius `r` centered at (xₒ, yₒ) from -π/4 to 5π/4. +a circle of radius `r` centered at `c` from -π/4 to 5π/4. """ -function place_nodes_in_circle(n::Int64, i::Int64, r::Float32, xₒ::Float32, yₒ::Float32) - θ::Float32 = n == 1 ? π : -π / 4 + 3π / 2 * (1 - (i - 1) / (n - 1)) - x::Float32 = xₒ + r * cos(θ) - y::Float32 = yₒ + r * sin(θ) - return x, y +function place_nodes_in_circle(n::Int64, i::Int64, r::Float32, c::Point2f) + θ::Float32 = n == 1 ? π32 : -π32 / 4 + 3π32 / 2 * (1 - (i - 1) / (n - 1)) + return c + r * Point2f(cos(θ), sin(θ)) end """ @@ -220,7 +218,7 @@ function save_design(design::EnergySystemDesign) for component ∈ get_components(design) # Extract x,y-coordinates - x, y = component.xy[] + x, y = get_xy(component)[] design_dict[string(get_parent(get_system(component)))] = Dict( :x => round(x; digits = 5), :y => round(y; digits = 5), @@ -232,8 +230,8 @@ function save_design(design::EnergySystemDesign) end end - @info "Saving design coordinates to file $(design.file)" - return save_design(design_dict, design.file) + @info "Saving design coordinates to file $(get_file(design))" + return save_design(design_dict, get_file(design)) end """ diff --git a/src/utils_gen/topo_utils.jl b/src/utils_gen/topo_utils.jl index 3bb642d..56da6d4 100644 --- a/src/utils_gen/topo_utils.jl +++ b/src/utils_gen/topo_utils.jl @@ -15,47 +15,81 @@ function square_intersection(c::Point2f, x::Point2f, θ::Float32, Δ::Float32) ymin, ymax = c[2] - Δ, c[2] + Δ # Parametric line: X = x[1] + t*dx, Y = x[2] + t*dy - ts = Float32[] - ts_out = Float32[] + ts = Float32[] # only true edge intersections # Check intersection with vertical sides (x = xmin and x = xmax) - if abs(dx) > eps() + if abs(dx) > eps(Float32) t1 = (xmin - x[1]) / dx y1 = x[2] + t1 * dy if ymin <= y1 <= ymax push!(ts, t1) - else - push!(ts_out, t1) end + t2 = (xmax - x[1]) / dx y2 = x[2] + t2 * dy if ymin <= y2 <= ymax push!(ts, t2) - else - push!(ts_out, t2) end end + # Check intersection with horizontal sides (y = ymin and y = ymax) - if abs(dy) > eps() + if abs(dy) > eps(Float32) t3 = (ymin - x[2]) / dy x3 = x[1] + t3 * dx if xmin <= x3 <= xmax push!(ts, t3) - else - push!(ts_out, t3) end + t4 = (ymax - x[2]) / dy x4 = x[1] + t4 * dx if xmin <= x4 <= xmax push!(ts, t4) - else - push!(ts_out, t4) end end - tmin = isempty(ts) ? minimum(abs.(ts_out)) : minimum(abs.(ts)) + if !isempty(ts) + # prefer intersections in the *forward* direction + forward = filter(t -> t ≥ 0, ts) + if !isempty(forward) + tmin = minimum(forward) + else + # no forward hit, take the nearest edge in either direction + tmin = ts[argmin(abs.(ts))] + end + return x + tmin * Point2f(dx, dy) + else + # No intersection with the square: extend the two facing sides and intersect + # Determine which corner is closest to the line direction from x + # The line points in direction (dx, dy) from x + + # Find which quadrant the direction points to relative to square center + cx_relative = c[1] - x[1] + cy_relative = c[2] - x[2] + + # Determine the corner by the signs of dx and dy + corner_x = dx > 0 ? xmax : xmin + corner_y = dy > 0 ? ymax : ymin + + # Extend the two sides meeting at this corner + if abs(dy) > 1/sqrt(2.0f0) # First side: parallel to x-axis through corner_y + t_horizontal = (corner_y - x[2]) / dy + x_on_horizontal = x[1] + t_horizontal * dx + # Check if this is in the right direction and on the extended side + if t_horizontal ≥ 0 + return Point2f(x_on_horizontal, corner_y) + end + else # Second side: parallel to y-axis through corner_x + t_vertical = (corner_x - x[1]) / dx + y_on_vertical = x[2] + t_vertical * dy + # Check if this is in the right direction and on the extended side + if t_vertical ≥ 0 + return Point2f(corner_x, y_on_vertical) + end + end - return x + tmin * Point2f(dx, dy) + # Fallback + return Point2f(corner_x, corner_y) + end end """ @@ -69,11 +103,11 @@ function square_intersection(c::Point2f, θ::Float32, Δ::Float32) end """ - l2_norm(x::Vector{<:Real}) + l2_norm(x::Point2f) Compute the l2-norm of a vector. """ -function l2_norm(x::Vector{<:Real}) +function l2_norm(x::Point2f) return sqrt(sum(x .^ 2)) end @@ -93,7 +127,7 @@ function find_min_max_coordinates( design::EnergySystemDesign, min_x::Number, max_x::Number, min_y::Number, max_y::Number, ) if !isa(get_parent(get_system(design)), NothingElement) - x, y = design.xy[][1], design.xy[][2] + x, y = get_xy(design)[][1], get_xy(design)[][2] min_x = min(min_x, x) max_x = max(max_x, x) min_y = min(min_y, y) @@ -125,7 +159,9 @@ Based on the location of `node_1` and `node_2`, return the angle between the x-a `node_2` with `node_1` being the origin. """ function angle(node_1::EnergySystemDesign, node_2::EnergySystemDesign) - return atan(node_2.xy[][2] - node_1.xy[][2], node_2.xy[][1] - node_1.xy[][1]) + xy_1 = get_xy(node_1)[] + xy_2 = get_xy(node_2)[] + return atan(xy_2[2] - xy_1[2], xy_2[1] - xy_1[1]) end """ @@ -134,8 +170,8 @@ end Compute the difference between two angles. """ function angle_difference(angle1::Float32, angle2::Float32) - diff::Float32 = abs(angle1 - angle2) % Float32(2π) - return min(diff, Float32(2π) - diff) + diff::Float32 = abs(angle1 - angle2) % 2π32 + return min(diff, 2π32 - diff) end """ @@ -179,7 +215,7 @@ end center::Point2f = Point2f(0.0f0, 0.0f0), Δ::Float32 = 1.0f0, θ₁::Float32 = 0.0f0, - θ₂::Float32 = Float32(π / 4), + θ₂::Float32 = π32 / 4, steps::Int=200, geometry::Symbol = :circle) @@ -191,28 +227,28 @@ function get_sector_points(; c::Point2f = Point2f(0.0f0, 0.0f0), Δ::Float32 = 1.0f0, θ₁::Float32 = 0.0f0, - θ₂::Float32 = Float32(π / 4), + θ₂::Float32 = π32 / 4, steps::Int = 200, geometry::Symbol = :circle, ) if geometry == :circle - θ::Vector{Float32} = LinRange(θ₁, θ₂, Int(round(steps * (θ₂ - θ₁) / (2π)))) + θ::Vector{Float32} = LinRange(θ₁, θ₂, Int(round(steps * (θ₂ - θ₁) / (2π32)))) x_coords::Vector{Float32} = Δ * cos.(θ) .+ c[1] y_coords::Vector{Float32} = Δ * sin.(θ) .+ c[2] # Include the center and close the polygon return Point2f[c, collect(zip(x_coords, y_coords))..., c] elseif geometry == :rect - if θ₁ == 0 && θ₂ ≈ 2π + if θ₁ == 0 && θ₂ ≈ 2π32 x_coords, y_coords = box(c[1], c[2], Δ) return collect(zip(x_coords, y_coords)) else xy1 = square_intersection(c, θ₁, Δ) xy2 = square_intersection(c, θ₂, Δ) vertices = Point2f[c, xy1] - xsign = [1, -1, -1, 1] - ysign = [1, 1, -1, -1] - for (i, corner_angle) ∈ enumerate(Float32[π/4, 3π/4, 5π/4, 7π/4]) + xsign = Float32[1, -1, -1, 1] + ysign = Float32[1, 1, -1, -1] + for (i, corner_angle) ∈ enumerate([π32/4, 3π32/4, 5π32/4, 7π32/4]) if θ₁ < corner_angle && θ₂ > corner_angle push!(vertices, c .+ (Δ * xsign[i], Δ * ysign[i])) end @@ -222,11 +258,11 @@ function get_sector_points(; return vertices end elseif geometry == :triangle - input::Bool = (θ₁ + θ₂) / 2 > π / 2 + input::Bool = (θ₁ + θ₂) / 2 > π32 / 2 if input # input resources on a triangle to the left - f = θ -> -2Δ * θ / π + 2Δ + f = θ -> -2Δ * θ / π32 + 2Δ else # output resources on a triangle to the right - f = θ -> 2Δ * θ / π + f = θ -> 2Δ * θ / π32 end d::Float32 = Δ / 2 x::Point2f = input ? c .- (d / 2, 0) : c .+ (d / 2, 0) diff --git a/src/utils_gen/utils.jl b/src/utils_gen/utils.jl index f6c1705..14dfd71 100644 --- a/src/utils_gen/utils.jl +++ b/src/utils_gen/utils.jl @@ -1,5 +1,6 @@ # Define a type for sparse variables to simplify code const SparseVars = Union{JuMP.Containers.SparseAxisArray,SparseVariables.IndexedVarArray} +const π32 = Float32(π) """ get_representative_period_indices(T::TS.TimeStructure, sp::Int64) diff --git a/test/EMI_geography_2.jl b/test/EMI_geography_2.jl new file mode 100644 index 0000000..1842406 --- /dev/null +++ b/test/EMI_geography_2.jl @@ -0,0 +1,285 @@ +function generate_example_data_geo_all_resources() + NG = ResourceEmit("NG", 0.2) + Coal = ResourceCarrier("Coal", 0.35) + Power = ResourceCarrier("Power", 0.0) + CO2 = ResourceEmit("CO2", 1.0) + H2 = ResourceCarrier("H2", 0.0) + Waste = ResourceCarrier("Waste", 0.0) + Biomass = ResourceCarrier("Biomass", 0.0) + Oil = ResourceCarrier("Oil", 0.3) + products = [NG, Coal, Power, CO2, H2, Waste, Biomass, Oil] + + area_ids = [1, 2, 3, 4] + d_scale = Dict(1 => 3.0, 2 => 1.5, 3 => 1.0, 4 => 0.5) + mc_scale = Dict(1 => 2.0, 2 => 2.0, 3 => 1.5, 4 => 0.5) + gen_scale = Dict(1 => 1.0, 2 => 1.0, 3 => 1.0, 4 => 0.5) + + an = Dict{Int,EMB.Node}() + nodes = EMB.Node[] + links = Link[] + + for a_id ∈ area_ids + n, l = get_sub_system_data_inv( + a_id, + products; + gen_scale = gen_scale[a_id], + mc_scale = mc_scale[a_id], + d_scale = d_scale[a_id], + ) + append!(nodes, n) + append!(links, l) + + an[a_id] = n[1] + end + + areas = [ + RefArea(1, "Oslo", 10.751, 59.921, an[1]), + RefArea(2, "Bergen", 5.334, 60.389, an[2]), + RefArea(3, "Trondheim", 10.398, 63.437, an[3]), + RefArea(4, "Tromsø", 18.953, 69.669, an[4]), + ] + + inv_data_binary = SingleInvData( + FixedProfile(500), # capex + FixedProfile(50), # max cap + FixedProfile(0), + BinaryInvestment(FixedProfile(50.0)), + ) + + inv_data_semicont = SingleInvData( + FixedProfile(10), + FixedProfile(100), + FixedProfile(0), + SemiContinuousInvestment(FixedProfile(10), FixedProfile(100)), + ) + + inv_data_discrete = SingleInvData( + FixedProfile(10), + FixedProfile(50), + FixedProfile(20), + DiscreteInvestment(FixedProfile(6)), + ) + + inv_data_continuous = SingleInvData( + FixedProfile(10), + FixedProfile(50), + FixedProfile(0), + ContinuousInvestment(FixedProfile(1), FixedProfile(100)), + ) + + inv_data_oil = SingleInvData( + FixedProfile(10), + FixedProfile(50), + FixedProfile(0), + ContinuousInvestment(FixedProfile(1), FixedProfile(100)), + ) + + OverheadLine_50MW_12 = RefStatic( + "PowerLine_50_12", + Power, + FixedProfile(50.0), + FixedProfile(0.05), + FixedProfile(0), + FixedProfile(0), + 2, + [inv_data_binary], + ) + + OverheadLine_50MW_23 = RefStatic( + "PowerLine_50_23", + Power, + FixedProfile(50.0), + FixedProfile(0.05), + FixedProfile(0), + FixedProfile(0), + 2, + [inv_data_discrete], + ) + + OverheadLine_50MW_34 = RefStatic( + "PowerLine_50_34", + Power, + FixedProfile(50.0), + FixedProfile(0.05), + FixedProfile(0), + FixedProfile(0), + 2, + [inv_data_continuous], + ) + + LNG_Ship_100MW_42 = RefDynamic( + "LNG_100_42", + NG, + FixedProfile(100.0), + FixedProfile(0.05), + FixedProfile(0), + FixedProfile(0), + 2, + ) + + PowerLine_50MW_OT = RefStatic( + "PowerLine_50_OT", + Power, + FixedProfile(50.0), + FixedProfile(0.05), + FixedProfile(0), + FixedProfile(0), + 2, + [inv_data_semicont], + ) + + NG_Pipeline_100MW_OT = RefStatic( + "NG_Pipeline_100_OT", + NG, + FixedProfile(100.0), + FixedProfile(0.03), + FixedProfile(0), + FixedProfile(0), + 2, + [inv_data_continuous], + ) + + Coal_Transport_50MW_OT = RefDynamic( + "Coal_Transport_50_OT", + Coal, + FixedProfile(50.0), + FixedProfile(0.08), + FixedProfile(0), + FixedProfile(0), + 2, + [], + ) + + CO2_Pipeline_100MW_OT = RefStatic( + "CO2_Pipeline_100_OT", + CO2, + FixedProfile(100.0), + FixedProfile(0.02), + FixedProfile(0), + FixedProfile(0), + 2, + [inv_data_continuous], + ) + + H2_Pipeline_100MW_OT = RefStatic( + "H2_Pipeline_100_OT", + H2, + FixedProfile(100.0), + FixedProfile(0.05), + FixedProfile(0), + FixedProfile(0), + 2, + [], + ) + + Waste_Transport_50MW_OT = RefDynamic( + "Waste_Transport_50_OT", + Waste, + FixedProfile(50.0), + FixedProfile(0.05), + FixedProfile(0), + FixedProfile(0), + 2, + [], + ) + + Biomass_Transport_50MW_OT = RefDynamic( + "Biomass_Transport_50_OT", + Biomass, + FixedProfile(50.0), + FixedProfile(0.05), + FixedProfile(0), + FixedProfile(0), + 2, + [], + ) + + Oil_Pipeline_100MW_OT = RefStatic( + "Oil_Pipeline_100_OT", + Oil, + FixedProfile(100.0), + FixedProfile(0.05), + FixedProfile(0), + FixedProfile(0), + 2, + [inv_data_oil], + ) + + transmissions = [ + Transmission(areas[1], areas[2], [OverheadLine_50MW_12]), + Transmission(areas[2], areas[3], [OverheadLine_50MW_23]), + Transmission(areas[3], areas[4], [OverheadLine_50MW_34]), + Transmission(areas[4], areas[2], [LNG_Ship_100MW_42]), + Transmission( + areas[1], + areas[3], + [ + PowerLine_50MW_OT, + NG_Pipeline_100MW_OT, + Coal_Transport_50MW_OT, + CO2_Pipeline_100MW_OT, + H2_Pipeline_100MW_OT, + Waste_Transport_50MW_OT, + ], + ), + Transmission( + areas[3], + areas[1], + [ + Biomass_Transport_50MW_OT, + Oil_Pipeline_100MW_OT, + ], + ), + ] + + T = TwoLevel(4, 1, SimpleTimes(24, 1)) + em_limits = Dict( + NG => FixedProfile(1e6), + CO2 => StrategicProfile([450, 400, 350, 300]), + ) + em_cost = Dict( + NG => FixedProfile(0), + CO2 => FixedProfile(0), + ) + + modeltype = InvestmentModel(em_limits, em_cost, CO2, 0.07) + + case = Case( + T, + products, + [nodes, links, areas, transmissions], + [[get_nodes, get_links], [get_areas, get_transmissions]], + ) + + return case, modeltype +end + +function run_case_EMI_geography_2() + # Get case and model data + case, model = generate_example_data_geo_all_resources() + + # Construct JuMP model for optimization + m = create_model(case, model) + + # Set optimizer for JuMP + set_optimizer(m, HiGHS.Optimizer) + + # Solve the optimization problem + optimize!(m) + + # Print solution summary + solution_summary(m) + + ## Plot topology and results in GUI + gui = GUI( + case; + design_path = joinpath(@__DIR__, "design", "EMI", "geography_2"), + model = m, + coarse_coast_lines = false, + scale_tot_opex = true, + scale_tot_capex = false, + pre_plot_sub_components = false, + ) + + return case, model, m, gui +end diff --git a/test/case7.jl b/test/case7.jl index c69732c..1e51207 100644 --- a/test/case7.jl +++ b/test/case7.jl @@ -413,11 +413,11 @@ function run_case() model = m, periods_labels = ["2022 - 2030", "2030 - 2040", "2040 - 2050"], representative_periods_labels = ["Winter", "Remaining"], - expand_all = true, path_to_results = path_to_results, case_name = case_name, scale_tot_opex = false, scale_tot_capex = true, + pre_plot_sub_components = false, ) return case, model, m, gui diff --git a/test/runtests.jl b/test/runtests.jl index 1249485..540fa88 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -6,20 +6,24 @@ using Logging const TEST_ATOL = 1e-6 const EMGUI = EnergyModelsGUI +pkg_dir = pkgdir(EnergyModelsGUI) +exdir = joinpath(pkg_dir, "examples") +testdir = joinpath(pkg_dir, "test") + # Include function that can loop through all components and plot its data -include("utils.jl") +include(joinpath(testdir, "utils.jl")) # Include the code that generates example data -exdir = joinpath(pkgdir(EnergyModelsGUI), "examples") env = Base.active_project() ENV["EMX_TEST"] = true # Set flag for example scripts to check if they are run as part of the tests include(joinpath(exdir, "generate_examples.jl")) Pkg.activate(env) -include("case7.jl") -include("example_test.jl") +include(joinpath(testdir, "case7.jl")) +include(joinpath(testdir, "EMI_geography_2.jl")) +include(joinpath(testdir, "example_test.jl")) # Add utilities needed for examples -include("../examples/utils.jl") +include(joinpath(exdir, "utils.jl")) logger_org = global_logger() logger_new = ConsoleLogger(stderr, Logging.Warn) @@ -28,22 +32,25 @@ global_logger(logger_new) @testset "EnergyModelsGUI" verbose = true begin redirect_stdio(stdout = devnull) do # Run all Aqua tests - include("Aqua.jl") + include(joinpath(testdir, "Aqua.jl")) # Check if there is need for formatting - include("JuliaFormatter.jl") + include(joinpath(testdir, "JuliaFormatter.jl")) # The following tests simply checks if the main examples can be run without errors - include("test_examples.jl") + include(joinpath(testdir, "test_examples.jl")) # The following tests results input and output functionality (saving and loading results) - include("test_results_IO.jl") + include(joinpath(testdir, "test_results_IO.jl")) + + # Test Base.show() functionalities + include(joinpath(testdir, "test_show.jl")) # Test specific GUI functionalities related to interactivity - include("test_interactivity.jl") + include(joinpath(testdir, "test_interactivity.jl")) # Test descriptive names functionalities - include("test_descriptive_names.jl") + include(joinpath(testdir, "test_descriptive_names.jl")) end end global_logger(logger_org) diff --git a/test/test_interactivity.jl b/test/test_interactivity.jl index eac8aba..e997086 100644 --- a/test/test_interactivity.jl +++ b/test/test_interactivity.jl @@ -39,32 +39,12 @@ axes_menu = get_menu(gui, :axes) pin_plot_button = get_button(gui, :pin_plot) -# Test specific miscellaneous functionalities -@testset "Test functionality" verbose = true begin - # Test print functionalities of GUI structures to the REPL - @testset "Test Base.show() functions" begin - design = EMGUI.get_design(gui) - component = EMGUI.get_components(design)[1] - connection = EMGUI.get_connections(design)[1] - @test Base.show(gui) == dump(gui; maxdepth = 1) - @test Base.show(design) == dump(design; maxdepth = 1) - @test Base.show(component) == dump(component; maxdepth = 1) - @test Base.show(connection) == dump(connection; maxdepth = 1) - - inv_data = EMGUI.get_inv_data(design) - @test Base.show(inv_data) == dump(inv_data; maxdepth = 1) - - system = EMGUI.parse_case(case) - @test Base.show(system) == dump(system; maxdepth = 1) - end -end - # Test specific GUI functionalities @testset "Test interactivity" verbose = true begin op_cost = [3371970.00359, 5382390.00598, 2010420.00219] inv_cost = [0.0, 0.0, 29536224.881975] @testset "Compare with Integrate results" begin - T = EMGUI.get_time_struct(gui) + T = get_time_struct(gui) m = EMGUI.get_model(gui) for (i, t) ∈ enumerate(strategic_periods(T)) if haskey(m, :cap_capex) @@ -80,45 +60,65 @@ end end end - # Test color toggling - @testset "Toggle colors" begin - pick_component!(gui, get_plots(area1)[1], :topo) - update!(gui) - @test area1.color[] == get_selection_color(gui) - pick_component!(gui, nothing, :topo) # deselect - @test area1.color[] == EMGUI.BLACK + # Test Expand all toggle functionality + @testset "get_toggle(gui,:expand_all).active" begin + # Check that sub-components are initially not plotted (as pre_plot_sub_components = false) + @test all(isempty(get_plots(component)) for component ∈ get_components(area1)) + get_toggle(gui, :expand_all).active = true - node2 = get_component(area1, "El 1") # fetch node El 1 - pick_component!(gui, get_plots(node2)[1], :topo) - update!(gui) - @test node2.color[] == get_selection_color(gui) - pick_component!(gui, nothing, :topo) # deselect - @test node2.color[] == EMGUI.BLACK + # Test if node n_El 1 became invisible + get_toggle(gui, :expand_all).active = false + n_el_1 = get_component(area1, "El 1") # fetch the n_El 1 node - connection1 = connections[1] # fetch the Area 1 - Area 2 transmission - plt_connection1 = get_plots(connection1)[1] - pick_component!(gui, plt_connection1, :topo) - update!(gui) - @test plt_connection1.color[][1] == get_selection_color(gui) - pick_component!(gui, nothing, :topo) # deselect - update!(gui) - @test plt_connection1.color[][1] == connection1.colors[1] + # Test if node n_El 1 became invisible + @test !get_plots(n_el_1)[1].visible[] - link1 = get_connections(area1)[5] # fetch the link to heat pump - plt_link1_sctr = get_plots(link1)[1] - pick_component!(gui, plt_link1_sctr, :topo) - update!(gui) - @test plt_link1_sctr.color[][1] == get_selection_color(gui) - pick_component!(gui, nothing, :topo) # deselect - for plot ∈ get_plots(link1) # This only tests one color (should probably add a test with more colors/`Resource`s) - for (i, color) ∈ enumerate(link1.colors) - if isa(plot, EMGUI.AbstractPlot) - @test plot.color[][i] == color - else - for plot_sub ∈ plot - @test plot_sub.color[] == color - end - end + # Test if node n_El 1 became visible again + get_toggle(gui, :expand_all).active = true + @test get_plots(n_el_1)[1].visible[] + + # Check that that sub-components are now plotted + @test all(!isempty(get_plots(component)) for component ∈ get_components(area1)) + end + + # Test color toggling + @testset "Toggle colors" begin + _, _, _, gui_2 = run_case_EMI_geography_2() + design_2 = get_root_design(gui_2) + oslo = get_component(design_2, 1) + pick_component!(gui_2, get_plots(oslo)[1], :topo) + update!(gui_2) + @test oslo.color[] == get_selection_color(gui_2) + pick_component!(gui_2, nothing, :topo) # deselect + @test oslo.color[] == EMGUI.BLACK + + pick_component!(gui_2, get_plots(oslo)[1], :topo) + notify(get_button(gui_2, :open).clicks) # Open Oslo + node2 = get_component(oslo, 1) # fetch node n_1 + pick_component!(gui_2, get_plots(node2)[1], :topo) + update!(gui_2) + @test node2.color[] == get_selection_color(gui_2) + pick_component!(gui_2, nothing, :topo) # deselect + @test node2.color[] == EMGUI.BLACK + notify(get_button(gui_2, :up).clicks) # Go back to the top level + + connection1 = get_connections(design_2)[5] # fetch the Oslo - Trondheim transmission + plt_connection1 = get_plots(connection1) + pick_component!(gui_2, plt_connection1[2], :topo) + update!(gui_2) + @test all( + all(plot.color[] .== get_selection_color(gui_2)) for plot ∈ plt_connection1 + ) + pick_component!(gui_2, nothing, :topo) # deselect + update!(gui_2) + i::Int64 = 1 + no_colors::Int64 = length(connection1.colors) + for plot ∈ plt_connection1 + if isa(plot.color[], Vector) + @test plot.color[] == connection1.colors + else + @test plot.color[] == connection1.colors[((i-1)%no_colors)+1] + i += 1 end end end @@ -145,7 +145,7 @@ end end # Test the align vert. button (aligning nodes horizontally) - clear_selection(gui, :topo) + clear_selection!(gui, :topo) @testset "get_button(gui,:align_vertical).clicks" begin pick_component!(gui, area2, :topo) # Select Area 2 pick_component!(gui, area3, :topo) # Select Area 3 @@ -188,19 +188,6 @@ end @test true # Hard to have a test here that works on CI end - # Test Expand all toggle functionality - @testset "get_toggle(gui,:expand_all).active" begin - # Test if node n_El 1 became invisible - get_toggle(gui, :expand_all).active = false - n_el_1 = get_component(area1, "El 1") # fetch the n_El 1 node - - @test !get_plots(n_el_1)[1].visible[] - - # Test if node n_El 1 became visible - get_toggle(gui, :expand_all).active = true - @test get_plots(n_el_1)[1].visible[] - end - # Run through all components @testset "Run through all components" begin run_through_all(gui; break_after_first = false) @@ -208,7 +195,7 @@ end end @testset "get_menu(gui,:period).i_selected" begin - clear_selection(gui, :topo) + clear_selection!(gui, :topo) sub_component = get_component(area2, "Power supply") # fetch the n_Power supply node pick_component!(gui, sub_component, :topo) update!(gui) @@ -229,7 +216,7 @@ end end @testset "get_menu(gui,:representative_period).i_selected" begin - clear_selection(gui, :topo) + clear_selection!(gui, :topo) heating1 = get_component(area1, "Heating 1") # fetch the Heating 1 node pick_component!(gui, heating1, :topo) update!(gui) @@ -291,7 +278,7 @@ end end @testset "pin_plot_button.clicks" begin - clear_selection(gui, :topo) + clear_selection!(gui, :topo) sub_component = get_component(area4, "Solar Power") # fetch the Solar Power node pick_component!(gui, sub_component, :topo) update!(gui) @@ -372,7 +359,7 @@ end end @testset "get_button(gui,:clear_all).clicks" begin - clear_selection(gui, :topo) + clear_selection!(gui, :topo) update_available_data_menu!(gui, nothing) # Make sure the menu is updated select_data!(gui, "emissions_strategic") notify(pin_plot_button.clicks) diff --git a/test/test_show.jl b/test/test_show.jl new file mode 100644 index 0000000..76de59e --- /dev/null +++ b/test/test_show.jl @@ -0,0 +1,24 @@ +case, _ = generate_example_ss() +gui = GUI(case) + +# Test specific miscellaneous functionalities +@testset "Test functionality" verbose = true begin + # Test print functionalities of GUI structures to the REPL + @testset "Test Base.show() functions" begin + design = EMGUI.get_design(gui) + component = EMGUI.get_components(design)[1] + component_element = EMGUI.get_element(component) + connection = EMGUI.get_connections(design)[1] + connection_element = EMGUI.get_element(connection) + @test Base.show(gui) == dump(gui; maxdepth = 1) + @test Base.show(design) == Base.show(gui) + @test Base.show(component) == Base.show(component_element) + @test Base.show(connection) == Base.show(connection_element) + + inv_data = EMGUI.get_inv_data(design) + @test Base.show(inv_data) == dump(inv_data; maxdepth = 1) + + system = EMGUI.parse_case(case) + @test Base.show(system) == Base.show(design) + end +end diff --git a/test/utils.jl b/test/utils.jl index 77b638b..0b2210d 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -6,7 +6,7 @@ import EnergyModelsGUI: get_button, update!, pick_component!, - clear_selection + clear_selection! """ run_through_all(gui::GUI) @@ -46,7 +46,7 @@ function run_through_all( for component ∈ get_components(design) @info indent_spacing^level * "Running through component $(get_ref_element(component))" - clear_selection(gui, :topo) + clear_selection!(gui, :topo) pick_component!(gui, component, :topo) update!(gui) run_through_menu( @@ -68,7 +68,7 @@ function run_through_all( for connection ∈ get_connections(design) @info indent_spacing^level * "Running through connection $(get_element(connection))" - clear_selection(gui, :topo) + clear_selection!(gui, :topo) pick_component!(gui, connection, :topo) update!(gui) run_through_menu(