diff --git a/.gitignore b/.gitignore index fbac85e..13925f8 100644 --- a/.gitignore +++ b/.gitignore @@ -13,17 +13,8 @@ # environment. *Manifest.toml -# Internal files for testing the system locally -*.geojson -*startup.jl -/test/test_coverage.jl - # Build artifacts for creating documentation generated by the Documenter package -/examples/exported_files /docs/build/ /docs/src/manual/NEWS.md /docs/src/figures/example.png -/docs/src/figures/colors_visualization.png - -# Exported files when running the tests -/examples/exported_files \ No newline at end of file +/docs/src/figures/colors_visualization.png \ No newline at end of file diff --git a/NEWS.md b/NEWS.md index 2ff4902..dc2be6d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,23 @@ # Release notes +## Unversioned + +### Enhancement + +* Enable reading model results from files by enabling `model::String` being the directory to the saved files instead of `model::JuMP.Model`. Note that the files can be generated by `EnergyModelsGUI.save_results(model::JuMP.model)`. +* Enhance the descriptive names for nodes having dictionaries with keys of type Resource as in the `MultipleBuildingTypes`-node in `EnergyModelsLanguageInterfaces.jl`. +* Enhance visualization of plots over OperationalPeriods having a constant non-zero value. + +### Adjustment + +* Move functions from generating cases from the example files to a single file to enable testing without requiring rerunning the cases. + +## Version 0.5.15 (2025-08-07) + +### Bugfix + +* Fix bug from breaking changes in GLMakie v0.10 on Windows. + ## Version 0.5.14 (2025-06-25) ### Bugfix @@ -20,6 +38,7 @@ * Added more descriptive names for `EnergyModelsHeat` and `EnergyModelsHydrogen` and add a colors for the `HeatLT` and `HeatHT` resources. ### Adjustment + * Order the colors (by id) in the Resources legend. * Move boundaries for countries just above the ocean layer. * Fix default placements of the nodes in a uniform circle (when coordinates are not provided). diff --git a/Project.toml b/Project.toml index 7d3c823..9ed2dae 100644 --- a/Project.toml +++ b/Project.toml @@ -4,8 +4,10 @@ authors = ["Jon Vegard Venås ", "Magnus Askeland "https://energymodelsx.github.io/EnergyModelsBase.jl/stable/", +) + makedocs(; sitename = "EnergyModelsGUI.jl", format = Documenter.HTML(; @@ -25,6 +30,7 @@ makedocs(; edit_link = "main", assets = String[], ansicolor = true, + size_threshold = 307200, # Default is 204800 (KiB) ), modules = [EnergyModelsGUI], pages = [ @@ -35,7 +41,7 @@ makedocs(; "Example"=>"manual/simple-example.md", "Release notes"=>"manual/NEWS.md", ], - "How-to" => Any[ + "How to" => Any[ "Save design to file"=>"how-to/save-design.md", "Export results"=>"how-to/export-results.md", "Customize colors"=>"how-to/customize-colors.md", @@ -47,6 +53,7 @@ makedocs(; "Internals"=>Any["Reference"=>"library/internals/reference.md",], ], ], + plugins = [links], ) deploydocs(; repo = "github.com/EnergyModelsX/EnergyModelsGUI.jl.git") diff --git a/docs/src/figures/EMI_geography.png b/docs/src/figures/EMI_geography.png index a0511f9..9b1a901 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 870d1c0..294bc9d 100644 Binary files a/docs/src/figures/EMI_geography_Oslo.png and b/docs/src/figures/EMI_geography_Oslo.png differ diff --git a/docs/src/manual/simple-example.md b/docs/src/manual/simple-example.md index e55c061..163116a 100644 --- a/docs/src/manual/simple-example.md +++ b/docs/src/manual/simple-example.md @@ -25,11 +25,8 @@ using EnergyModelsGUI # Get the path of the examples directory exdir = joinpath(pkgdir(EnergyModelsGUI), "examples") -# Activate project for the examples in the EnergyModelsGUI repository -Pkg.activate(exdir) -Pkg.instantiate() - # Include the code into the Julia REPL to run the following example +include(joinpath(exdir, "generate_examples.jl")) include(joinpath(exdir, "EMI_geography.jl")) ``` diff --git a/examples/EMB_network.jl b/examples/EMB_network.jl index b513b06..8ae6d12 100644 --- a/examples/EMB_network.jl +++ b/examples/EMB_network.jl @@ -1,135 +1,3 @@ -# Import the required packages -using EnergyModelsBase -using JuMP -using HiGHS -using PrettyTables -using TimeStruct - -""" - generate_example_network() - -Generate the data for an example consisting of a simple electricity network. -The more stringent CO₂ emission in latter investment periods force the utilization of the -more expensive natural gas power plant with CCS to reduce emissions. -""" -function generate_example_network() - @info "Generate case data - Simple network example" - - # Define the different resources and their emission intensity in tCO2/MWh - NG = ResourceEmit("NG", 0.2) - Coal = ResourceCarrier("Coal", 0.35) - Power = ResourceCarrier("Power", 0.0) - CO2 = ResourceEmit("CO2", 1.0) - products = [NG, Coal, Power, CO2] - - # Variables for the individual entries of the time structure - op_duration = 2 # Each operational period has a duration of 2 - op_number = 4 # There are in total 4 operational periods - operational_periods = SimpleTimes(op_number, op_duration) - - # The number of operational periods times the duration of the operational periods, which - # can also be extracted using the function `duration` of a `SimpleTimes` structure. - # This implies, that a strategic period is 8 times longer than an operational period, - # resulting in the values below as "/8h". - op_per_strat = op_duration * op_number - - # Creation of the time structure and global data - T = TwoLevel(4, 1, operational_periods; op_per_strat) - model = OperationalModel( - Dict( # Emission cap for CO₂ in t/8h and for NG in MWh/8h - CO2 => StrategicProfile([160, 140, 120, 100]), - NG => FixedProfile(1e6), - ), - Dict( # Emission price for CO₂ in EUR/t and for NG in EUR/MWh - CO2 => FixedProfile(0), - NG => FixedProfile(0), - ), - CO2, # CO2 instance - ) - - # Creation of the emission data for the individual nodes. - capture_data = CaptureEnergyEmissions(0.9) - emission_data = EmissionsEnergy() - - # Create the individual test nodes, corresponding to a system with an electricity demand/sink, - # coal and nautral gas sources, coal and natural gas (with CCS) power plants and CO₂ storage. - nodes = [ - GenAvailability("Availability", products), - RefSource( - "NG source", # Node id - FixedProfile(100), # Capacity in MW - FixedProfile(30), # Variable OPEX in EUR/MW - FixedProfile(0), # Fixed OPEX in EUR/MW/8h - Dict(NG => 1), # Output from the Node, in this case, NG - ), - RefSource( - "coal source", # Node id - FixedProfile(100), # Capacity in MW - FixedProfile(9), # Variable OPEX in EUR/MWh - FixedProfile(0), # Fixed OPEX in EUR/MW/8h - Dict(Coal => 1), # Output from the Node, in this case, coal - ), - RefNetworkNode( - "NG+CCS power plant", # Node id - FixedProfile(25), # Capacity in MW - FixedProfile(5.5), # Variable OPEX in EUR/MWh - FixedProfile(0), # Fixed OPEX in EUR/MW/8h - Dict(NG => 2), # Input to the node with input ratio - Dict(Power => 1, CO2 => 1), # Output from the node with output ratio - # Line above: CO2 is required as output for variable definition, but the - # value does not matter - [capture_data], # Additonal data for emissions and CO₂ capture - ), - RefNetworkNode( - "coal power plant", # Node id - FixedProfile(25), # Capacity in MW - FixedProfile(6), # Variable OPEX in EUR/MWh - FixedProfile(0), # Fixed OPEX in EUR/MW/8h - Dict(Coal => 2.5), # Input to the node with input ratio - Dict(Power => 1), # Output from the node with output ratio - [emission_data], # Additonal data for emissions - ), - RefStorage{AccumulatingEmissions}( - "CO2 storage", # Node id - StorCapOpex( - FixedProfile(60), # Charge capacity in t/h - FixedProfile(9.1), # Storage variable OPEX for the charging in EUR/t - FixedProfile(0) # Storage fixed OPEX for the charging in EUR/(t/h 8h) - ), - StorCap(FixedProfile(600)), # Storage capacity in t - CO2, # Stored resource - Dict(CO2 => 1, Power => 0.02), # Input resource with input ratio - # Line above: This implies that storing CO₂ requires Power - Dict(CO2 => 1), # Output from the node with output ratio - # In practice, for CO₂ storage, this is never used. - ), - RefSink( - "electricity demand", # Node id - OperationalProfile([20, 30, 40, 30]), # Demand in MW - Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), - # Line above: Surplus and deficit penalty for the node in EUR/MWh - Dict(Power => 1), # Energy demand and corresponding ratio - ), - ] - - # Connect all nodes with the availability node for the overall energy/mass balance - links = [ - Direct("Av-NG_pp", nodes[1], nodes[4], Linear()) - Direct("Av-coal_pp", nodes[1], nodes[5], Linear()) - Direct("Av-CO2_stor", nodes[1], nodes[6], Linear()) - Direct("Av-demand", nodes[1], nodes[7], Linear()) - Direct("NG_src-av", nodes[2], nodes[1], Linear()) - Direct("Coal_src-av", nodes[3], nodes[1], Linear()) - Direct("NG_pp-av", nodes[4], nodes[1], Linear()) - Direct("Coal_pp-av", nodes[5], nodes[1], Linear()) - Direct("CO2_stor-av", nodes[6], nodes[1], Linear()) - ] - - # Input data structure - case = Case(T, products, [nodes, links], [[get_nodes, get_links]]) - return case, model -end - # Generate the case and model data and run the model case, model = generate_example_network() optimizer = optimizer_with_attributes(HiGHS.Optimizer, MOI.Silent() => true) @@ -159,7 +27,6 @@ pretty_table( ## Code below for displaying the GUI using EnergyModelsGUI -const EMB = EnergyModelsBase # Set a special icon only for last node and the other icons based on type id_to_icon_map = Dict( diff --git a/examples/EMB_sink_source.jl b/examples/EMB_sink_source.jl index fdc4417..9617bed 100644 --- a/examples/EMB_sink_source.jl +++ b/examples/EMB_sink_source.jl @@ -1,72 +1,3 @@ -# Import the required packages -using EnergyModelsBase -using JuMP -using HiGHS -using PrettyTables -using TimeStruct - -""" - generate_example_ss() - -Generate the data for an example consisting of an electricity source and sink. It shows how -the source adjusts to the demand. -""" -function generate_example_ss() - @info "Generate case data - Simple sink-source example" - - # Define the different resources and their emission intensity in tCO2/MWh - Power = ResourceCarrier("Power", 0.0) - CO2 = ResourceEmit("CO2", 1.0) - products = [Power, CO2] - - # Variables for the individual entries of the time structure - op_duration = 2 # Each operational period has a duration of 2 - op_number = 4 # There are in total 4 operational periods - operational_periods = SimpleTimes(op_number, op_duration) - - # The number of operational periods times the duration of the operational periods, which - # can also be extracted using the function `duration` of a `SimpleTimes` structure. - # This implies, that a strategic period is 8 times longer than an operational period, - # resulting in the values below as "/8h". - op_per_strat = op_duration * op_number - - # Creation of the time structure and global data - T = TwoLevel(2, 1, operational_periods; op_per_strat) - model = OperationalModel( - Dict(CO2 => FixedProfile(10)), # Emission cap for CO₂ in t/8h - Dict(CO2 => FixedProfile(0)), # Emission price for CO₂ in EUR/t - CO2, # CO₂ instance - ) - - # Create the individual test nodes, corresponding to a system with an electricity - # demand/sink and source - nodes = [ - RefSource( - "electricity source", # Node id - FixedProfile(50), # Capacity in MW - FixedProfile(30), # Variable OPEX in EUR/MW - FixedProfile(0), # Fixed OPEX in EUR/MW/8h - Dict(Power => 1), # Output from the Node, in this case, Power - ), - RefSink( - "electricity demand", # Node id - OperationalProfile([20, 30, 40, 30]), # Demand in MW - Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), - # Line above: Surplus and deficit penalty for the node in EUR/MWh - Dict(Power => 1), # Energy demand and corresponding ratio - ), - ] - - # Connect all nodes with the availability node for the overall energy/mass balance - links = [ - Direct("source-demand", nodes[1], nodes[2], Linear()), - ] - - # Input data structure - case = Case(T, products, [nodes, links], [[get_nodes, get_links]]) - return case, model -end - # Generate the case and model data and run the model case, model = generate_example_ss() optimizer = optimizer_with_attributes(HiGHS.Optimizer, MOI.Silent() => true) diff --git a/examples/EMG_network.jl b/examples/EMG_network.jl index f2caf7a..4b339ac 100644 --- a/examples/EMG_network.jl +++ b/examples/EMG_network.jl @@ -1,320 +1,5 @@ -# Import the required packages -using EnergyModelsBase -using EnergyModelsGeography -using JuMP -using HiGHS -using TimeStruct - -const EMB = EnergyModelsBase -const EMG = EnergyModelsGeography - -""" - generate_example_data() - -Generate the data for an example consisting of a simple electricity network. The simple \ -network is existing within 5 regions with differing demand. Each region has the same \ -technologies. - -The example is partly based on the provided example `network.jl` in `EnergyModelsBase`. -""" -function generate_example_data() - @info "Generate case data - Simple network example with 5 regions with the same \ - technologies" - - # Retrieve the products - products = get_resources() - NG = products[1] - Power = products[3] - CO2 = products[4] - - # Variables for the individual entries of the time structure - op_duration = 1 # Each operational period has a duration of 2 - op_number = 24 # There are in total 4 operational periods - operational_periods = SimpleTimes(op_number, op_duration) - - # The number of operational periods times the duration of the operational periods, which - # can also be extracted using the function `duration` of a `SimpleTimes` structure. - # This implies, that a strategic period is 8 times longer than an operational period, - # resulting in the values below as "/24h". - op_per_strat = op_duration * op_number - - # Creation of the time structure and global data - T = TwoLevel(4, 1, operational_periods; op_per_strat) - model = OperationalModel( - Dict( - CO2 => StrategicProfile([160, 140, 120, 100]), # CO₂ emission cap in t/24h - NG => FixedProfile(1e6) # NG cap in MWh/24h - ), - Dict( - CO2 => FixedProfile(0), # CO₂ emission cost in EUR/t - NG => FixedProfile(0) # NG emission cost in EUR/t - ), - CO2, - ) - - # Create input data for the individual areas - # The input data is based on scaling factors and/or specified demands - area_ids = [1, 2, 3, 4, 5, 6, 7] - d_scale = Dict(1 => 3.0, 2 => 1.5, 3 => 1.0, 4 => 0.5, 5 => 0.5, 6 => 0.0, 7 => 3.0) - mc_scale = Dict(1 => 2.0, 2 => 2.0, 3 => 1.5, 4 => 0.5, 5 => 0.5, 6 => 0.5, 7 => 3.0) - - op_data = OperationalProfile([ - 10, - 10, - 10, - 10, - 35, - 40, - 45, - 45, - 50, - 50, - 60, - 60, - 50, - 45, - 45, - 40, - 35, - 40, - 45, - 40, - 35, - 30, - 30, - 30, - ]) - tromsø_demand = [op_data; - op_data; - op_data; - op_data - ] - demand = Dict( - 1 => false, - 2 => false, - 3 => false, - 4 => tromsø_demand, - 5 => false, - 6 => false, - 7 => false, - ) - - # Create identical areas with index according to the input array - an = Dict() - nodes = EMB.Node[] - links = Link[] - for a_id ∈ area_ids - n, l = get_sub_system_data( - a_id, - products; - mc_scale = mc_scale[a_id], - d_scale = d_scale[a_id], - demand = demand[a_id], - ) - append!(nodes, n) - append!(links, l) - - # Add area node for each subsystem - an[a_id] = n[1] - end - - # Create the individual areas - # The individual fields are: - # 1. id - Identifier of the area - # 2. name - Name of the area - # 3. lon - Longitudinal position of the area - # 4. lon - Latitudinal position of the area - # 5. node - Availability node of the area - 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.4366, an[3]), - RefArea(4, "Tromsø", 18.953, 69.669, an[4]), - RefArea(5, "Kristiansand", 7.984, 58.146, an[5]), - RefArea(6, "Sørlige Nordsjø II", 6.836, 57.151, an[6]), - RefArea(7, "Danmark", 8.614, 56.359, an[7])] - - # Create the individual transmission modes to transport the energy between the - # individual areass. - # The individuaal fields are explained below, while the other fields are: - # 1. Identifier of the transmission mode - # 2. Transported resource - # 7. 2 for bidirectional transport, 1 for unidirectional - # 8. Potential additional data - cap_ohl = FixedProfile(50.0) # Capacity of an overhead line in MW - cap_lng = FixedProfile(100.0) # Capacity of the LNG transport in MW - loss = FixedProfile(0.05) # Relative loss of either transport mode - opex_var = FixedProfile(0.05) # Variable OPEX in EUR/MWh - opex_fix = FixedProfile(0.05) # Fixed OPEX in EUR/24h - - OB_OverheadLine_50MW = RefStatic("OB_PowerLine_50", Power, cap_ohl, loss, opex_var, opex_fix, 2) - OT_OverheadLine_50MW = RefStatic("OT_PowerLine_50", Power, cap_ohl, loss, opex_var, opex_fix, 2) - OK_OverheadLine_50MW = RefStatic("OK_PowerLine_50", Power, cap_ohl, loss, opex_var, opex_fix, 2) - BT_OverheadLine_50MW = RefStatic("BT_PowerLine_50", Power, cap_ohl, loss, opex_var, opex_fix, 2) - BTN_LNG_Ship_100MW = RefDynamic("BTN_LNG_100", NG, cap_lng, loss, opex_var, opex_fix, 1) - BK_OverheadLine_50MW = RefStatic("BK_PowerLine_50", Power, cap_ohl, loss, opex_var, opex_fix, 2) - TTN_OverheadLine_50MW = RefStatic("TTN_PowerLine_50", Power, cap_ohl, loss, opex_var, opex_fix, 2) - KS_OverheadLine_50MW = RefStatic("KS_PowerLine_50", Power, cap_ohl, loss, opex_var, opex_fix, 2) - SD_OverheadLine_50MW = RefStatic("SD_PowerLine_50", Power, cap_ohl, loss, opex_var, opex_fix, 2) - - # Create the different transmission corridors between the individual areas - transmissions = [ - Transmission(areas[1], areas[2], [OB_OverheadLine_50MW]), - Transmission(areas[1], areas[3], [OT_OverheadLine_50MW]), - Transmission(areas[1], areas[5], [OK_OverheadLine_50MW]), - Transmission(areas[2], areas[3], [BT_OverheadLine_50MW]), - Transmission(areas[2], areas[4], [BTN_LNG_Ship_100MW]), - Transmission(areas[2], areas[5], [BK_OverheadLine_50MW]), - Transmission(areas[3], areas[4], [TTN_OverheadLine_50MW]), - Transmission(areas[5], areas[6], [KS_OverheadLine_50MW]), - Transmission(areas[6], areas[7], [SD_OverheadLine_50MW]), - ] - - # Input data structure - case = Case( - T, - products, - [nodes, links, areas, transmissions], - [[get_nodes, get_links], [get_areas, get_transmissions]], - ) - return case, model -end - -function get_resources() - - # Define the different resources - NG = ResourceEmit("NG", 0.2) - Coal = ResourceCarrier("Coal", 0.35) - Power = ResourceCarrier("Power", 0.0) - CO2 = ResourceEmit("CO2", 1.0) - products = [NG, Coal, Power, CO2] - - return products -end - -# Subsystem test data for geography package. All subsystems are the same, except for the -# profiles -# The subsystem is similar to the subsystem in the `network.jl` example of EnergyModelsBase. -function get_sub_system_data( - i, - products; - mc_scale::Float64 = 1.0, - d_scale::Float64 = 1.0, - demand = false, -) - NG, Coal, Power, CO2 = products - - # Use of standard demand if not provided differently - d_standard = OperationalProfile([ - 20, - 20, - 20, - 20, - 25, - 30, - 35, - 35, - 40, - 40, - 40, - 40, - 40, - 35, - 35, - 30, - 25, - 30, - 35, - 30, - 25, - 20, - 20, - 20, - ]) - if demand == false - demand = [d_standard; d_standard; d_standard; d_standard] - demand *= d_scale - end - - # Create the individual test nodes, corresponding to a system with an electricity demand/sink, - # coal and nautral gas sources, coal and natural gas (with CCS) power plants and CO₂ storage. - j = (i - 1) * 100 - nodes = [ - GeoAvailability(j + 1, products), - RefSource( - j + 2, # Node id - FixedProfile(1e12), # Capacity in MW - FixedProfile(30 * mc_scale), # Variable OPEX in EUR/MW - FixedProfile(0), # Fixed OPEX in EUR/24h - Dict(NG => 1), # Output from the Node, in this case, NG - ), - RefSource( - j + 3, # Node id - FixedProfile(1e12), # Capacity in MW - FixedProfile(9 * mc_scale), # Variable OPEX in EUR/MWh - FixedProfile(0), # Fixed OPEX in EUR/24h - Dict(Coal => 1), # Output from the Node, in this case, coal - ), - RefNetworkNode( - j + 4, # Node id - FixedProfile(25), # Capacity in MW - FixedProfile(5.5 * mc_scale), # Variable OPEX in EUR/MWh - FixedProfile(0), # Fixed OPEX in EUR/24h - Dict(NG => 2), # Input to the node with input ratio - Dict(Power => 1, CO2 => 1), # Output from the node with output ratio - # Line above: CO2 is required as output for variable definition, but the - # value does not matter - [CaptureEnergyEmissions(0.9)], # Additonal data for emissions and CO₂ capture - ), - RefNetworkNode( - j + 5, # Node id - FixedProfile(25), # Capacity in MW - FixedProfile(6 * mc_scale), # Variable OPEX in EUR/MWh - FixedProfile(0), # Fixed OPEX in EUR/24h - Dict(Coal => 2.5), # Input to the node with input ratio - Dict(Power => 1), # Output from the node with output ratio - [EmissionsEnergy()], # Additonal data for emissions - ), - RefStorage{AccumulatingEmissions}( - j + 6, # Node id - StorCapOpex( - FixedProfile(20), # Charge capacity in t/h - FixedProfile(9.1), # Storage variable OPEX for the charging in EUR/t - FixedProfile(0) # Storage fixed OPEX for the charging in EUR/(t/h 8h) - ), - StorCap(FixedProfile(600)), # Storage capacity in t - CO2, # Stored resource - 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 - StrategicProfile(demand), # Demand in MW - Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), - # Line above: Surplus and deficit penalty for the node in EUR/MWh - Dict(Power => 1), # Energy demand and corresponding ratio - ), - ] - - # Connect all nodes with the availability node for the overall energy/mass balance - links = [ - Direct(j + 14, nodes[1], nodes[4], Linear()) - Direct(j + 15, nodes[1], nodes[5], Linear()) - Direct(j + 16, nodes[1], nodes[6], Linear()) - Direct(j + 17, nodes[1], nodes[7], Linear()) - Direct(j + 21, nodes[2], nodes[1], Linear()) - Direct(j + 31, nodes[3], nodes[1], Linear()) - Direct(j + 41, nodes[4], nodes[1], Linear()) - Direct(j + 51, nodes[5], nodes[1], Linear()) - Direct(j + 61, nodes[6], nodes[1], Linear()) - ] - return nodes, links -end - # Generate the case and model data and run the model -case, model = generate_example_data() +case, model = generate_example_geo() optimizer = optimizer_with_attributes(HiGHS.Optimizer, MOI.Silent() => true) m = create_model(case, model) set_optimizer(m, optimizer) diff --git a/examples/EMI_geography.jl b/examples/EMI_geography.jl index fdd1b09..6d41cd8 100644 --- a/examples/EMI_geography.jl +++ b/examples/EMI_geography.jl @@ -1,474 +1,3 @@ -# Import the required packages -using EnergyModelsBase -using EnergyModelsGeography -using EnergyModelsInvestments -using HiGHS -using JuMP -using TimeStruct - -const EMB = EnergyModelsBase -const EMG = EnergyModelsGeography -const EMI = EnergyModelsInvestments - -""" - generate_example_data_geo() - -Generate the data for an example consisting of a simple electricity network. The simple \ -network is existing within 5 regions with differing demand. Each region has the same \ -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 - products = get_resources_inv() - NG = products[1] - Power = products[3] - CO2 = products[4] - - # Create input data for the areas - 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) - - # Create identical areas with index according to input array - an = Dict() - 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) - - # Add area node for each subsystem - an[a_id] = n[1] - end - - # Create the individual areas - 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]), - ] - - # Create the investment data for the different power line investment modes - inv_data_12 = SingleInvData( - FixedProfile(500), - FixedProfile(50), - FixedProfile(0), - BinaryInvestment(FixedProfile(50.0)), - ) - - inv_data_13 = SingleInvData( - FixedProfile(10), - FixedProfile(100), - FixedProfile(0), - SemiContinuousInvestment(FixedProfile(10), FixedProfile(100)), - ) - - inv_data_23 = SingleInvData( - FixedProfile(10), - FixedProfile(50), - FixedProfile(20), - DiscreteInvestment(FixedProfile(6)), - ) - - inv_data_34 = SingleInvData( - FixedProfile(10), - FixedProfile(50), - FixedProfile(0), - ContinuousInvestment(FixedProfile(1), FixedProfile(100)), - ) - - # Create the TransmissionModes and the Transmission corridors - OverheadLine_50MW_12 = RefStatic( - "PowerLine_50", - Power, - FixedProfile(50.0), - FixedProfile(0.05), - FixedProfile(0), - FixedProfile(0), - 2, - [inv_data_12], - ) - OverheadLine_50MW_13 = RefStatic( - "PowerLine_50", - Power, - FixedProfile(50.0), - FixedProfile(0.05), - FixedProfile(0), - FixedProfile(0), - 2, - [inv_data_13], - ) - OverheadLine_50MW_23 = RefStatic( - "PowerLine_50", - Power, - FixedProfile(50.0), - FixedProfile(0.05), - FixedProfile(0), - FixedProfile(0), - 2, - [inv_data_23], - ) - OverheadLine_50MW_34 = RefStatic( - "PowerLine_50", - Power, - FixedProfile(50.0), - FixedProfile(0.05), - FixedProfile(0), - FixedProfile(0), - 2, - [inv_data_34], - ) - LNG_Ship_100MW = RefDynamic( - "LNG_100", - NG, - FixedProfile(100.0), - FixedProfile(0.05), - FixedProfile(0), - FixedProfile(0), - 2, - [], - ) - - transmissions = [ - Transmission(areas[1], areas[2], [OverheadLine_50MW_12]), - Transmission(areas[1], areas[3], [OverheadLine_50MW_13]), - Transmission(areas[2], areas[3], [OverheadLine_50MW_23]), - Transmission(areas[3], areas[4], [OverheadLine_50MW_34]), - Transmission(areas[4], areas[2], [LNG_Ship_100MW]), - ] - - # Creation of the time structure and global data - 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) - - # Input data structure - case = Case( - T, - products, - [nodes, links, areas, transmissions], - [[get_nodes, get_links], [get_areas, get_transmissions]], - ) - return case, modeltype -end - -function get_resources_inv() - - # Define the different resources - NG = ResourceEmit("NG", 0.2) - Coal = ResourceCarrier("Coal", 0.35) - Power = ResourceCarrier("Power", 0.0) - CO2 = ResourceEmit("CO2", 1.0) - products = [NG, Coal, Power, CO2] - - return products -end - -function get_sub_system_data_inv( - i, - products; - gen_scale::Float64 = 1.0, - mc_scale::Float64 = 1.0, - d_scale::Float64 = 1.0, - demand = false, -) - NG, Coal, Power, CO2 = products - - if demand == false - demand = [ - OperationalProfile([ - 20, - 20, - 20, - 20, - 25, - 30, - 35, - 35, - 40, - 40, - 40, - 40, - 40, - 35, - 35, - 30, - 25, - 30, - 35, - 30, - 25, - 20, - 20, - 20, - ]), - OperationalProfile([ - 20, - 20, - 20, - 20, - 25, - 30, - 35, - 35, - 40, - 40, - 40, - 40, - 40, - 35, - 35, - 30, - 25, - 30, - 35, - 30, - 25, - 20, - 20, - 20, - ]), - OperationalProfile([ - 20, - 20, - 20, - 20, - 25, - 30, - 35, - 35, - 40, - 40, - 40, - 40, - 40, - 35, - 35, - 30, - 25, - 30, - 35, - 30, - 25, - 20, - 20, - 20, - ]), - OperationalProfile([ - 20, - 20, - 20, - 20, - 25, - 30, - 35, - 35, - 40, - 40, - 40, - 40, - 40, - 35, - 35, - 30, - 25, - 30, - 35, - 30, - 25, - 20, - 20, - 20, - ]), - ] - demand *= d_scale - end - - j = (i - 1) * 100 - nodes = [ - GeoAvailability(j + 1, products), - RefSink( - j + 2, - StrategicProfile(demand), - Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), - Dict(Power => 1), - ), - RefSource( - j + 3, - FixedProfile(30), - FixedProfile(30 * mc_scale), - FixedProfile(100), - Dict(NG => 1), - [ - SingleInvData( - FixedProfile(1000), # capex [€/kW] - FixedProfile(200), # max installed capacity [kW] - ContinuousInvestment(FixedProfile(10), FixedProfile(200)), # investment mode - ), - ], - ), - RefSource( - j + 4, - FixedProfile(9), - FixedProfile(9 * mc_scale), - FixedProfile(100), - Dict(Coal => 1), - [ - SingleInvData( - FixedProfile(1000), # capex [€/kW] - FixedProfile(200), # max installed capacity [kW] - FixedProfile(0), - ContinuousInvestment(FixedProfile(10), FixedProfile(200)), # investment mode - ), - ], - ), - RefNetworkNode( - j + 5, - FixedProfile(0), - FixedProfile(5.5 * mc_scale), - FixedProfile(100), - Dict(NG => 2), - Dict(Power => 1, CO2 => 0), - [ - SingleInvData( - FixedProfile(600), # capex [€/kW] - FixedProfile(25), # max installed capacity [kW] - ContinuousInvestment(FixedProfile(0), FixedProfile(25)), # investment mode - ), - CaptureEnergyEmissions(0.9), - ], - ), - RefNetworkNode( - j + 6, - FixedProfile(0), - FixedProfile(6 * mc_scale), - FixedProfile(100), - Dict(Coal => 2.5), - Dict(Power => 1), - [ - SingleInvData( - FixedProfile(800), # capex [€/kW] - FixedProfile(25), # max installed capacity [kW] - ContinuousInvestment(FixedProfile(0), FixedProfile(25)), # investment mode - ), - EmissionsEnergy(), - ], - ), - RefStorage{AccumulatingEmissions}( - j + 7, - StorCapOpex(FixedProfile(0), FixedProfile(9.1 * mc_scale), FixedProfile(100)), - StorCap(FixedProfile(0)), - CO2, - Dict(CO2 => 1, Power => 0.02), - Dict(CO2 => 1), - [ - StorageInvData( - charge = NoStartInvData( - FixedProfile(500), - FixedProfile(600), - ContinuousInvestment(FixedProfile(0), FixedProfile(600)), - ), - level = NoStartInvData( - FixedProfile(500), - FixedProfile(600), - ContinuousInvestment(FixedProfile(0), FixedProfile(600)), - ), - ), - ], - ), - RefNetworkNode( - j + 8, - FixedProfile(0), - FixedProfile(0 * mc_scale), - FixedProfile(0), - Dict(Coal => 2.5), - Dict(Power => 1), - [ - SingleInvData( - FixedProfile(10000), # capex [€/kW] - FixedProfile(25), # max installed capacity [kW] - ContinuousInvestment(FixedProfile(0), FixedProfile(2)), # investment mode - ), - EmissionsEnergy(), - ], - ), - RefStorage{AccumulatingEmissions}( - j + 9, - StorCapOpex(FixedProfile(3), FixedProfile(0 * mc_scale), FixedProfile(0)), - StorCap(FixedProfile(5)), - CO2, - Dict(CO2 => 1, Power => 0.02), - Dict(CO2 => 1), - [ - StorageInvData( - charge = NoStartInvData( - FixedProfile(500), - FixedProfile(30), - ContinuousInvestment(FixedProfile(0), FixedProfile(3)), - ), - level = NoStartInvData( - FixedProfile(500), - FixedProfile(50), - ContinuousInvestment(FixedProfile(0), FixedProfile(2)), - ), - ), - ], - ), - RefNetworkNode( - j + 10, - FixedProfile(0), - FixedProfile(0 * mc_scale), - FixedProfile(0), - Dict(Coal => 2.5), - Dict(Power => 1), - [ - SingleInvData( - FixedProfile(10000), # capex [€/kW] - FixedProfile(10000), # max installed capacity [kW] - ContinuousInvestment(FixedProfile(0), FixedProfile(10000)), # investment mode - ), - EmissionsEnergy(), - ], - ), - ] - - links = [ - Direct(j * 10 + 15, nodes[1], nodes[5], Linear()) - Direct(j * 10 + 16, nodes[1], nodes[6], Linear()) - Direct(j * 10 + 17, nodes[1], nodes[7], Linear()) - Direct(j * 10 + 18, nodes[1], nodes[8], Linear()) - Direct(j * 10 + 19, nodes[1], nodes[9], Linear()) - Direct(j * 10 + 110, nodes[1], nodes[10], Linear()) - Direct(j * 10 + 12, nodes[1], nodes[2], Linear()) - Direct(j * 10 + 31, nodes[3], nodes[1], Linear()) - Direct(j * 10 + 41, nodes[4], nodes[1], Linear()) - Direct(j * 10 + 51, nodes[5], nodes[1], Linear()) - Direct(j * 10 + 61, nodes[6], nodes[1], Linear()) - Direct(j * 10 + 71, nodes[7], nodes[1], Linear()) - Direct(j * 10 + 81, nodes[8], nodes[1], Linear()) - Direct(j * 10 + 91, nodes[9], nodes[1], Linear()) - Direct(j * 10 + 101, nodes[10], nodes[1], Linear()) - ] - return nodes, links -end - # Generate case data case, model = generate_example_data_geo() optimizer = optimizer_with_attributes(HiGHS.Optimizer, MOI.Silent() => true) diff --git a/examples/EMI_network.jl b/examples/EMI_network.jl index dd02093..666dc21 100644 --- a/examples/EMI_network.jl +++ b/examples/EMI_network.jl @@ -1,160 +1,3 @@ -# Import the required packages -using EnergyModelsBase -using EnergyModelsInvestments -using JuMP -using HiGHS -using PrettyTables -using TimeStruct - -""" - generate_example_network_investment() - -Generate the data for an example consisting of a simple electricity network. -The more stringent CO₂ emission in latter investment periods force the investment into both -the natural gas power plant with CCS and the CO₂ storage node. -""" -function generate_example_network_investment() - @info "Generate case data - Simple network example with investments" - - # Define the different resources and their emission intensity in tCO2/MWh - NG = ResourceEmit("NG", 0.2) - Coal = ResourceCarrier("Coal", 0.35) - Power = ResourceCarrier("Power", 0.0) - CO2 = ResourceEmit("CO2", 1.0) - products = [NG, Coal, Power, CO2] - - # Variables for the individual entries of the time structure - op_duration = 2 # Each operational period has a duration of 2 - op_number = 4 # There are in total 4 operational periods - operational_periods = SimpleTimes(op_number, op_duration) - - # Each operational period should correspond to a duration of 2 h while a duration if 1 - # of a strategic period should correspond to a year. - # This implies, that a strategic period is 8760 times longer than an operational period, - # resulting in the values below as "/year". - op_per_strat = 8760 - - # Creation of the time structure and global data - T = TwoLevel(4, 1, operational_periods; op_per_strat) - model = InvestmentModel( - Dict( # Emission cap for CO₂ in t/year and for NG in MWh/year - CO2 => StrategicProfile([170, 150, 130, 110]) * 1000, - NG => FixedProfile(1e6), - ), - Dict( # Emission price for CO₂ in EUR/t and for NG in EUR/MWh - CO2 => FixedProfile(0), - NG => FixedProfile(0), - ), - CO2, # CO2 instance - 0.07, # Discount rate in absolute value - ) - - # Creation of the emission data for the individual nodes. - capture_data = CaptureEnergyEmissions(0.9) - emission_data = EmissionsEnergy() - - # Create the individual test nodes, corresponding to a system with an electricity demand/sink, - # coal and nautral gas sources, coal and natural gas (with CCS) power plants and CO₂ storage. - nodes = [ - GenAvailability("Availability", products), - RefSource( - "NG source", # Node id - FixedProfile(100), # Capacity in MW - FixedProfile(30), # Variable OPEX in EUR/MWh - FixedProfile(0), # Fixed OPEX in EUR/MW/year - Dict(NG => 1), # Output from the Node, in this case, NG - ), - RefSource( - "coal source", # Node id - FixedProfile(100), # Capacity in MW - FixedProfile(9), # Variable OPEX in EUR/MWh - FixedProfile(0), # Fixed OPEX in EUR/MW/year - Dict(Coal => 1), # Output from the Node, in this case, coal - ), - RefNetworkNode( - "NG+CCS power plant", # Node id - FixedProfile(0), # Capacity in MW - FixedProfile(5.5), # Variable OPEX in EUR/MWh - FixedProfile(0), # Fixed OPEX in EUR/MW/year - Dict(NG => 2), # Input to the node with input ratio - Dict(Power => 1, CO2 => 0), # Output from the node with output ratio - # Line above: CO2 is required as output for variable definition, but the - # value does not matter - [ - capture_data, # Additonal data for emissions and CO₂ capture - SingleInvData( - FixedProfile(600 * 1e3), # Capex in EUR/MW - FixedProfile(40), # Max installed capacity [MW] - SemiContinuousInvestment(FixedProfile(5), FixedProfile(40)), - # Line above: Investment mode with the following arguments: - # 1. argument: min added capactity per sp [MW] - # 2. argument: max added capactity per sp [MW] - ), - ], - ), - RefNetworkNode( - "coal power plant", # Node id - FixedProfile(40), # Capacity in MW - FixedProfile(6), # Variable OPEX in EUR/MWh - FixedProfile(0), # Fixed OPEX in EUR/MW/year - Dict(Coal => 2.5), # Input to the node with input ratio - Dict(Power => 1), # Output from the node with output ratio - [emission_data], # Additonal data for emissions - ), - RefStorage{AccumulatingEmissions}( - "CO2 storage", # Node id - StorCapOpex( - FixedProfile(0), # Charge capacity in t/h - FixedProfile(9.1), # Storage variable OPEX for the charging in EUR/t - FixedProfile(0) # Storage fixed OPEX for the charging in EUR/(t/h year) - ), - StorCap(FixedProfile(1e8)), # Storage capacity in t - CO2, # Stored resource - Dict(CO2 => 1, Power => 0.02), # Input resource with input ratio - # Line above: This implies that storing CO₂ requires Power - Dict(CO2 => 1), # Output from the node with output ratio - # In practice, for CO₂ storage, this is never used. - [ - StorageInvData( - charge = NoStartInvData( - FixedProfile(200 * 1e3), # CAPEX [EUR/(t/h)] - FixedProfile(60), # Max installed capacity [EUR/(t/h)] - ContinuousInvestment(FixedProfile(0), FixedProfile(5)), - # Line above: Investment mode with the following arguments: - # 1. argument: min added capactity per sp [t/h] - # 2. argument: max added capactity per sp [t/h] - UnlimitedLife(), # Lifetime mode - ), - ), - ], - ), - RefSink( - "electricity demand", # Node id - OperationalProfile([20, 30, 40, 30]), # Demand in MW - Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), - # Line above: Surplus and deficit penalty for the node in EUR/MWh - Dict(Power => 1), # Energy demand and corresponding ratio - ), - ] - - # Connect all nodes with the availability node for the overall energy/mass balance - links = [ - Direct("Av-NG_pp", nodes[1], nodes[4], Linear()) - Direct("Av-coal_pp", nodes[1], nodes[5], Linear()) - Direct("Av-CO2_stor", nodes[1], nodes[6], Linear()) - Direct("Av-demand", nodes[1], nodes[7], Linear()) - Direct("NG_src-av", nodes[2], nodes[1], Linear()) - Direct("Coal_src-av", nodes[3], nodes[1], Linear()) - Direct("NG_pp-av", nodes[4], nodes[1], Linear()) - Direct("Coal_pp-av", nodes[5], nodes[1], Linear()) - Direct("CO2_stor-av", nodes[6], nodes[1], Linear()) - ] - - # Input data structure - case = Case(T, products, [nodes, links], [[get_nodes, get_links]]) - return case, model -end - # Generate the case and model data and run the model case, model = generate_example_network_investment() optimizer = optimizer_with_attributes(HiGHS.Optimizer, MOI.Silent() => true) diff --git a/examples/EMI_sink_source.jl b/examples/EMI_sink_source.jl index ca0f99e..5802d82 100644 --- a/examples/EMI_sink_source.jl +++ b/examples/EMI_sink_source.jl @@ -1,94 +1,3 @@ -# Import the required packages -using EnergyModelsBase -using EnergyModelsInvestments -using JuMP -using HiGHS -using PrettyTables -using TimeStruct - -""" - generate_example_ss_investment(lifemode = RollingLife; discount_rate = 0.05) - -Generate the data for an example consisting of an electricity source and sink. -The electricity source has initially no capacity. Hence, investments are required. -""" -function generate_example_ss_investment(lifemode = RollingLife; discount_rate = 0.05) - @info "Generate case data - Simple sink-source example" - - # Define the different resources and their emission intensity in tCO2/MWh - Power = ResourceCarrier("Power", 0.0) - CO2 = ResourceEmit("CO2", 1.0) - products = [Power, CO2] - - # Variables for the individual entries of the time structure - op_duration = 2 # Each operational period has a duration of 2 - op_number = 4 # There are in total 4 operational periods - operational_periods = SimpleTimes(op_number, op_duration) - - # Each operational period should correspond to a duration of 2 h while a duration if 1 - # of a strategic period should correspond to a year. - # This implies, that a strategic period is 8760 times longer than an operational period, - # resulting in the values below as "/year". - op_per_strat = 8760 - - sp_duration = 5 # The duration of a investment period is given as 5 years - - # Creation of the time structure and global data - T = TwoLevel(4, sp_duration, operational_periods; op_per_strat) - - # Create the global data - model = InvestmentModel( - Dict(CO2 => FixedProfile(10)), # Emission cap for CO₂ in t/year - Dict(CO2 => FixedProfile(0)), # Emission price for CO₂ in EUR/t - CO2, # CO₂ instance - discount_rate, # Discount rate in absolute value - ) - - # The lifetime of the technology is 15 years, requiring reinvestment in the - # 5th investment period - lifetime = FixedProfile(15) - - # Create the investment data for the source node - investment_data_source = SingleInvData( - FixedProfile(300 * 1e3), # capex [€/MW] - FixedProfile(50), # max installed capacity [MW] - ContinuousInvestment(FixedProfile(0), FixedProfile(30)), - # Line above: Investment mode with the following arguments: - # 1. argument: min added capactity per sp [MW] - # 2. argument: max added capactity per sp [MW] - lifemode(lifetime), # Lifetime mode - ) - - # Create the individual test nodes, corresponding to a system with an electricity - # demand/sink and source - nodes = [ - RefSource( - "electricity source", # Node id - FixedProfile(0), # Capacity in MW - FixedProfile(10), # Variable OPEX in EUR/MW - FixedProfile(5), # Fixed OPEX in EUR/MW/year - Dict(Power => 1), # Output from the Node, in this case, Power - [investment_data_source], # Additional data used for adding the investment data - ), - RefSink( - "electricity demand", # Node id - OperationalProfile([20, 30, 40, 30]), # Demand in MW - Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), - # Line above: Surplus and deficit penalty for the node in EUR/MWh - Dict(Power => 1), # Energy demand and corresponding ratio - ), - ] - - # Connect all nodes with the availability node for the overall energy/mass balance - links = [ - Direct("source-demand", nodes[1], nodes[2], Linear()), - ] - - # Input data structure - case = Case(T, products, [nodes, links], [[get_nodes, get_links]]) - return case, model -end - # Generate the case and model data and run the model case, model = generate_example_ss_investment() optimizer = optimizer_with_attributes(HiGHS.Optimizer, MOI.Silent() => true) diff --git a/examples/EMR_hydro_power.jl b/examples/EMR_hydro_power.jl index 9713d7f..05f3f87 100644 --- a/examples/EMR_hydro_power.jl +++ b/examples/EMR_hydro_power.jl @@ -1,107 +1,5 @@ -# Import the required packages -using EnergyModelsBase -using EnergyModelsRenewableProducers -using HiGHS -using JuMP -using PrettyTables -using TimeStruct - -const EMB = EnergyModelsBase - -""" - generate_example_data() - -Generate the data for an example consisting of a simple electricity network with a -non-dispatchable power source, a regulated hydro power plant, as well as a demand. -It illustrates how the hydro power plant can balance the intermittent renewable power -generation. -""" -function generate_example_data() - @info "Generate case data - Simple `HydroStor` example" - - # Define the different resources and their emission intensity in tCO2/MWh - # CO2 has to be defined, even if not used, as it is required for the `EnergyModel` type - CO2 = ResourceEmit("CO2", 1.0) - Power = ResourceCarrier("Power", 1.0) - products = [CO2, Power] - - # Variables for the individual entries of the time structure - op_duration = 2 # Each operational period has a duration of 2 - op_number = 4 # There are in total 4 operational periods - operational_periods = SimpleTimes(op_number, op_duration) - - # The number of operational periods times the duration of the operational periods. - # This implies, that a strategic period is 8 times longer than an operational period, - # resulting in the values below as "/8h". - op_per_strat = op_duration * op_number - - # Create the time structure and global data - T = TwoLevel(2, 1, operational_periods; op_per_strat) - model = OperationalModel( - Dict(CO2 => FixedProfile(10)), # Emission cap for CO2 in t/8h - Dict(CO2 => FixedProfile(0)), # Emission price for CO2 in EUR/t - CO2, # CO2 instance - ) - # Create the Availability/bus node for the system - av = GenAvailability(1, products) - - # Create a non-dispatchable renewable energy source - wind = NonDisRES( - "wind", # Node ID - FixedProfile(2), # Capacity in MW - OperationalProfile([0.9, 0.4, 0.1, 0.8]), # Profile - FixedProfile(5), # Variable OPEX in EUR/MW - FixedProfile(10), # Fixed OPEX in EUR/8h - Dict(Power => 1), # Output from the Node, in this gase, Power - ) - - # Create a regulated hydro power plant without storage capacity - hydro = HydroStor{CyclicStrategic}( - "hydropower", # Node ID - StorCapOpexFixed(FixedProfile(90), FixedProfile(3)), - # Line above for the storage level: - # Argument 1: Storage capacity in MWh - # Argument 2: Fixed OPEX in EUR/8h - StorCapOpexVar(FixedProfile(2.0), FixedProfile(8)), - # Line above for the discharge rate: - # Argument 1: Rate capacity in MW - # Argument 2: Variable OPEX in EUR/MWh - FixedProfile(10), # Initial storage level in MWh - FixedProfile(1), # Inflow to the Node in MW - FixedProfile(0.0), # Minimum storage level as fraction - Power, # Stored resource - Dict(Power => 0.9), # Input to the power plant, irrelevant in this case - Dict(Power => 1), # Output from the Node, in this gase, Power - ) - - # Create a power demand node - sink = RefSink( - "electricity demand", # Node id - FixedProfile(2), # Demand in MW - Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), - # Line above: Surplus and deficit penalty for the node in EUR/MWh - Dict(Power => 1), # Energy demand and corresponding ratio - ) - - # Create the array of ndoes - nodes = [av, wind, hydro, sink] - - # Connect all nodes with the availability node for the overall energy balance - links = [ - Direct("wind-av", wind, av), - Direct("hy-av", hydro, av), - Direct("av-hy", av, hydro), - Direct("av-demand", av, sink), - ] - - # Create the case dictionary - case = Case(T, products, [nodes, links]) - - return case, model -end - # Generate the case and model data and run the model -case, model = generate_example_data() +case, model = generate_example_hp() optimizer = optimizer_with_attributes(HiGHS.Optimizer, MOI.Silent() => true) m = EMB.run_model(case, model, optimizer) diff --git a/examples/EMR_simple_nondisres.jl b/examples/EMR_simple_nondisres.jl index 64d8eb9..7906c95 100644 --- a/examples/EMR_simple_nondisres.jl +++ b/examples/EMR_simple_nondisres.jl @@ -1,89 +1,5 @@ -# Import the required packages -using EnergyModelsBase -using EnergyModelsRenewableProducers -using HiGHS -using JuMP -using PrettyTables -using TimeStruct - -const EMB = EnergyModelsBase - -""" - generate_example_data() - -Generate the data for an example consisting of a simple electricity network with a -non-dispatchable power source, a standard source, as well as a demand. -It illustrates how the non-dispatchable power source requires a balancing power source. -""" -function generate_example_data() - @info "Generate case data - Simple `NonDisRES` example" - - # Define the different resources and their emission intensity in tCO2/MWh - # CO2 has to be defined, even if not used, as it is required for the `EnergyModel` type - CO2 = ResourceEmit("CO2", 1.0) - Power = ResourceCarrier("Power", 0.0) - products = [Power, CO2] - - # Variables for the individual entries of the time structure - op_duration = 2 # Each operational period has a duration of 2 - op_number = 4 # There are in total 4 operational periods - operational_periods = SimpleTimes(op_number, op_duration) - - # The number of operational periods times the duration of the operational periods. - # This implies, that a strategic period is 8 times longer than an operational period, - # resulting in the values below as "/8h". - op_per_strat = op_duration * op_number - - # Creation of the time structure and global data - T = TwoLevel(2, 1, operational_periods; op_per_strat) - model = OperationalModel( - Dict(CO2 => FixedProfile(10)), # Emission cap for CO2 in t/8h - Dict(CO2 => FixedProfile(0)), # Emission price for CO2 in EUR/t - CO2, # CO2 instance - ) - - # Create the individual test nodes, corresponding to a system with an electricity - # demand/sink and source - source = RefSource( - "source", # Node ID - FixedProfile(2), # Capacity in MW - FixedProfile(30), # Variable OPEX in EUR/MW - FixedProfile(10), # Fixed OPEX in EUR/8h - Dict(Power => 1), # Output from the Node, in this gase, Power - ) - sink = RefSink( - "electricity demand", # Node id - FixedProfile(2), # Demand in MW - Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), - # Line above: Surplus and deficit penalty for the node in EUR/MWh - Dict(Power => 1), # Energy demand and corresponding ratio - ) - nodes = [source, sink] - - # Connect the two nodes with each other - links = [Direct("source-demand", nodes[1], nodes[2], Linear())] - - # Create the additonal non-dispatchable power source - wind = NonDisRES( - "wind", # Node ID - FixedProfile(4), # Capacity in MW - OperationalProfile([0.9, 0.4, 0.1, 0.8]), # Profile of the NonDisRES node - FixedProfile(10), # Variable OPEX in EUR/MW - FixedProfile(10), # Fixed OPEX in EUR/8h - Dict(Power => 1), # Output from the Node, in this gase, Power - ) - - # Update the case data with the non-dispatchable power source and link - push!(nodes, wind) - link = Direct("wind-demand", nodes[3], nodes[2], Linear()) - push!(links, link) - case = Case(T, products, [nodes, links]) - - return case, model -end - # Generate the case and model data and run the model -case, model = generate_example_data() +case, model = generate_example_snd() optimizer = optimizer_with_attributes(HiGHS.Optimizer, MOI.Silent() => true) m = EMB.run_model(case, model, optimizer) diff --git a/examples/README.md b/examples/README.md index 7426b9a..83bb025 100644 --- a/examples/README.md +++ b/examples/README.md @@ -16,6 +16,9 @@ using Pkg Pkg.activate(exdir) Pkg.instantiate() -# Include the code into the Julia REPL to run an example (*e.g.*, EMI_geography.jl): +# Fetch functions to generate examples +include(joinpath(exdir, "generate_examples.jl")) + +# Run an example (*e.g.*, EMI_geography.jl): include(joinpath(exdir, "EMI_geography.jl")) ``` diff --git a/examples/design/case8/Area 1.yml b/examples/design/case8/Area 1.yml deleted file mode 100644 index 241aa18..0000000 --- a/examples/design/case8/Area 1.yml +++ /dev/null @@ -1,27 +0,0 @@ -n_Heat pump: - y: 42.23889 - x: -90.78404 -n_DH_Load_points_11171: - y: 40.22812 - x: -90.19889 -n_Water heater: - y: 39.7055 - x: -90.19889 -n_El 1: - y: 41.1405 - x: -90.89043 -n_Heating 1: - y: 40.44957 - x: -88.52325 -n_Waste heat data center: - y: 42.23889 - x: -92.40649 -n_El busbar_11125: - y: 40.0 - x: -92.0 -n_Hot water 1: - y: 39.68778 - x: -88.52325 -n_Heat generator: - y: 40.96334 - x: -90.19889 diff --git a/examples/design/case8/Area 2.yml b/examples/design/case8/Area 2.yml deleted file mode 100644 index f05e7cf..0000000 --- a/examples/design/case8/Area 2.yml +++ /dev/null @@ -1,18 +0,0 @@ -n_Heat central: - y: 36.69357 - x: -92.26637 -n_El busbar_1: - y: 38.0008 - x: -93.77252 -n_Power supply: - y: 39.6152 - x: -93.77252 -n_CHP Plant: - y: 36.69357 - x: -93.0135 -n_Waste supply: - y: 36.69357 - x: -93.77252 -n_El busbar_11124: - y: 39.08497 - x: -93.0 diff --git a/examples/design/case8/Area 3.yml b/examples/design/case8/Area 3.yml deleted file mode 100644 index a5fcd05..0000000 --- a/examples/design/case8/Area 3.yml +++ /dev/null @@ -1,6 +0,0 @@ -n_El busbar_11137: - y: 38.70869 - x: -91.36919 -n_EV charger: - y: 38.01638 - x: -91.3665 diff --git a/examples/design/case8/Area 4.yml b/examples/design/case8/Area 4.yml deleted file mode 100644 index 0b27358..0000000 --- a/examples/design/case8/Area 4.yml +++ /dev/null @@ -1,9 +0,0 @@ -n_Battery: - y: 41.55605 - x: -93.77252 -n_El busbar_11149: - y: 41.0 - x: -93.0 -n_Solar Power: - y: 40.29289 - x: -93.77252 diff --git a/examples/design/case8/Area 5.yml b/examples/design/case8/Area 5.yml deleted file mode 100644 index 50c96f3..0000000 --- a/examples/design/case8/Area 5.yml +++ /dev/null @@ -1,3 +0,0 @@ -n_DH_Junction_points_11165: - y: 36.73543 - x: -90.92699 diff --git a/examples/design/case8/Area 6.yml b/examples/design/case8/Area 6.yml deleted file mode 100644 index fdd3068..0000000 --- a/examples/design/case8/Area 6.yml +++ /dev/null @@ -1,3 +0,0 @@ -n_DH_Junction_points_11167: - y: 38.22739 - x: -90.30779 diff --git a/examples/design/case8/Area 7.yml b/examples/design/case8/Area 7.yml deleted file mode 100644 index d96357a..0000000 --- a/examples/design/case8/Area 7.yml +++ /dev/null @@ -1,3 +0,0 @@ -n_DH_Junction_points_11169: - y: 38.6888 - x: -89.43499 diff --git a/examples/design/case8/Area 8.yml b/examples/design/case8/Area 8.yml deleted file mode 100644 index 148fb0f..0000000 --- a/examples/design/case8/Area 8.yml +++ /dev/null @@ -1,6 +0,0 @@ -n_DH_Junction_points_11173: - y: 38.08448 - x: -87.89029 -n_Heat for port and ships: - y: 36.69357 - x: -87.89029 diff --git a/examples/design/case8/Area 9.yml b/examples/design/case8/Area 9.yml deleted file mode 100644 index c37baab..0000000 --- a/examples/design/case8/Area 9.yml +++ /dev/null @@ -1,6 +0,0 @@ -n_Heat for airport: - y: 36.69357 - x: -89.00637 -n_DH_Junction_points_11177: - y: 37.67531 - x: -89.00637 diff --git a/examples/design/case8/top_level.yml b/examples/design/case8/top_level.yml deleted file mode 100644 index 960201e..0000000 --- a/examples/design/case8/top_level.yml +++ /dev/null @@ -1,27 +0,0 @@ -Area 4: - y: 41.0 - x: -93.0 -Area 1: - y: 40.0 - x: -92.0 -Area 3: - y: 38.70869 - x: -91.36919 -Area 6: - y: 38.22739 - x: -90.30779 -Area 2: - y: 39.08497 - x: -93.0 -Area 7: - y: 38.6888 - x: -89.43499 -Area 8: - y: 38.08448 - x: -87.89029 -Area 5: - y: 36.73543 - x: -90.92699 -Area 9: - y: 37.67531 - x: -89.00637 diff --git a/examples/generate_examples.jl b/examples/generate_examples.jl new file mode 100644 index 0000000..ed197d3 --- /dev/null +++ b/examples/generate_examples.jl @@ -0,0 +1,1368 @@ +using Pkg +# Activate the local environment including EnergyModelsGeography, HiGHS, PrettyTables +Pkg.activate(@__DIR__) +# Use dev version if run as part of tests +haskey(ENV, "EMX_TEST") && Pkg.develop(path = joinpath(@__DIR__, "..")) +# Install the dependencies. +Pkg.instantiate() + +using EnergyModelsBase +using EnergyModelsGeography +using EnergyModelsInvestments +using EnergyModelsRenewableProducers +using TimeStruct +using JuMP +using HiGHS +using PrettyTables + +const EMB = EnergyModelsBase +const EMG = EnergyModelsGeography + +""" + generate_example_network() + +Generate the data for an example consisting of a simple electricity network. +The more stringent CO₂ emission in latter investment periods force the utilization of the +more expensive natural gas power plant with CCS to reduce emissions. +""" +function generate_example_network() + @info "Generate case data - Simple network example" + + # Define the different resources and their emission intensity in tCO2/MWh + NG = ResourceEmit("NG", 0.2) + Coal = ResourceCarrier("Coal", 0.35) + Power = ResourceCarrier("Power", 0.0) + CO2 = ResourceEmit("CO2", 1.0) + products = [NG, Coal, Power, CO2] + + # Variables for the individual entries of the time structure + op_duration = 2 # Each operational period has a duration of 2 + op_number = 4 # There are in total 4 operational periods + operational_periods = SimpleTimes(op_number, op_duration) + + # The number of operational periods times the duration of the operational periods, which + # can also be extracted using the function `duration` of a `SimpleTimes` structure. + # This implies, that a strategic period is 8 times longer than an operational period, + # resulting in the values below as "/8h". + op_per_strat = op_duration * op_number + + # Creation of the time structure and global data + T = TwoLevel(4, 1, operational_periods; op_per_strat) + model = OperationalModel( + Dict( # Emission cap for CO₂ in t/8h and for NG in MWh/8h + CO2 => StrategicProfile([160, 140, 120, 100]), + NG => FixedProfile(1e6), + ), + Dict( # Emission price for CO₂ in EUR/t and for NG in EUR/MWh + CO2 => FixedProfile(0), + NG => FixedProfile(0), + ), + CO2, # CO2 instance + ) + + # Creation of the emission data for the individual nodes. + capture_data = CaptureEnergyEmissions(0.9) + emission_data = EmissionsEnergy() + + # Create the individual test nodes, corresponding to a system with an electricity demand/sink, + # coal and nautral gas sources, coal and natural gas (with CCS) power plants and CO₂ storage. + nodes = [ + GenAvailability("Availability", products), + RefSource( + "NG source", # Node id + FixedProfile(100), # Capacity in MW + FixedProfile(30), # Variable OPEX in EUR/MW + FixedProfile(0), # Fixed OPEX in EUR/MW/8h + Dict(NG => 1), # Output from the Node, in this case, NG + ), + RefSource( + "coal source", # Node id + FixedProfile(100), # Capacity in MW + FixedProfile(9), # Variable OPEX in EUR/MWh + FixedProfile(0), # Fixed OPEX in EUR/MW/8h + Dict(Coal => 1), # Output from the Node, in this case, coal + ), + RefNetworkNode( + "NG+CCS power plant", # Node id + FixedProfile(25), # Capacity in MW + FixedProfile(5.5), # Variable OPEX in EUR/MWh + FixedProfile(0), # Fixed OPEX in EUR/MW/8h + Dict(NG => 2), # Input to the node with input ratio + Dict(Power => 1, CO2 => 1), # Output from the node with output ratio + # Line above: CO2 is required as output for variable definition, but the + # value does not matter + [capture_data], # Additonal data for emissions and CO₂ capture + ), + RefNetworkNode( + "coal power plant", # Node id + FixedProfile(25), # Capacity in MW + FixedProfile(6), # Variable OPEX in EUR/MWh + FixedProfile(0), # Fixed OPEX in EUR/MW/8h + Dict(Coal => 2.5), # Input to the node with input ratio + Dict(Power => 1), # Output from the node with output ratio + [emission_data], # Additonal data for emissions + ), + RefStorage{AccumulatingEmissions}( + "CO2 storage", # Node id + StorCapOpex( + FixedProfile(60), # Charge capacity in t/h + FixedProfile(9.1), # Storage variable OPEX for the charging in EUR/t + FixedProfile(0) # Storage fixed OPEX for the charging in EUR/(t/h 8h) + ), + StorCap(FixedProfile(600)), # Storage capacity in t + CO2, # Stored resource + Dict(CO2 => 1, Power => 0.02), # Input resource with input ratio + # Line above: This implies that storing CO₂ requires Power + Dict(CO2 => 1), # Output from the node with output ratio + # In practice, for CO₂ storage, this is never used. + ), + RefSink( + "electricity demand", # Node id + OperationalProfile([20, 30, 40, 30]), # Demand in MW + Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), + # Line above: Surplus and deficit penalty for the node in EUR/MWh + Dict(Power => 1), # Energy demand and corresponding ratio + ), + ] + + # Connect all nodes with the availability node for the overall energy/mass balance + links = [ + Direct("Av-NG_pp", nodes[1], nodes[4], Linear()) + Direct("Av-coal_pp", nodes[1], nodes[5], Linear()) + Direct("Av-CO2_stor", nodes[1], nodes[6], Linear()) + Direct("Av-demand", nodes[1], nodes[7], Linear()) + Direct("NG_src-av", nodes[2], nodes[1], Linear()) + Direct("Coal_src-av", nodes[3], nodes[1], Linear()) + Direct("NG_pp-av", nodes[4], nodes[1], Linear()) + Direct("Coal_pp-av", nodes[5], nodes[1], Linear()) + Direct("CO2_stor-av", nodes[6], nodes[1], Linear()) + ] + + # Input data structure + case = Case(T, products, [nodes, links], [[get_nodes, get_links]]) + return case, model +end + +""" + generate_example_ss() + +Generate the data for an example consisting of an electricity source and sink. It shows how +the source adjusts to the demand. +""" +function generate_example_ss() + @info "Generate case data - Simple sink-source example" + + # Define the different resources and their emission intensity in tCO2/MWh + Power = ResourceCarrier("Power", 0.0) + CO2 = ResourceEmit("CO2", 1.0) + products = [Power, CO2] + + # Variables for the individual entries of the time structure + op_duration = 2 # Each operational period has a duration of 2 + op_number = 4 # There are in total 4 operational periods + operational_periods = SimpleTimes(op_number, op_duration) + + # The number of operational periods times the duration of the operational periods, which + # can also be extracted using the function `duration` of a `SimpleTimes` structure. + # This implies, that a strategic period is 8 times longer than an operational period, + # resulting in the values below as "/8h". + op_per_strat = op_duration * op_number + + # Creation of the time structure and global data + T = TwoLevel(2, 1, operational_periods; op_per_strat) + model = OperationalModel( + Dict(CO2 => FixedProfile(10)), # Emission cap for CO₂ in t/8h + Dict(CO2 => FixedProfile(0)), # Emission price for CO₂ in EUR/t + CO2, # CO₂ instance + ) + + # Create the individual test nodes, corresponding to a system with an electricity + # demand/sink and source + nodes = [ + RefSource( + "electricity source", # Node id + FixedProfile(50), # Capacity in MW + FixedProfile(30), # Variable OPEX in EUR/MW + FixedProfile(0), # Fixed OPEX in EUR/MW/8h + Dict(Power => 1), # Output from the Node, in this case, Power + ), + RefSink( + "electricity demand", # Node id + OperationalProfile([20, 30, 40, 30]), # Demand in MW + Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), + # Line above: Surplus and deficit penalty for the node in EUR/MWh + Dict(Power => 1), # Energy demand and corresponding ratio + ), + ] + + # Connect all nodes with the availability node for the overall energy/mass balance + links = [ + Direct("source-demand", nodes[1], nodes[2], Linear()), + ] + + # Input data structure + case = Case(T, products, [nodes, links], [[get_nodes, get_links]]) + return case, model +end + +""" + generate_example_geo() + +Generate the data for an example consisting of a simple electricity network. The simple \ +network is existing within 5 regions with differing demand. Each region has the same \ +technologies. + +The example is partly based on the provided example `network.jl` in `EnergyModelsBase`. +""" +function generate_example_geo() + @info "Generate case data - Simple network example with 5 regions with the same \ + technologies" + + # Retrieve the products + products = get_resources() + NG = products[1] + Power = products[3] + CO2 = products[4] + + # Variables for the individual entries of the time structure + op_duration = 1 # Each operational period has a duration of 2 + op_number = 24 # There are in total 4 operational periods + operational_periods = SimpleTimes(op_number, op_duration) + + # The number of operational periods times the duration of the operational periods, which + # can also be extracted using the function `duration` of a `SimpleTimes` structure. + # This implies, that a strategic period is 8 times longer than an operational period, + # resulting in the values below as "/24h". + op_per_strat = op_duration * op_number + + # Creation of the time structure and global data + T = TwoLevel(4, 1, operational_periods; op_per_strat) + model = OperationalModel( + Dict( + CO2 => StrategicProfile([160, 140, 120, 100]), # CO₂ emission cap in t/24h + NG => FixedProfile(1e6) # NG cap in MWh/24h + ), + Dict( + CO2 => FixedProfile(0), # CO₂ emission cost in EUR/t + NG => FixedProfile(0) # NG emission cost in EUR/t + ), + CO2, + ) + + # Create input data for the individual areas + # The input data is based on scaling factors and/or specified demands + area_ids = [1, 2, 3, 4, 5, 6, 7] + d_scale = Dict(1 => 3.0, 2 => 1.5, 3 => 1.0, 4 => 0.5, 5 => 0.5, 6 => 0.0, 7 => 3.0) + mc_scale = Dict(1 => 2.0, 2 => 2.0, 3 => 1.5, 4 => 0.5, 5 => 0.5, 6 => 0.5, 7 => 3.0) + + op_data = OperationalProfile([ + 10, + 10, + 10, + 10, + 35, + 40, + 45, + 45, + 50, + 50, + 60, + 60, + 50, + 45, + 45, + 40, + 35, + 40, + 45, + 40, + 35, + 30, + 30, + 30, + ]) + tromsø_demand = [op_data; + op_data; + op_data; + op_data + ] + demand = Dict( + 1 => false, + 2 => false, + 3 => false, + 4 => tromsø_demand, + 5 => false, + 6 => false, + 7 => false, + ) + + # Create identical areas with index according to the input array + an = Dict() + nodes = EMB.Node[] + links = Link[] + for a_id ∈ area_ids + n, l = get_sub_system_data( + a_id, + products; + mc_scale = mc_scale[a_id], + d_scale = d_scale[a_id], + demand = demand[a_id], + ) + append!(nodes, n) + append!(links, l) + + # Add area node for each subsystem + an[a_id] = n[1] + end + + # Create the individual areas + # The individual fields are: + # 1. id - Identifier of the area + # 2. name - Name of the area + # 3. lon - Longitudinal position of the area + # 4. lon - Latitudinal position of the area + # 5. node - Availability node of the area + 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.4366, an[3]), + RefArea(4, "Tromsø", 18.953, 69.669, an[4]), + RefArea(5, "Kristiansand", 7.984, 58.146, an[5]), + RefArea(6, "Sørlige Nordsjø II", 6.836, 57.151, an[6]), + RefArea(7, "Danmark", 8.614, 56.359, an[7])] + + # Create the individual transmission modes to transport the energy between the + # individual areass. + # The individuaal fields are explained below, while the other fields are: + # 1. Identifier of the transmission mode + # 2. Transported resource + # 7. 2 for bidirectional transport, 1 for unidirectional + # 8. Potential additional data + cap_ohl = FixedProfile(50.0) # Capacity of an overhead line in MW + cap_lng = FixedProfile(100.0) # Capacity of the LNG transport in MW + loss = FixedProfile(0.05) # Relative loss of either transport mode + opex_var = FixedProfile(0.05) # Variable OPEX in EUR/MWh + opex_fix = FixedProfile(0.05) # Fixed OPEX in EUR/24h + + OB_OverheadLine_50MW = RefStatic("OB_PowerLine_50", Power, cap_ohl, loss, opex_var, opex_fix, 2) + OT_OverheadLine_50MW = RefStatic("OT_PowerLine_50", Power, cap_ohl, loss, opex_var, opex_fix, 2) + OK_OverheadLine_50MW = RefStatic("OK_PowerLine_50", Power, cap_ohl, loss, opex_var, opex_fix, 2) + BT_OverheadLine_50MW = RefStatic("BT_PowerLine_50", Power, cap_ohl, loss, opex_var, opex_fix, 2) + BTN_LNG_Ship_100MW = RefDynamic("BTN_LNG_100", NG, cap_lng, loss, opex_var, opex_fix, 1) + BK_OverheadLine_50MW = RefStatic("BK_PowerLine_50", Power, cap_ohl, loss, opex_var, opex_fix, 2) + TTN_OverheadLine_50MW = RefStatic("TTN_PowerLine_50", Power, cap_ohl, loss, opex_var, opex_fix, 2) + KS_OverheadLine_50MW = RefStatic("KS_PowerLine_50", Power, cap_ohl, loss, opex_var, opex_fix, 2) + SD_OverheadLine_50MW = RefStatic("SD_PowerLine_50", Power, cap_ohl, loss, opex_var, opex_fix, 2) + + # Create the different transmission corridors between the individual areas + transmissions = [ + Transmission(areas[1], areas[2], [OB_OverheadLine_50MW]), + Transmission(areas[1], areas[3], [OT_OverheadLine_50MW]), + Transmission(areas[1], areas[5], [OK_OverheadLine_50MW]), + Transmission(areas[2], areas[3], [BT_OverheadLine_50MW]), + Transmission(areas[2], areas[4], [BTN_LNG_Ship_100MW]), + Transmission(areas[2], areas[5], [BK_OverheadLine_50MW]), + Transmission(areas[3], areas[4], [TTN_OverheadLine_50MW]), + Transmission(areas[5], areas[6], [KS_OverheadLine_50MW]), + Transmission(areas[6], areas[7], [SD_OverheadLine_50MW]), + ] + + # Input data structure + case = Case( + T, + products, + [nodes, links, areas, transmissions], + [[get_nodes, get_links], [get_areas, get_transmissions]], + ) + return case, model +end + +function get_resources() + + # Define the different resources + NG = ResourceEmit("NG", 0.2) + Coal = ResourceCarrier("Coal", 0.35) + Power = ResourceCarrier("Power", 0.0) + CO2 = ResourceEmit("CO2", 1.0) + products = [NG, Coal, Power, CO2] + + return products +end + +# Subsystem test data for geography package. All subsystems are the same, except for the +# profiles +# The subsystem is similar to the subsystem in the `network.jl` example of EnergyModelsBase. +function get_sub_system_data( + i, + products; + mc_scale::Float64 = 1.0, + d_scale::Float64 = 1.0, + demand = false, +) + NG, Coal, Power, CO2 = products + + # Use of standard demand if not provided differently + d_standard = OperationalProfile([ + 20, + 20, + 20, + 20, + 25, + 30, + 35, + 35, + 40, + 40, + 40, + 40, + 40, + 35, + 35, + 30, + 25, + 30, + 35, + 30, + 25, + 20, + 20, + 20, + ]) + if demand == false + demand = [d_standard; d_standard; d_standard; d_standard] + demand *= d_scale + end + + # Create the individual test nodes, corresponding to a system with an electricity demand/sink, + # coal and nautral gas sources, coal and natural gas (with CCS) power plants and CO₂ storage. + j = (i - 1) * 100 + nodes = [ + GeoAvailability(j + 1, products), + RefSource( + j + 2, # Node id + FixedProfile(1e12), # Capacity in MW + FixedProfile(30 * mc_scale), # Variable OPEX in EUR/MW + FixedProfile(0), # Fixed OPEX in EUR/24h + Dict(NG => 1), # Output from the Node, in this case, NG + ), + RefSource( + j + 3, # Node id + FixedProfile(1e12), # Capacity in MW + FixedProfile(9 * mc_scale), # Variable OPEX in EUR/MWh + FixedProfile(0), # Fixed OPEX in EUR/24h + Dict(Coal => 1), # Output from the Node, in this case, coal + ), + RefNetworkNode( + j + 4, # Node id + FixedProfile(25), # Capacity in MW + FixedProfile(5.5 * mc_scale), # Variable OPEX in EUR/MWh + FixedProfile(0), # Fixed OPEX in EUR/24h + Dict(NG => 2), # Input to the node with input ratio + Dict(Power => 1, CO2 => 1), # Output from the node with output ratio + # Line above: CO2 is required as output for variable definition, but the + # value does not matter + [CaptureEnergyEmissions(0.9)], # Additonal data for emissions and CO₂ capture + ), + RefNetworkNode( + j + 5, # Node id + FixedProfile(25), # Capacity in MW + FixedProfile(6 * mc_scale), # Variable OPEX in EUR/MWh + FixedProfile(0), # Fixed OPEX in EUR/24h + Dict(Coal => 2.5), # Input to the node with input ratio + Dict(Power => 1), # Output from the node with output ratio + [EmissionsEnergy()], # Additonal data for emissions + ), + RefStorage{AccumulatingEmissions}( + j + 6, # Node id + StorCapOpex( + FixedProfile(20), # Charge capacity in t/h + FixedProfile(9.1), # Storage variable OPEX for the charging in EUR/t + FixedProfile(0) # Storage fixed OPEX for the charging in EUR/(t/h 8h) + ), + StorCap(FixedProfile(600)), # Storage capacity in t + CO2, # Stored resource + 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 + StrategicProfile(demand), # Demand in MW + Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), + # Line above: Surplus and deficit penalty for the node in EUR/MWh + Dict(Power => 1), # Energy demand and corresponding ratio + ), + ] + + # Connect all nodes with the availability node for the overall energy/mass balance + links = [ + Direct(j + 14, nodes[1], nodes[4], Linear()) + Direct(j + 15, nodes[1], nodes[5], Linear()) + Direct(j + 16, nodes[1], nodes[6], Linear()) + Direct(j + 17, nodes[1], nodes[7], Linear()) + Direct(j + 21, nodes[2], nodes[1], Linear()) + Direct(j + 31, nodes[3], nodes[1], Linear()) + Direct(j + 41, nodes[4], nodes[1], Linear()) + Direct(j + 51, nodes[5], nodes[1], Linear()) + Direct(j + 61, nodes[6], nodes[1], Linear()) + ] + return nodes, links +end + +""" + generate_example_data_geo() + +Generate the data for an example consisting of a simple electricity network. The simple \ +network is existing within 5 regions with differing demand. Each region has the same \ +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 + products = get_resources_inv() + NG = products[1] + Power = products[3] + CO2 = products[4] + + # Create input data for the areas + 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) + + # Create identical areas with index according to input array + an = Dict() + 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) + + # Add area node for each subsystem + an[a_id] = n[1] + end + + # Create the individual areas + 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]), + ] + + # Create the investment data for the different power line investment modes + inv_data_12 = SingleInvData( + FixedProfile(500), + FixedProfile(50), + FixedProfile(0), + BinaryInvestment(FixedProfile(50.0)), + ) + + inv_data_13 = SingleInvData( + FixedProfile(10), + FixedProfile(100), + FixedProfile(0), + SemiContinuousInvestment(FixedProfile(10), FixedProfile(100)), + ) + + inv_data_23 = SingleInvData( + FixedProfile(10), + FixedProfile(50), + FixedProfile(20), + DiscreteInvestment(FixedProfile(6)), + ) + + inv_data_34 = SingleInvData( + FixedProfile(10), + FixedProfile(50), + FixedProfile(0), + ContinuousInvestment(FixedProfile(1), FixedProfile(100)), + ) + + # Create the TransmissionModes and the Transmission corridors + OverheadLine_50MW_12 = RefStatic( + "PowerLine_50", + Power, + FixedProfile(50.0), + FixedProfile(0.05), + FixedProfile(0), + FixedProfile(0), + 2, + [inv_data_12], + ) + OverheadLine_50MW_13 = RefStatic( + "PowerLine_50", + Power, + FixedProfile(50.0), + FixedProfile(0.05), + FixedProfile(0), + FixedProfile(0), + 2, + [inv_data_13], + ) + OverheadLine_50MW_23 = RefStatic( + "PowerLine_50", + Power, + FixedProfile(50.0), + FixedProfile(0.05), + FixedProfile(0), + FixedProfile(0), + 2, + [inv_data_23], + ) + OverheadLine_50MW_34 = RefStatic( + "PowerLine_50", + Power, + FixedProfile(50.0), + FixedProfile(0.05), + FixedProfile(0), + FixedProfile(0), + 2, + [inv_data_34], + ) + LNG_Ship_100MW = RefDynamic( + "LNG_100", + NG, + FixedProfile(100.0), + FixedProfile(0.05), + FixedProfile(0), + FixedProfile(0), + 2, + [], + ) + + transmissions = [ + Transmission(areas[1], areas[2], [OverheadLine_50MW_12]), + Transmission(areas[1], areas[3], [OverheadLine_50MW_13]), + Transmission(areas[2], areas[3], [OverheadLine_50MW_23]), + Transmission(areas[3], areas[4], [OverheadLine_50MW_34]), + Transmission(areas[4], areas[2], [LNG_Ship_100MW]), + ] + + # Creation of the time structure and global data + 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) + + # Input data structure + case = Case( + T, + products, + [nodes, links, areas, transmissions], + [[get_nodes, get_links], [get_areas, get_transmissions]], + ) + return case, modeltype +end + +function get_resources_inv() + + # Define the different resources + NG = ResourceEmit("NG", 0.2) + Coal = ResourceCarrier("Coal", 0.35) + Power = ResourceCarrier("Power", 0.0) + CO2 = ResourceEmit("CO2", 1.0) + products = [NG, Coal, Power, CO2] + + return products +end + +function get_sub_system_data_inv( + i, + products; + gen_scale::Float64 = 1.0, + mc_scale::Float64 = 1.0, + d_scale::Float64 = 1.0, + demand = false, +) + NG, Coal, Power, CO2 = products + + if demand == false + demand = [ + OperationalProfile([ + 20, + 20, + 20, + 20, + 25, + 30, + 35, + 35, + 40, + 40, + 40, + 40, + 40, + 35, + 35, + 30, + 25, + 30, + 35, + 30, + 25, + 20, + 20, + 20, + ]), + OperationalProfile([ + 20, + 20, + 20, + 20, + 25, + 30, + 35, + 35, + 40, + 40, + 40, + 40, + 40, + 35, + 35, + 30, + 25, + 30, + 35, + 30, + 25, + 20, + 20, + 20, + ]), + OperationalProfile([ + 20, + 20, + 20, + 20, + 25, + 30, + 35, + 35, + 40, + 40, + 40, + 40, + 40, + 35, + 35, + 30, + 25, + 30, + 35, + 30, + 25, + 20, + 20, + 20, + ]), + OperationalProfile([ + 20, + 20, + 20, + 20, + 25, + 30, + 35, + 35, + 40, + 40, + 40, + 40, + 40, + 35, + 35, + 30, + 25, + 30, + 35, + 30, + 25, + 20, + 20, + 20, + ]), + ] + demand *= d_scale + end + + j = (i - 1) * 100 + nodes = [ + GeoAvailability(j + 1, products), + RefSink( + j + 2, + StrategicProfile(demand), + Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), + Dict(Power => 1), + ), + RefSource( + j + 3, + FixedProfile(30), + FixedProfile(30 * mc_scale), + FixedProfile(100), + Dict(NG => 1), + [ + SingleInvData( + FixedProfile(1000), # capex [€/kW] + FixedProfile(200), # max installed capacity [kW] + ContinuousInvestment(FixedProfile(10), FixedProfile(200)), # investment mode + ), + ], + ), + RefSource( + j + 4, + FixedProfile(9), + FixedProfile(9 * mc_scale), + FixedProfile(100), + Dict(Coal => 1), + [ + SingleInvData( + FixedProfile(1000), # capex [€/kW] + FixedProfile(200), # max installed capacity [kW] + FixedProfile(0), + ContinuousInvestment(FixedProfile(10), FixedProfile(200)), # investment mode + ), + ], + ), + RefNetworkNode( + j + 5, + FixedProfile(0), + FixedProfile(5.5 * mc_scale), + FixedProfile(100), + Dict(NG => 2), + Dict(Power => 1, CO2 => 0), + [ + SingleInvData( + FixedProfile(600), # capex [€/kW] + FixedProfile(25), # max installed capacity [kW] + ContinuousInvestment(FixedProfile(0), FixedProfile(25)), # investment mode + ), + CaptureEnergyEmissions(0.9), + ], + ), + RefNetworkNode( + j + 6, + FixedProfile(0), + FixedProfile(6 * mc_scale), + FixedProfile(100), + Dict(Coal => 2.5), + Dict(Power => 1), + [ + SingleInvData( + FixedProfile(800), # capex [€/kW] + FixedProfile(25), # max installed capacity [kW] + ContinuousInvestment(FixedProfile(0), FixedProfile(25)), # investment mode + ), + EmissionsEnergy(), + ], + ), + RefStorage{AccumulatingEmissions}( + j + 7, + StorCapOpex(FixedProfile(0), FixedProfile(9.1 * mc_scale), FixedProfile(100)), + StorCap(FixedProfile(0)), + CO2, + Dict(CO2 => 1, Power => 0.02), + Dict(CO2 => 1), + [ + StorageInvData( + charge = NoStartInvData( + FixedProfile(500), + FixedProfile(600), + ContinuousInvestment(FixedProfile(0), FixedProfile(600)), + ), + level = NoStartInvData( + FixedProfile(500), + FixedProfile(600), + ContinuousInvestment(FixedProfile(0), FixedProfile(600)), + ), + ), + ], + ), + RefNetworkNode( + j + 8, + FixedProfile(0), + FixedProfile(0 * mc_scale), + FixedProfile(0), + Dict(Coal => 2.5), + Dict(Power => 1), + [ + SingleInvData( + FixedProfile(10000), # capex [€/kW] + FixedProfile(25), # max installed capacity [kW] + ContinuousInvestment(FixedProfile(0), FixedProfile(2)), # investment mode + ), + EmissionsEnergy(), + ], + ), + RefStorage{AccumulatingEmissions}( + j + 9, + StorCapOpex(FixedProfile(3), FixedProfile(0 * mc_scale), FixedProfile(0)), + StorCap(FixedProfile(5)), + CO2, + Dict(CO2 => 1, Power => 0.02), + Dict(CO2 => 1), + [ + StorageInvData( + charge = NoStartInvData( + FixedProfile(500), + FixedProfile(30), + ContinuousInvestment(FixedProfile(0), FixedProfile(3)), + ), + level = NoStartInvData( + FixedProfile(500), + FixedProfile(50), + ContinuousInvestment(FixedProfile(0), FixedProfile(2)), + ), + ), + ], + ), + RefNetworkNode( + j + 10, + FixedProfile(0), + FixedProfile(0 * mc_scale), + FixedProfile(0), + Dict(Coal => 2.5), + Dict(Power => 1), + [ + SingleInvData( + FixedProfile(10000), # capex [€/kW] + FixedProfile(10000), # max installed capacity [kW] + ContinuousInvestment(FixedProfile(0), FixedProfile(10000)), # investment mode + ), + EmissionsEnergy(), + ], + ), + ] + + links = [ + Direct(j * 10 + 15, nodes[1], nodes[5], Linear()) + Direct(j * 10 + 16, nodes[1], nodes[6], Linear()) + Direct(j * 10 + 17, nodes[1], nodes[7], Linear()) + Direct(j * 10 + 18, nodes[1], nodes[8], Linear()) + Direct(j * 10 + 19, nodes[1], nodes[9], Linear()) + Direct(j * 10 + 110, nodes[1], nodes[10], Linear()) + Direct(j * 10 + 12, nodes[1], nodes[2], Linear()) + Direct(j * 10 + 31, nodes[3], nodes[1], Linear()) + Direct(j * 10 + 41, nodes[4], nodes[1], Linear()) + Direct(j * 10 + 51, nodes[5], nodes[1], Linear()) + Direct(j * 10 + 61, nodes[6], nodes[1], Linear()) + Direct(j * 10 + 71, nodes[7], nodes[1], Linear()) + Direct(j * 10 + 81, nodes[8], nodes[1], Linear()) + Direct(j * 10 + 91, nodes[9], nodes[1], Linear()) + Direct(j * 10 + 101, nodes[10], nodes[1], Linear()) + ] + return nodes, links +end + +""" + generate_example_network_investment() + +Generate the data for an example consisting of a simple electricity network. +The more stringent CO₂ emission in latter investment periods force the investment into both +the natural gas power plant with CCS and the CO₂ storage node. +""" +function generate_example_network_investment() + @info "Generate case data - Simple network example with investments" + + # Define the different resources and their emission intensity in tCO2/MWh + NG = ResourceEmit("NG", 0.2) + Coal = ResourceCarrier("Coal", 0.35) + Power = ResourceCarrier("Power", 0.0) + CO2 = ResourceEmit("CO2", 1.0) + products = [NG, Coal, Power, CO2] + + # Variables for the individual entries of the time structure + op_duration = 2 # Each operational period has a duration of 2 + op_number = 4 # There are in total 4 operational periods + operational_periods = SimpleTimes(op_number, op_duration) + + # Each operational period should correspond to a duration of 2 h while a duration if 1 + # of a strategic period should correspond to a year. + # This implies, that a strategic period is 8760 times longer than an operational period, + # resulting in the values below as "/year". + op_per_strat = 8760 + + # Creation of the time structure and global data + T = TwoLevel(4, 1, operational_periods; op_per_strat) + model = InvestmentModel( + Dict( # Emission cap for CO₂ in t/year and for NG in MWh/year + CO2 => StrategicProfile([170, 150, 130, 110]) * 1000, + NG => FixedProfile(1e6), + ), + Dict( # Emission price for CO₂ in EUR/t and for NG in EUR/MWh + CO2 => FixedProfile(0), + NG => FixedProfile(0), + ), + CO2, # CO2 instance + 0.07, # Discount rate in absolute value + ) + + # Creation of the emission data for the individual nodes. + capture_data = CaptureEnergyEmissions(0.9) + emission_data = EmissionsEnergy() + + # Create the individual test nodes, corresponding to a system with an electricity demand/sink, + # coal and nautral gas sources, coal and natural gas (with CCS) power plants and CO₂ storage. + nodes = [ + GenAvailability("Availability", products), + RefSource( + "NG source", # Node id + FixedProfile(100), # Capacity in MW + FixedProfile(30), # Variable OPEX in EUR/MWh + FixedProfile(0), # Fixed OPEX in EUR/MW/year + Dict(NG => 1), # Output from the Node, in this case, NG + ), + RefSource( + "coal source", # Node id + FixedProfile(100), # Capacity in MW + FixedProfile(9), # Variable OPEX in EUR/MWh + FixedProfile(0), # Fixed OPEX in EUR/MW/year + Dict(Coal => 1), # Output from the Node, in this case, coal + ), + RefNetworkNode( + "NG+CCS power plant", # Node id + FixedProfile(0), # Capacity in MW + FixedProfile(5.5), # Variable OPEX in EUR/MWh + FixedProfile(0), # Fixed OPEX in EUR/MW/year + Dict(NG => 2), # Input to the node with input ratio + Dict(Power => 1, CO2 => 0), # Output from the node with output ratio + # Line above: CO2 is required as output for variable definition, but the + # value does not matter + [ + capture_data, # Additonal data for emissions and CO₂ capture + SingleInvData( + FixedProfile(600 * 1e3), # Capex in EUR/MW + FixedProfile(40), # Max installed capacity [MW] + SemiContinuousInvestment(FixedProfile(5), FixedProfile(40)), + # Line above: Investment mode with the following arguments: + # 1. argument: min added capactity per sp [MW] + # 2. argument: max added capactity per sp [MW] + ), + ], + ), + RefNetworkNode( + "coal power plant", # Node id + FixedProfile(40), # Capacity in MW + FixedProfile(6), # Variable OPEX in EUR/MWh + FixedProfile(0), # Fixed OPEX in EUR/MW/year + Dict(Coal => 2.5), # Input to the node with input ratio + Dict(Power => 1), # Output from the node with output ratio + [emission_data], # Additonal data for emissions + ), + RefStorage{AccumulatingEmissions}( + "CO2 storage", # Node id + StorCapOpex( + FixedProfile(0), # Charge capacity in t/h + FixedProfile(9.1), # Storage variable OPEX for the charging in EUR/t + FixedProfile(0) # Storage fixed OPEX for the charging in EUR/(t/h year) + ), + StorCap(FixedProfile(1e8)), # Storage capacity in t + CO2, # Stored resource + Dict(CO2 => 1, Power => 0.02), # Input resource with input ratio + # Line above: This implies that storing CO₂ requires Power + Dict(CO2 => 1), # Output from the node with output ratio + # In practice, for CO₂ storage, this is never used. + [ + StorageInvData( + charge = NoStartInvData( + FixedProfile(200 * 1e3), # CAPEX [EUR/(t/h)] + FixedProfile(60), # Max installed capacity [EUR/(t/h)] + ContinuousInvestment(FixedProfile(0), FixedProfile(5)), + # Line above: Investment mode with the following arguments: + # 1. argument: min added capactity per sp [t/h] + # 2. argument: max added capactity per sp [t/h] + UnlimitedLife(), # Lifetime mode + ), + ), + ], + ), + RefSink( + "electricity demand", # Node id + OperationalProfile([20, 30, 40, 30]), # Demand in MW + Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), + # Line above: Surplus and deficit penalty for the node in EUR/MWh + Dict(Power => 1), # Energy demand and corresponding ratio + ), + ] + + # Connect all nodes with the availability node for the overall energy/mass balance + links = [ + Direct("Av-NG_pp", nodes[1], nodes[4], Linear()) + Direct("Av-coal_pp", nodes[1], nodes[5], Linear()) + Direct("Av-CO2_stor", nodes[1], nodes[6], Linear()) + Direct("Av-demand", nodes[1], nodes[7], Linear()) + Direct("NG_src-av", nodes[2], nodes[1], Linear()) + Direct("Coal_src-av", nodes[3], nodes[1], Linear()) + Direct("NG_pp-av", nodes[4], nodes[1], Linear()) + Direct("Coal_pp-av", nodes[5], nodes[1], Linear()) + Direct("CO2_stor-av", nodes[6], nodes[1], Linear()) + ] + + # Input data structure + case = Case(T, products, [nodes, links], [[get_nodes, get_links]]) + return case, model +end + +""" + generate_example_ss_investment(lifemode = RollingLife; discount_rate = 0.05) + +Generate the data for an example consisting of an electricity source and sink. +The electricity source has initially no capacity. Hence, investments are required. +""" +function generate_example_ss_investment(lifemode = RollingLife; discount_rate = 0.05) + @info "Generate case data - Simple sink-source example" + + # Define the different resources and their emission intensity in tCO2/MWh + Power = ResourceCarrier("Power", 0.0) + CO2 = ResourceEmit("CO2", 1.0) + products = [Power, CO2] + + # Variables for the individual entries of the time structure + op_duration = 2 # Each operational period has a duration of 2 + op_number = 4 # There are in total 4 operational periods + operational_periods = SimpleTimes(op_number, op_duration) + + # Each operational period should correspond to a duration of 2 h while a duration if 1 + # of a strategic period should correspond to a year. + # This implies, that a strategic period is 8760 times longer than an operational period, + # resulting in the values below as "/year". + op_per_strat = 8760 + + sp_duration = 5 # The duration of a investment period is given as 5 years + + # Creation of the time structure and global data + T = TwoLevel(4, sp_duration, operational_periods; op_per_strat) + + # Create the global data + model = InvestmentModel( + Dict(CO2 => FixedProfile(10)), # Emission cap for CO₂ in t/year + Dict(CO2 => FixedProfile(0)), # Emission price for CO₂ in EUR/t + CO2, # CO₂ instance + discount_rate, # Discount rate in absolute value + ) + + # The lifetime of the technology is 15 years, requiring reinvestment in the + # 5th investment period + lifetime = FixedProfile(15) + + # Create the investment data for the source node + investment_data_source = SingleInvData( + FixedProfile(300 * 1e3), # capex [€/MW] + FixedProfile(50), # max installed capacity [MW] + ContinuousInvestment(FixedProfile(0), FixedProfile(30)), + # Line above: Investment mode with the following arguments: + # 1. argument: min added capactity per sp [MW] + # 2. argument: max added capactity per sp [MW] + lifemode(lifetime), # Lifetime mode + ) + + # Create the individual test nodes, corresponding to a system with an electricity + # demand/sink and source + nodes = [ + RefSource( + "electricity source", # Node id + FixedProfile(0), # Capacity in MW + FixedProfile(10), # Variable OPEX in EUR/MW + FixedProfile(5), # Fixed OPEX in EUR/MW/year + Dict(Power => 1), # Output from the Node, in this case, Power + [investment_data_source], # Additional data used for adding the investment data + ), + RefSink( + "electricity demand", # Node id + OperationalProfile([20, 30, 40, 30]), # Demand in MW + Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), + # Line above: Surplus and deficit penalty for the node in EUR/MWh + Dict(Power => 1), # Energy demand and corresponding ratio + ), + ] + + # Connect all nodes with the availability node for the overall energy/mass balance + links = [ + Direct("source-demand", nodes[1], nodes[2], Linear()), + ] + + # Input data structure + case = Case(T, products, [nodes, links], [[get_nodes, get_links]]) + return case, model +end + +""" + generate_example_data() + +Generate the data for an example consisting of a simple electricity network with a +non-dispatchable power source, a regulated hydro power plant, as well as a demand. +It illustrates how the hydro power plant can balance the intermittent renewable power +generation. +""" +function generate_example_hp() + @info "Generate case data - Simple `HydroStor` example" + + # Define the different resources and their emission intensity in tCO2/MWh + # CO2 has to be defined, even if not used, as it is required for the `EnergyModel` type + CO2 = ResourceEmit("CO2", 1.0) + Power = ResourceCarrier("Power", 1.0) + products = [CO2, Power] + + # Variables for the individual entries of the time structure + op_duration = 2 # Each operational period has a duration of 2 + op_number = 4 # There are in total 4 operational periods + operational_periods = SimpleTimes(op_number, op_duration) + + # The number of operational periods times the duration of the operational periods. + # This implies, that a strategic period is 8 times longer than an operational period, + # resulting in the values below as "/8h". + op_per_strat = op_duration * op_number + + # Create the time structure and global data + T = TwoLevel(2, 1, operational_periods; op_per_strat) + model = OperationalModel( + Dict(CO2 => FixedProfile(10)), # Emission cap for CO2 in t/8h + Dict(CO2 => FixedProfile(0)), # Emission price for CO2 in EUR/t + CO2, # CO2 instance + ) + # Create the Availability/bus node for the system + av = GenAvailability(1, products) + + # Create a non-dispatchable renewable energy source + wind = NonDisRES( + "wind", # Node ID + FixedProfile(2), # Capacity in MW + OperationalProfile([0.9, 0.4, 0.1, 0.8]), # Profile + FixedProfile(5), # Variable OPEX in EUR/MW + FixedProfile(10), # Fixed OPEX in EUR/8h + Dict(Power => 1), # Output from the Node, in this gase, Power + ) + + # Create a regulated hydro power plant without storage capacity + hydro = HydroStor{CyclicStrategic}( + "hydropower", # Node ID + StorCapOpexFixed(FixedProfile(90), FixedProfile(3)), + # Line above for the storage level: + # Argument 1: Storage capacity in MWh + # Argument 2: Fixed OPEX in EUR/8h + StorCapOpexVar(FixedProfile(2.0), FixedProfile(8)), + # Line above for the discharge rate: + # Argument 1: Rate capacity in MW + # Argument 2: Variable OPEX in EUR/MWh + FixedProfile(10), # Initial storage level in MWh + FixedProfile(1), # Inflow to the Node in MW + FixedProfile(0.0), # Minimum storage level as fraction + Power, # Stored resource + Dict(Power => 0.9), # Input to the power plant, irrelevant in this case + Dict(Power => 1), # Output from the Node, in this gase, Power + ) + + # Create a power demand node + sink = RefSink( + "electricity demand", # Node id + FixedProfile(2), # Demand in MW + Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), + # Line above: Surplus and deficit penalty for the node in EUR/MWh + Dict(Power => 1), # Energy demand and corresponding ratio + ) + + # Create the array of ndoes + nodes = [av, wind, hydro, sink] + + # Connect all nodes with the availability node for the overall energy balance + links = [ + Direct("wind-av", wind, av), + Direct("hy-av", hydro, av), + Direct("av-hy", av, hydro), + Direct("av-demand", av, sink), + ] + + # Create the case dictionary + case = Case(T, products, [nodes, links]) + + return case, model +end + +""" + generate_example_snd() + +Generate the data for an example consisting of a simple electricity network with a +non-dispatchable power source, a standard source, as well as a demand. +It illustrates how the non-dispatchable power source requires a balancing power source. +""" +function generate_example_snd() + @info "Generate case data - Simple `NonDisRES` example" + + # Define the different resources and their emission intensity in tCO2/MWh + # CO2 has to be defined, even if not used, as it is required for the `EnergyModel` type + CO2 = ResourceEmit("CO2", 1.0) + Power = ResourceCarrier("Power", 0.0) + products = [Power, CO2] + + # Variables for the individual entries of the time structure + op_duration = 2 # Each operational period has a duration of 2 + op_number = 4 # There are in total 4 operational periods + operational_periods = SimpleTimes(op_number, op_duration) + + # The number of operational periods times the duration of the operational periods. + # This implies, that a strategic period is 8 times longer than an operational period, + # resulting in the values below as "/8h". + op_per_strat = op_duration * op_number + + # Creation of the time structure and global data + T = TwoLevel(2, 1, operational_periods; op_per_strat) + model = OperationalModel( + Dict(CO2 => FixedProfile(10)), # Emission cap for CO2 in t/8h + Dict(CO2 => FixedProfile(0)), # Emission price for CO2 in EUR/t + CO2, # CO2 instance + ) + + # Create the individual test nodes, corresponding to a system with an electricity + # demand/sink and source + source = RefSource( + "source", # Node ID + FixedProfile(2), # Capacity in MW + FixedProfile(30), # Variable OPEX in EUR/MW + FixedProfile(10), # Fixed OPEX in EUR/8h + Dict(Power => 1), # Output from the Node, in this gase, Power + ) + sink = RefSink( + "electricity demand", # Node id + FixedProfile(2), # Demand in MW + Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), + # Line above: Surplus and deficit penalty for the node in EUR/MWh + Dict(Power => 1), # Energy demand and corresponding ratio + ) + nodes = [source, sink] + + # Connect the two nodes with each other + links = [Direct("source-demand", nodes[1], nodes[2], Linear())] + + # Create the additonal non-dispatchable power source + wind = NonDisRES( + "wind", # Node ID + FixedProfile(4), # Capacity in MW + OperationalProfile([0.9, 0.4, 0.1, 0.8]), # Profile of the NonDisRES node + FixedProfile(10), # Variable OPEX in EUR/MW + FixedProfile(10), # Fixed OPEX in EUR/8h + Dict(Power => 1), # Output from the Node, in this gase, Power + ) + + # Update the case data with the non-dispatchable power source and link + push!(nodes, wind) + link = Direct("wind-demand", nodes[3], nodes[2], Linear()) + push!(links, link) + case = Case(T, products, [nodes, links]) + + return case, model +end diff --git a/ext/EMGExt/EMGExt.jl b/ext/EMGExt/EMGExt.jl index 15b6c83..04d7d93 100644 --- a/ext/EMGExt/EMGExt.jl +++ b/ext/EMGExt/EMGExt.jl @@ -10,21 +10,58 @@ using GLMakie using EnergyModelsGUI +using DataFrames + const TS = TimeStruct const EMG = EnergyModelsGeography const EMB = EnergyModelsBase const EMI = EnergyModelsInvestments const EMGUI = EnergyModelsGUI +""" + EMG.get_areas(system::SystemGeo) + +Returns the `Area`s of a `SystemGeo` `system`. +""" +EMG.get_areas(system::EMGUI.SystemGeo) = EMGUI.get_children(system) + +""" + EMG.get_transmissions(system::EMGUI.SystemGeo) + +Returns the `Transmission`s of a `SystemGeo` `system`. +""" +EMG.get_transmissions(system::EMGUI.SystemGeo) = EMGUI.get_connections(system) + +""" + get_modes(system::EMGUI.SystemGeo) + +Get all transmission modes of a `SystemGeo` `system`. +""" +function get_modes(system::EMGUI.SystemGeo) + transmission_modes = Vector{TransmissionMode}() + for t ∈ get_transmissions(system) + append!(transmission_modes, modes(t)) + end + return transmission_modes +end + ############################################################################################ ## From datastructures.jl """ - EMGUI.get_transmissions(system::System) + EMB.get_links(system::EMGUI.SystemGeo) -Returns the `Transmission`s of a `System` `system`. +Returns the `Links`s of a `SystemGeo` `system`. """ -EMGUI.get_transmissions(system::EMGUI.SystemGeo) = - filter(el -> isa(el, Vector{<:Transmission}), EMGUI.get_elements_vec(system))[1] +EMG.get_links(system::EMGUI.SystemGeo) = + filter(el -> isa(el, Vector{<:Link}), get_elements_vec(system))[1] + +""" + EMB.get_nodes(system::EMGUI.SystemGeo) + +Returns the `Node`s of a `SystemGeo` `system`. +""" +EMB.get_nodes(system::EMGUI.SystemGeo) = + filter(el -> isa(el, Vector{<:EMB.Node}), get_elements_vec(system))[1] """ EMGUI.SystemGeo(case::Case) @@ -32,18 +69,33 @@ EMGUI.get_transmissions(system::EMGUI.SystemGeo) = Initialize a `SystemGeo` from a `Case`. """ function EMGUI.SystemGeo(case::Case) - areas = EMG.get_areas(case) + areas = get_areas(case) ref_element = areas[1] return EMGUI.SystemGeo( - EMB.get_time_struct(case), - EMB.get_products(case), - EMB.get_elements_vec(case), + get_time_struct(case), + get_products(case), + get_elements_vec(case), areas, - EMG.get_transmissions(case), + get_transmissions(case), EMGUI.NothingElement(), ref_element, ) end + +""" + EMGUI.get_plotables(system::EMGUI.SystemGeo) + +Get all plotable elements of a `SystemGeo` `system`, which includes nodes, links, areas, and modes. +""" +function EMGUI.get_plotables(system::EMGUI.SystemGeo) + return vcat( + get_nodes(system), + get_links(system), + get_areas(system), + get_modes(system), + ) +end + ############################################################################################ ## From structure_utils.jl """ @@ -60,11 +112,11 @@ end ############################################################################################ ## From utils.jl """ - EMGUI.get_max_installed(m::EMG.TransmissionMode, t::Vector{<:TS.TimeStructure}) + EMGUI.get_max_installed(m::TransmissionMode, t::Vector{<:TS.TimeStructure}) Get the maximum capacity installable by an investemnt. """ -function EMGUI.get_max_installed(m::EMG.TransmissionMode, t::Vector{<:TS.TimeStructure}) +function EMGUI.get_max_installed(m::TransmissionMode, t::Vector{<:TS.TimeStructure}) if EMI.has_investment(m) time_profile = EMI.max_installed(EMI.investment_data(m, :cap)) return maximum(time_profile[t]) @@ -72,7 +124,7 @@ function EMGUI.get_max_installed(m::EMG.TransmissionMode, t::Vector{<:TS.TimeStr return 0.0 end end -function EMGUI.get_max_installed(trans::EMG.Transmission, t::Vector{<:TS.TimeStructure}) +function EMGUI.get_max_installed(trans::Transmission, t::Vector{<:TS.TimeStructure}) return maximum([EMGUI.get_max_installed(m, t) for m ∈ modes(trans)]) end @@ -87,10 +139,10 @@ function EMGUI.sub_system(system::EMGUI.SystemGeo, element::AbstractElement) area_an::EMB.Node = availability_node(element) # Allocate redundantly large vector (for efficiency) to collect all links and nodes - links::Vector{Link} = EMGUI.get_links(system) + links::Vector{Link} = get_links(system) area_links::Vector{Link} = Vector{Link}(undef, length(links)) area_nodes::Vector{EMB.Node} = Vector{EMB.Node}( - undef, length(EMGUI.get_nodes(system)), + undef, length(get_nodes(system)), ) area_nodes[1] = area_an @@ -102,9 +154,9 @@ function EMGUI.sub_system(system::EMGUI.SystemGeo, element::AbstractElement) resize!(area_links, indices[1] - 1) resize!(area_nodes, indices[2] - 1) return EMGUI.System( - EMGUI.get_time_struct(system), - EMGUI.get_products(system), - EMGUI.get_elements_vec(system), + get_time_struct(system), + get_products(system), + get_elements_vec(system), area_nodes, area_links, element, @@ -148,11 +200,18 @@ Get the mapping between modes and transmissions for a `SystemGeo`. """ function EMGUI.get_mode_to_transmission_map(system::EMGUI.SystemGeo) mode_to_transmission = Dict() - for t ∈ EMGUI.get_transmissions(system) + for t ∈ get_transmissions(system) for m ∈ modes(t) mode_to_transmission[m] = t end end return mode_to_transmission end + +""" + _type_to_header(::Type{<:TransmissionMode}) + +Map types to header symbols for saving results. +""" +EMGUI._type_to_header(::Type{<:TransmissionMode}) = :element end diff --git a/src/EnergyModelsGUI.jl b/src/EnergyModelsGUI.jl index b01771b..c566321 100644 --- a/src/EnergyModelsGUI.jl +++ b/src/EnergyModelsGUI.jl @@ -16,7 +16,6 @@ using YAML using FileIO using TimeStruct using EnergyModelsBase -import EnergyModelsBase: get_elements_vec, get_nodes # Use Colors to visualize using the colors in the colors.yml file using Colors @@ -38,7 +37,11 @@ using SparseVariables using EnergyModelsInvestments -# Needed for plottig geographical map +# Import CSV and DataFrames to enable reading JuMP results from CSV files +using CSV +using DataFrames + +# Needed for plotting geographical map using GeoMakie, GeoJSON # Needed to download the .json file for geographical coastlines diff --git a/src/datastructures.jl b/src/datastructures.jl index cf18611..ec3d344 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -49,11 +49,11 @@ function System(case::Case) first_av = getfirst(x -> isa(x, Availability), get_nodes(case)) ref_element = isnothing(first_av) ? NothingElement() : first_av return System( - EMB.get_time_struct(case), - EMB.get_products(case), - EMB.get_elements_vec(case), - EMB.get_nodes(case), - EMB.get_links(case), + get_time_struct(case), + get_products(case), + get_elements_vec(case), + get_nodes(case), + get_links(case), NothingElement(), ref_element, ) @@ -234,6 +234,7 @@ The main type for the realization of the GUI. # Fields - **`fig::Figure`** is the figure handle to the main figure (window). +- **`screen::GLMakie.Screen`** is the screen handle for displaying the figure. - **`axes::Dict{Symbol,Axis}`** is a collection of axes: :topo (axis for visualizing the topology), :results (axis for plotting results), and :info (axis for displaying information). @@ -247,11 +248,12 @@ The main type for the realization of the GUI. to the gui.axes[:results] object. - **`root_design::EnergySystemDesign`** is the data structure used for the root topology. - **`design::EnergySystemDesign`** is the data structure used for visualizing the topology. -- **`model::Model`** contains the optimization results. +- **`model::Union{Model, Dict}`** contains the optimization results. - **`vars::Dict{Symbol,Any}`** is a dictionary of miscellaneous variables and parameters. """ mutable struct GUI fig::Figure + screen::GLMakie.Screen axes::Dict{Symbol,Makie.Block} legends::Dict{Symbol,Union{Makie.Legend,Nothing}} buttons::Dict{Symbol,Makie.Button} @@ -259,7 +261,7 @@ mutable struct GUI toggles::Dict{Symbol,Makie.Toggle} root_design::EnergySystemDesign design::EnergySystemDesign - model::Model + model::Union{Model,Dict} vars::Dict{Symbol,Any} end @@ -309,25 +311,25 @@ function Base.iterate(design::EnergySystemDesign, state = (nothing, nothing)) end """ - get_time_struct(system::AbstractSystem) + EMB.get_time_struct(system::AbstractSystem) Returns the time structure of the AbstractSystem `system`. """ -get_time_struct(system::AbstractSystem) = system.T +EMB.get_time_struct(system::AbstractSystem) = system.T """ - get_products(system::AbstractSystem) + EMB.get_products(system::AbstractSystem) Returns the vector of products of the AbstractSystem `system`. """ -get_products(system::AbstractSystem) = system.products +EMB.get_products(system::AbstractSystem) = system.products """ - get_elements_vec(system::AbstractSystem) + EMB.get_elements_vec(system::AbstractSystem) Returns the vector of element-vectors of the AbstractSystem `system`. """ -get_elements_vec(system::AbstractSystem) = system.elements +EMB.get_elements_vec(system::AbstractSystem) = system.elements """ get_children(system::AbstractSystem) @@ -358,34 +360,32 @@ Returns the `ref_element` field of a `AbstractSystem` `system`. get_ref_element(system::AbstractSystem) = system.ref_element """ - get_links(system::AbstractSystem) + EMB.get_links(system::AbstractSystem) -Returns the `ref_element` field of a `AbstractSystem` `system`. +Returns the links of a `AbstractSystem` `system`. """ -get_links(system::AbstractSystem) = - filter(el -> isa(el, Vector{<:Link}), get_elements_vec(system))[1] +EMB.get_links(system::AbstractSystem) = get_connections(system) """ - get_nodes(system::AbstractSystem) + EMB.get_nodes(system::AbstractSystem) -Returns the `ref_element` field of a `AbstractSystem` `system`. +Returns the nodes of a `AbstractSystem` `system`. """ -get_nodes(system::AbstractSystem) = - filter(el -> isa(el, Vector{<:EMB.Node}), get_elements_vec(system))[1] +EMB.get_nodes(system::AbstractSystem) = get_children(system) """ - get_transmissions(system::System) + get_element(system::System) -Returns the `Connections`s of a `System` `system`. +Returns the `element` assosiated of a `System` `system`. """ -get_transmissions(system::System) = get_connections(system) +get_element(system::System) = get_parent(system) """ - get_element(system::System) + get_plotables(system::System) -Returns the `element` assosiated of a `System` `system`. +Returns the `Node`s and `Link`s of a `System` `system`. """ -get_element(system::System) = get_parent(system) +get_plotables(system::System) = vcat(get_nodes(system), get_links(system)) """ get_system(design::EnergySystemDesign) @@ -472,11 +472,11 @@ Returns the `plots` field of a `EnergySystemDesign` `design`. get_plots(design::EnergySystemDesign) = design.plots """ - get_time_struct(design::EnergySystemDesign) + EMB.get_time_struct(design::EnergySystemDesign) Returns the time structure of the EnergySystemDesign `design`. """ -get_time_struct(design::EnergySystemDesign) = get_time_struct(get_system(design)) +EMB.get_time_struct(design::EnergySystemDesign) = EMB.get_time_struct(get_system(design)) """ get_ref_element(design::EnergySystemDesign) @@ -561,6 +561,20 @@ Returns the `fig` field of a `GUI` `gui`. """ get_fig(gui::GUI) = gui.fig +""" + get_screen(gui::GUI) + +Returns the `screen` field of a `GUI` `gui`. +""" +get_screen(gui::GUI) = gui.screen + +""" + close(gui::GUI) + +Closes the GUI `gui`. +""" +close(gui::GUI) = GLMakie.close(get_screen(gui)) + """ get_axes(gui::GUI) @@ -704,11 +718,11 @@ Get the selection color for the `gui`. get_selection_color(gui::GUI) = get_var(gui, :selection_color) """ - get_time_struct(gui::GUI) + EMB.get_time_struct(gui::GUI) Returns the time structure of the GUI `gui`. """ -get_time_struct(gui::GUI) = get_time_struct(get_design(gui)) +EMB.get_time_struct(gui::GUI) = EMB.get_time_struct(get_design(gui)) """ get_parent(gui::GUI) diff --git a/src/setup_GUI.jl b/src/setup_GUI.jl index 3668b27..8648d21 100644 --- a/src/setup_GUI.jl +++ b/src/setup_GUI.jl @@ -1,18 +1,21 @@ """ GUI(case::Case; kwargs...) + GUI(case::Dict; kwargs...) -Initialize the EnergyModelsGUI window and visualize the topology of a system `case` \ +Initialize the `EnergyModelsGUI` window and visualize the topology of a system `case` (and optionally visualize its results in a JuMP object model). The input argument can either -be a `Case` object from the EnergyModelsBase package or a dictionary containing system-related -data stored as key-value pairs. This dictionary is corresponding to the the old EnergyModelsX -`case` dictionary. +be a [`Case`](@extref EnergyModelsBase.Case) instance from the `EnergyModelsBase` package or +a dictionary containing system-related data stored as key-value pairs. The latter corresponds +to the old EnergyModelsX `case` dictionary. # Keyword arguments: - **`design_path::String=""`** is a file path or identifier related to the design. - **`id_to_color_map::Dict=Dict()`** is a dict that maps `Resource`s `id` to colors. - **`id_to_icon_map::Dict=Dict()`** is a dict that maps `Node/Area` `id` to .png files for icons. -- **`model::JuMP.Model=JuMP.Model()`** is the solved JuMP model with results for the `case`. +- **`model::Union{JuMP.Model, String}`** is the solved JuMP model with results for the `case`, + but can also be the path (`String`) to the directory containing the JuMP results written as + CSV-files. - **`hide_topo_ax_decorations::Bool=true`** is a visibility toggle of ticks, ticklabels and grids for the topology axis. - **`expand_all::Bool=false`** is the default option for toggling visibility of all nodes @@ -35,13 +38,17 @@ data stored as key-value pairs. This dictionary is corresponding to the the old - **`scale_tot_capex::Bool=false`** divides total CAPEX quantities with the duration of the strategic period. - **`colormap::Vector=Makie.wong_colors()`** is the colormap used for plotting results. - **`tol::Float64=1e-12`** the tolerance for numbers close to machine epsilon precision. + +!!! warning "Reading model results from CSV-files" + Reading model results from a directory (*i.e.*, `model::String` implying that the results + are stored in CSV-files) does not support more than three indices for variables. """ function GUI( case::Case; design_path::String = "", id_to_color_map::Dict = Dict(), id_to_icon_map::Dict = Dict(), - model::JuMP.Model = JuMP.Model(), + model::Union{JuMP.Model,String} = JuMP.Model(), hide_topo_ax_decorations::Bool = true, expand_all::Bool = false, periods_labels::Vector = [], @@ -165,9 +172,19 @@ function GUI( # Construct the makie figure and its objects fig, buttons, menus, toggles, axes, legends = create_makie_objects(vars, root_design) + # Construct screen object + manifest = Pkg.Operations.Context().env.manifest + version = manifest[findfirst(v -> v.name == "EnergyModelsGUI", manifest)].version + fig_title = "EnergyModelsGUI v$version" + if !isempty(case_name) + fig_title *= ": $case_name" + end + screen = GLMakie.Screen(title = fig_title) + ## Create the main structure for the EnergyModelsGUI gui::GUI = GUI( - fig, axes, legends, buttons, menus, toggles, root_design, design, model, vars, + fig, screen, axes, legends, buttons, menus, toggles, root_design, design, + transfer_model(model, get_system(root_design)), vars, ) # Create complete Dict of descriptive names @@ -195,13 +212,7 @@ function GUI( DataInspector(fig; range = 3, indicator_linewidth = 0) # display the figure - manifest = Pkg.Operations.Context().env.manifest - version = manifest[findfirst(v -> v.name == "EnergyModelsGUI", manifest)].version - fig_title = "EnergyModelsGUI v$version" - if !isempty(case_name) - fig_title *= ": $case_name" - end - display(GLMakie.Screen(title = fig_title), fig) + display(screen, fig) return gui end diff --git a/src/setup_topology.jl b/src/setup_topology.jl index 68a3c39..3714000 100644 --- a/src/setup_topology.jl +++ b/src/setup_topology.jl @@ -115,7 +115,7 @@ function EnergySystemDesign( end # Add `Transmission`s and `Link`s to `connections` as a `Connection` - elements = get_transmissions(system) + elements = get_connections(system) if !isnothing(elements) for element ∈ elements # Find the EnergySystemDesign corresponding to element.from.node diff --git a/src/utils_GUI/GUI_utils.jl b/src/utils_GUI/GUI_utils.jl index c8d84ca..a9b4219 100644 --- a/src/utils_GUI/GUI_utils.jl +++ b/src/utils_GUI/GUI_utils.jl @@ -211,6 +211,19 @@ function get_mode_to_transmission_map(::System) return Dict() end +""" + results_available(model::Model) + results_available(model::String) + +Check if the model has a feasible solution. +""" +function results_available(model::Model) + return termination_status(model) == MOI.OPTIMAL +end +function results_available(model::Dict) + return !isempty(model) && model[:metadata][:termination_status] == string(MOI.OPTIMAL) +end + """ initialize_available_data!(gui) @@ -226,7 +239,7 @@ function initialize_available_data!(gui) ) # Find appearances of node/area/link/transmission in the model - if termination_status(model) == MOI.OPTIMAL # Plot results if available + if results_available(model) T = get_time_struct(gui) mode_to_transmission = get_mode_to_transmission_map(system) for sym ∈ get_JuMP_names(gui) @@ -272,7 +285,7 @@ function initialize_available_data!(gui) opex_key = Symbol(opex_field[1]) description = opex_field[2] if haskey(model, opex_key) - opex = vec(sum(Array(value.(model[opex_key])), dims = 1)) + opex = vec(get_total_sum_time(model[opex_key], collect(𝒯ᴵⁿᵛ))) tot_opex_unscaled .+= opex if scale_tot_opex opex .*= sp_dur @@ -300,7 +313,7 @@ function initialize_available_data!(gui) capex_key = Symbol(capex_field[1]) description = capex_field[2] if haskey(model, capex_key) - capex = vec(sum(Array(value.(model[capex_key])), dims = 1)) + capex = vec(get_total_sum_time(model[capex_key], collect(𝒯ᴵⁿᵛ))) tot_capex_unscaled .+= capex if scale_tot_capex capex ./= sp_dur @@ -366,7 +379,7 @@ function initialize_available_data!(gui) 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(objective_value(model)))\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" @@ -413,8 +426,15 @@ end """ extract_data_selection(var::SparseVars, selection::Vector, i_T::Int64, periods::Vector) + extract_data_selection(var::Jump.Containers.DenseAxisArray, selection::Vector, i_T::Int64, ::Vector) + extract_data_selection(var::DataFrame, selection::Vector, ::Int64, ::Vector) Extract data from `var` having its time dimension at index `i_T` for all time periods in `periods`. + +!!! warning "Reading model results from CSV-files" + This function does not support more than three indices for `var::DataFrame` (*i.e.*, + when model results are read from CSV-files). This implies it is incompatible with + potential extensions that introduce more than three indices for variables. """ function extract_data_selection( var::SparseVars, selection::Vector, i_T::Int64, periods::Vector, @@ -429,6 +449,23 @@ function extract_data_selection( ) return var[vcat(selection[1:(i_T-1)], :, selection[i_T:end])...] end +function extract_data_selection( + var::DataFrame, selection::Vector, ::Int64, ::Vector, +) + res_idx = findfirst(x -> isa(x, Resource), selection) + element_idx = findfirst(x -> !isa(x, Resource), selection) + if !isnothing(res_idx) && !isnothing(element_idx) + res = selection[res_idx] + element = selection[element_idx] + return var[(var.:res .== [res]) .& (var.:element .== [element]), :] + elseif !isnothing(res_idx) + res = selection[res_idx] + return var[var.:res .== [res], :] + elseif !isnothing(element_idx) + element = selection[element_idx] + return var[var.:element .== [element], :] + end +end """ get_JuMP_names(gui::GUI) @@ -438,10 +475,56 @@ Get all names registered in the model as a vector except the names to be ignored function get_JuMP_names(gui::GUI) model = get_model(gui) ignore_names = Symbol.(get_var(gui, :descriptive_names)[:ignore]) - names = collect(keys(object_dictionary(model))) + names = collect(keys(get_JuMP_dict(model))) return [name for name ∈ names if !(name ∈ ignore_names)] end +""" + get_obj_value(model::Model) + get_obj_value(model::Dict) + +Get the objective value of the model. +""" +function get_obj_value(model::Model) + return objective_value(model) +end +function get_obj_value(model::Dict) + return model[:metadata][:objective_value] +end + +""" + get_JuMP_dict(model::Model) + get_JuMP_dict(model::Dict) + +Get the dictionary of the model results. If the model is a JuMP.Model, it returns the object +dictionary. +""" +get_JuMP_dict(model::Dict) = Dict(k => v for (k, v) ∈ model if k != :metadata) +get_JuMP_dict(model::JuMP.Model) = object_dictionary(model) + +""" + get_values(vals::SparseVars) + get_values(vals::JuMP.Containers.DenseAxisArray) + get_values(vals::DataFrame) + get_values(vals::JuMP.Containers.SparseAxisArray, ts::Vector) + get_values(vals::SparseVariables.IndexedVarArray, ts::Vector) + get_values(vals::JuMP.Containers.DenseAxisArray, ts::Vector) + get_values(vals::DataFrame, ts::Vector) + +Get the values of the variables in `vals`. If a vector of time periods `ts` is provided, it +returns the values for the times in `ts`. +""" +get_values(vals::SparseVars) = isempty(vals) ? [] : collect(Iterators.flatten(value.(vals))) +get_values(vals::SparseVariables.IndexedVarArray) = collect(value.(values(vals.data))) +get_values(vals::JuMP.Containers.DenseAxisArray) = Array(value.(vals)) +get_values(vals::DataFrame) = vals[!, :val] +get_values(vals::JuMP.Containers.SparseAxisArray, ts::Vector) = [value(vals[t]) for t ∈ ts] +get_values(vals::SparseVariables.IndexedVarArray, ts::Vector) = + isempty(vals) ? [] : value.(vals[ts]) +get_values(vals::JuMP.Containers.DenseAxisArray, ts::Vector) = Array(value.(vals[ts])) +get_values(vals::DataFrame, ts::Vector) = vals[in.(vals.t, Ref(ts)), :val] +get_values(vals::TimeProfile, ts::Vector) = vals[ts] + """ get_investment_times(gui::GUI, max_inst::Float64) @@ -491,7 +574,9 @@ function get_investment_times(gui::GUI, max_inst::Float64) end """ - get_combinations(var, i_T::Int) + get_combinations(var::SparseVars, i_T::Int) + get_combinations(var::JuMP.Containers.DenseAxisArray, i_T::Int) + get_combinations(var::DataFrame, ::Int) Get an iterator of combinations of unique indices excluding the time index located at index `i_T`. """ @@ -501,6 +586,13 @@ end function get_combinations(var::JuMP.Containers.DenseAxisArray, i_T::Int) return Iterators.product(axes(var)[vcat(1:(i_T-1), (i_T+1):end)]...) end +function get_combinations(var::DataFrame, ::Int) + # Exclude the time and value columns (assumed to be :t and :vals) + cols = names(var) + non_time_val_cols = filter(c -> c != "t" && c != "val", cols) + # Get unique tuples of the non-time/value columns + return unique(Tuple(row[c] for c ∈ non_time_val_cols) for row ∈ eachrow(var)) +end """ update_available_data_menu!(gui::GUI, element) @@ -591,3 +683,120 @@ function select_data!(gui::GUI, name::String) # Select data menu.i_selected = i_selected end + +""" + get_total_sum_time(data::JuMP.Containers.DenseAxisArray, ::Vector{<:TS.TimeStructure}) + get_total_sum_time(data::DataFrame, periods::Vector{<:TS.TimeStructure}) + +Get the total sum of the data for each time period in `data`. +""" +function get_total_sum_time( + data::JuMP.Containers.DenseAxisArray, + ::Vector{<:TS.TimeStructure}, +) + return sum(get_values(data), dims = 1) +end +function get_total_sum_time(data::DataFrame, periods::Vector{<:TS.TimeStructure}) + return [sum(data[data.:t .== [t], :val]) for t ∈ periods] +end + +""" + get_all_periods!(vec::Vector, ts::TwoLevel) + get_all_periods!(vec::Vector, ts::RepresentativePeriods) + get_all_periods!(vec::Vector, ts::OperationalScenarios) + get_all_periods!(vec::Vector, ts::Any) + +Get all TimeStructures in `ts` and append them to `vec`. +""" +function get_all_periods!(vec::Vector, ts::TwoLevel) + append!(vec, collect(ts)) + append!(vec, strategic_periods(ts)) + for t ∈ ts.operational + get_all_periods!(vec, t) + end +end +function get_all_periods!(vec::Vector, ts::RepresentativePeriods) + append!(vec, repr_periods(T)) + for t ∈ ts.rep_periods + get_all_periods!(vec, t) + end +end +function get_all_periods!(vec::Vector, ts::OperationalScenarios) + append!(vec, opscenarios(T)) + for t ∈ ts.scenarios + get_all_periods!(vec, t) + end +end +function get_all_periods!(::Vector, ::Any) + return nothing +end + +""" + get_repr_dict(vec::AbstractVector{T}) where T + +Get a dictionary with the string representation of each element in `vec` as keys. +""" +function get_repr_dict(vec::AbstractVector{T}) where {T} + return Dict{String,T}(repr(x) => x for x ∈ vec) +end + +""" + convert_array(v::AbstractArray, dict::Dict) + +Apply the transformation of the `dict` to the array `v`. +""" +function convert_array(v::AbstractArray, dict::Dict) + return map(x -> dict[x], v) +end + +""" + transfer_model(model::Model, system::AbstractSystem) + transfer_model(model::String, system::AbstractSystem) + +Convert the model to a DataFrame if it is provided as a path to a directory. +""" +transfer_model(model::Model, ::AbstractSystem) = model +function transfer_model(model::String, system::AbstractSystem) + data = Dict{Symbol,Any}() + if isdir(model) + files = filter(f -> endswith(f, ".csv"), readdir(model, join = true)) + 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 + varname = Symbol(basename(file)[1:(end-4)]) + + df = CSV.read(file, DataFrame) + + # Rename columns :sp, :op, or :osc to :t if present. Note that the type of the + # time structure is available through the type of the column. + for col ∈ (:sp, :rp, :osc) + if string(col) ∈ names(df) + rename!(df, col => :t) + end + end + + col_names = names(df) + all_periods = [] + get_all_periods!(all_periods, 𝒯) + df[!, :t] = convert_array(df[!, :t], get_repr_dict(unique(all_periods))) + if "res" ∈ col_names + df[!, :res] = convert_array( + df[!, :res], + get_repr_dict(get_products(system)), + ) + end + if "element" ∈ col_names + df[!, :element] = convert_array( + df[!, :element], + get_repr_dict(get_plotables(system)), + ) + end + + data[varname] = df + end + else + @warn "The model must be a directory containing the results. No results loaded." + end + return data +end diff --git a/src/utils_GUI/event_functions.jl b/src/utils_GUI/event_functions.jl index 37ab543..9caaa78 100644 --- a/src/utils_GUI/event_functions.jl +++ b/src/utils_GUI/event_functions.jl @@ -384,48 +384,7 @@ function define_event_functions(gui::GUI) # Export button: Export ax_results to file (format given by export_type_menu.selection[]) on(get_button(gui, :export).clicks; priority = 10) do _ if get_menu(gui, :export_type).selection[] == "REPL" - axes_str::String = get_menu(gui, :axes).selection[] - if axes_str == "Plots" - time_axis = time_menu.selection[] - vis_plots = get_visible_data(gui, time_axis) - if !isempty(vis_plots) # Check if any plots exist - t = vis_plots[1][:t] - 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), - ) - header[1][1] = "t" - header[2][1] = "(" * string(nameof(eltype(t))) * ")" - for (j, vis_plot) ∈ enumerate(vis_plots) - data[:, j+1] = vis_plot[:y] - header[1][j+1] = vis_plots[j][:name] - header[2][j+1] = join( - [string(x) for x ∈ vis_plots[j][:selection]], ", " -) - end - println("\n") # done in order to avoid the prompt shifting the topspline of the table - pretty_table(data; header = header) - end - elseif axes_str == "All" - model = get_model(gui) - for sym ∈ get_JuMP_names(gui) - container = model[sym] - if isempty(container) - continue - end - if typeof(container) <: JuMP.Containers.DenseAxisArray - axis_types = nameof.([eltype(a) for a ∈ JuMP.axes(model[sym])]) - elseif typeof(container) <: SparseVars - axis_types = collect(nameof.(typeof.(first(keys(container.data))))) - end - header = vcat(axis_types, [:value]) - pretty_table( - JuMP.Containers.rowtable(value, container; header = header), - ) - end - end + export_to_repl(gui) else export_to_file(gui) end diff --git a/src/utils_GUI/results_axis_utils.jl b/src/utils_GUI/results_axis_utils.jl index 30bf80b..be7d280 100644 --- a/src/utils_GUI/results_axis_utils.jl +++ b/src/utils_GUI/results_axis_utils.jl @@ -168,11 +168,11 @@ end """ get_data( - model::JuMP.Model, + model::Union{JuMP.Model, Dict}, selection::Dict{Symbol, Any}, T::TS.TimeStructure, sp::Int64, - rp::Int64 + rp::Int64, sc::Int64, ) @@ -180,7 +180,8 @@ Get the values from the JuMP `model`, or the input data, at `selection` for all restricted to strategic period `sp`, representative period `rp`, and scenario `sc`. """ function get_data( - model::JuMP.Model, selection::Dict, T::TS.TimeStructure, sp::Int64, rp::Int64, sc::Int64, + model::Union{JuMP.Model,Dict}, selection::Dict, T::TS.TimeStructure, sp::Int64, + rp::Int64, sc::Int64, ) field_data = selection[:field_data] if selection[:is_jump_data] @@ -190,15 +191,7 @@ function get_data( type = nested_eltype(field_data) end periods, time_axis = get_periods(T, type, sp, rp, sc) - if selection[:is_jump_data] - if isa(field_data, JuMP.Containers.SparseAxisArray) - y_values = [value(field_data[t]) for t ∈ periods] - else - y_values = Array(value.(field_data[periods])) - end - else - y_values = field_data[periods] - end + y_values = get_values(field_data, periods) return periods, y_values, time_axis end @@ -270,6 +263,7 @@ function get_time_axis( JuMP.Containers.DenseAxisArray, JuMP.Containers.SparseAxisArray, SparseVariables.IndexedVarArray, + DataFrames.DataFrame, }, ) types::Vector{Type} = collect(get_jump_axis_types(data)) @@ -285,14 +279,20 @@ end """ get_jump_axis_types(data::JuMP.Containers.DenseAxisArray) + get_jump_axis_types(data::SparseVars) + get_jump_axis_types(data::DataFrame) Get the types for each axis in the data. """ function get_jump_axis_types(data::JuMP.Containers.DenseAxisArray) - return eltype.(axes(data)) + return collect(eltype.(axes(data))) end function get_jump_axis_types(data::SparseVars) - return typeof.(first(keys(data.data))) + return collect(Base.unwrap_unionall(typeof(data)).parameters[3].parameters) +end +function get_jump_axis_types(data::DataFrame) + n = ncol(data) + return [eltype(col) for (i, col) ∈ enumerate(eachcol(data)) if i < n] end """ @@ -634,8 +634,8 @@ function update_limits!(ax::Axis) # Do the following for data with machine epsilon precision noice around zero that causes # the warning "Warning: No strict ticks found" and the the bug related to issue #4266 in Makie if abs(ywidth) < 1e-13 - ywidth = 2.0 - yorigin = min_y - 1.0 + ywidth = 2 * max(1.0, max_y) + yorigin = 0.0 else yorigin = min_y - ywidth * 0.04 ywidth += 2 * ywidth * 0.04 diff --git a/src/utils_gen/export_utils.jl b/src/utils_gen/export_utils.jl index fec1a93..5883777 100644 --- a/src/utils_gen/export_utils.jl +++ b/src/utils_gen/export_utils.jl @@ -157,7 +157,8 @@ end """ export_to_file(gui::GUI) -Export results based on the state of `gui`. +Export results based on the state of `gui` to a file located within the folder specified +through the `path_to_results` keyword of [`GUI`](@ref). """ function export_to_file(gui::GUI) path = get_var(gui, :path_to_results) @@ -171,75 +172,86 @@ function export_to_file(gui::GUI) end axes_str::String = get_menu(gui, :axes).selection[] file_ending = get_menu(gui, :export_type).selection[] + filename = joinpath(path, axes_str * "." * file_ending) if file_ending ∈ ["svg"] CairoMakie.activate!() # Set CairoMakie as backend for proper export quality cairo_makie_activated = true else cairo_makie_activated = false end - if axes_str == "All" - filename = joinpath(path, axes_str * "." * file_ending) - if file_ending ∈ ["bmp", "tiff", "tif", "jpg", "jpeg"] - @warn "Exporting the entire figure to an $file_ending file is not implemented" - flag = 1 - elseif file_ending == "xlsx" - flag = export_xlsx(gui, filename) - elseif file_ending == "lp" || file_ending == "mps" - try - write_to_file(get_model(gui), filename) - flag = 0 - catch - flag = 2 - end - else - try - save(filename, get_fig(gui)) - flag = 0 - catch - flag = 2 - end + if file_ending == "lp" || file_ending == "mps" + if isa(get_model(gui), DataFrame) + @info "Writing model to a $file_ending file is not supported when reading results from .csv-files" + return 1 + elseif isempty(get_model(gui)) + @info "No model to be exported" + return 2 + end + try + write_to_file(get_model(gui), filename) + flag = 0 + catch + flag = 2 end else - if axes_str == "Plots" - ax_sym = :results - elseif axes_str == "Topo" - ax_sym = :topo + valid_combinations = Dict( + "All" => ["jpg", "jpeg", "svg", "xlsx", "png"], + "Plots" => ["bmp", "tif", "tiff", "jpg", "jpeg", "svg", "xlsx", "png"], + "Topo" => ["svg"], + ) + if !(file_ending ∈ valid_combinations[axes_str]) + @info "Exporting $axes_str to a $file_ending file is not supported" + return 1 end - filename = joinpath(path, "$ax_sym.$file_ending") - if file_ending == "svg" + if axes_str == "All" + if file_ending == "xlsx" + flag = export_xlsx(gui, filename) + else + try + save(filename, get_fig(gui)) + flag = 0 + catch + flag = 2 + end + end + else if axes_str == "Plots" - flag = export_svg( - get_ax(gui, ax_sym), filename; legend = get_results_legend(gui), - ) + ax_sym = :results elseif axes_str == "Topo" - flag = export_svg( - get_ax(gui, ax_sym), filename; legend = get_topo_legend(gui), - ) - else - flag = export_svg(get_ax(gui, ax_sym), filename) + ax_sym = :topo end - elseif file_ending == "xlsx" - if ax_sym == :topo - @warn "Exporting the topology to an xlsx file is not implemented" - flag = 1 + if file_ending == "svg" + if axes_str == "Plots" + flag = export_svg( + get_ax(gui, ax_sym), filename; legend = get_results_legend(gui), + ) + elseif axes_str == "Topo" + flag = export_svg( + get_ax(gui, ax_sym), filename; legend = get_topo_legend(gui), + ) + else + flag = export_svg(get_ax(gui, ax_sym), filename) + end + elseif file_ending == "xlsx" + if axes_str == "Plots" + time_axis = get_menu(gui, :time).selection[] + plots = get_visible_data(gui, time_axis) + flag = export_xlsx(plots, filename, ax_sym) + end + elseif file_ending == "lp" || file_ending == "mps" + try + write_to_file(get_model(gui), filename) + flag = 0 + catch + flag = 2 + end else - time_axis = get_menu(gui, :time).selection[] - plots = get_visible_data(gui, time_axis) - flag = export_xlsx(plots, filename, ax_sym) - end - elseif file_ending == "lp" || file_ending == "mps" - try - write_to_file(get_model(gui), filename) - flag = 0 - catch - flag = 2 - end - else - try - save(filename, colorbuffer(get_ax(gui, ax_sym))) - flag = 0 - catch - flag = 2 + try + save(filename, colorbuffer(get_ax(gui, ax_sym))) + flag = 0 + catch + flag = 2 + end end end end @@ -249,7 +261,56 @@ function export_to_file(gui::GUI) if flag == 0 @info "Exported results to $filename" elseif flag == 2 - @info "An error occured, no file exported" + @info "An error occurred, no file exported" end return flag end + +""" + export_to_repl(gui::GUI) + +Export results based on the state of `gui` to the REPL. +""" +function export_to_repl(gui::GUI) + axes_str::String = get_menu(gui, :axes).selection[] + if axes_str == "Plots" + time_axis = get_menu(gui, :time).selection[] + vis_plots = get_visible_data(gui, time_axis) + if !isempty(vis_plots) # Check if any plots exist + t = vis_plots[1][:t] + 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), + ) + header[1][1] = "t" + header[2][1] = "(" * string(nameof(eltype(t))) * ")" + for (j, vis_plot) ∈ enumerate(vis_plots) + data[:, j+1] = vis_plot[:y] + header[1][j+1] = vis_plots[j][:name] + header[2][j+1] = join([string(x) for x ∈ vis_plots[j][:selection]], ", ") + end + println("\n") # done in order to avoid the prompt shifting the topspline of the table + pretty_table(data; header = header) + end + else + model = get_model(gui) + for sym ∈ get_JuMP_names(gui) + container = model[sym] + if isempty(container) + continue + end + if typeof(container) <: JuMP.Containers.DenseAxisArray + axis_types = nameof.([eltype(a) for a ∈ JuMP.axes(model[sym])]) + elseif typeof(container) <: SparseVars + axis_types = collect(nameof.(typeof.(first(keys(container.data))))) + end + header = vcat(axis_types, [:value]) + pretty_table( + JuMP.Containers.rowtable(value, container; header = header), + ) + end + end + return 0 +end diff --git a/src/utils_gen/utils.jl b/src/utils_gen/utils.jl index 26e81a0..b7ef8fb 100644 --- a/src/utils_gen/utils.jl +++ b/src/utils_gen/utils.jl @@ -207,3 +207,66 @@ function scroll_ylim(ax::Makie.AbstractAxis, val::Float64) end ylims!(ax, ylim[1], ylim[2]) end + +""" + _type_to_header(::Type{<:TS.AbstractStrategicPeriod}) + _type_to_header(::Type{<:TS.AbstractRepresentativePeriod}) + _type_to_header(::Type{<:TS.AbstractOperationalScenario}) + _type_to_header(::Type{<:TS.TimePeriod}) + _type_to_header(::Type{<:TS.TimeStructure}) + _type_to_header(::Type{<:Resource}) + _type_to_header(::Type{<:AbstractElement}) + +Map types to header symbols for saving results. +""" +_type_to_header(::Type{<:TS.AbstractStrategicPeriod}) = :sp +_type_to_header(::Type{<:TS.AbstractRepresentativePeriod}) = :rp +_type_to_header(::Type{<:TS.AbstractOperationalScenario}) = :osc +_type_to_header(::Type{<:TS.TimePeriod}) = :t +_type_to_header(::Type{<:TS.TimeStructure}) = :t +_type_to_header(::Type{<:Resource}) = :res +_type_to_header(::Type{<:AbstractElement}) = :element + +""" + save_results(model::Model; directory=joinpath(pwd(),"csv_files")) + +Saves the model results of all variables as CSV files and metadata as a yml-file. +If no directory is specified, it will create, if necessary, a new directory "csv_files" in +the current working directory and save the files in said directory. +""" +function save_results(model::Model; directory = joinpath(pwd(), "csv_files")) + if !ispath(directory) + mkpath(directory) + end + + # Write each variable to a CSV file + Threads.@threads for v ∈ collect(keys(object_dictionary(model))) + if !isempty(model[v]) + datatypes::Vector = get_jump_axis_types(model[v]) + headers::Vector{Symbol} = _type_to_header.(datatypes) + push!(headers, :val) + fn = joinpath(directory, string(v) * ".csv") + CSV.write( + fn, + JuMP.Containers.rowtable(value, model[v]); + header = headers, + ) + end + end + + # Write metadata to a YAML file + metadata = Dict( + "name" => JuMP.name(model), + "solver" => JuMP._try_solver_name(model), + "objective_sense" => objective_sense(model), + "num_variables" => num_variables(model), + "objective_value" => objective_value(model), + "termination_status" => termination_status(model), + "date" => string(Dates.now()), + "EnergyModelsGUI version" => installed()["EnergyModelsGUI"], + ) + metadata_file = joinpath(directory, "metadata.yaml") + open(metadata_file, "w") do io + YAML.write(io, metadata) + end +end diff --git a/test/Project.toml b/test/Project.toml index 1f41ea6..3c5d54d 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -7,6 +7,7 @@ EnergyModelsRenewableProducers = "b007c34f-ba52-4995-ba37-fffe79fbde35" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/examples/case7.jl b/test/case7.jl similarity index 98% rename from examples/case7.jl rename to test/case7.jl index 42626bd..c69732c 100644 --- a/examples/case7.jl +++ b/test/case7.jl @@ -58,7 +58,7 @@ function read_data() nodes = EMB.Node[] links = Link[] for (i, a_id) ∈ enumerate(area_ids) - n, l = get_sub_system_data(a_id, products, T) + n, l = get_sub_system_data_case7(a_id, products, T) append!(nodes, n) append!(links, l) @@ -86,15 +86,13 @@ function read_data() # Create a power line between the busbars Power_line = RefStatic( - "El power line_11126", Power, trans_cap, loss, opex_var, opex_fix, direction, [], + "El power line_11126", Power, trans_cap, loss, opex_var, opex_fix, direction, ) Power_line2 = RefStatic( "El power line_11139", Power, trans_cap2, loss, opex_var, opex_fix, direction, - [], ) Power_line3 = RefStatic( "El power line_11139", Power, trans_cap3, loss, opex_var, opex_fix, direction, - [], ) # Construct the transmissions vector @@ -114,12 +112,12 @@ function read_data() end # Subsystem test data for geography package -function get_sub_system_data(a_id, products, T) +function get_sub_system_data_case7(a_id, products, T) Power = products[1] Heat = products[2] WarmWater = products[3] El_change_factor = [1, 2, 4] # Alter the electricity change factor. This scales the demand for electricity by a factor 2 for 2030--2039 and a factor 4 for 2040--2049 - inputFolder = joinpath(@__DIR__, "Inputfiles") + inputFolder = joinpath(@__DIR__, "inputfiles") if a_id == "Area 1" # Load the demand profile from file El_1_demand_file = readlines(inputFolder * raw"/el load.dat") diff --git a/examples/design/case7/Area 1.yml b/test/design/case7/Area 1.yml similarity index 100% rename from examples/design/case7/Area 1.yml rename to test/design/case7/Area 1.yml diff --git a/examples/design/case7/Area 2.yml b/test/design/case7/Area 2.yml similarity index 100% rename from examples/design/case7/Area 2.yml rename to test/design/case7/Area 2.yml diff --git a/examples/design/case7/Area 3.yml b/test/design/case7/Area 3.yml similarity index 100% rename from examples/design/case7/Area 3.yml rename to test/design/case7/Area 3.yml diff --git a/examples/design/case7/Area 4.yml b/test/design/case7/Area 4.yml similarity index 100% rename from examples/design/case7/Area 4.yml rename to test/design/case7/Area 4.yml diff --git a/examples/design/case7/top_level.yml b/test/design/case7/top_level.yml similarity index 100% rename from examples/design/case7/top_level.yml rename to test/design/case7/top_level.yml diff --git a/test/example_test.jl b/test/example_test.jl index ed57b5f..f4c5415 100644 --- a/test/example_test.jl +++ b/test/example_test.jl @@ -7,12 +7,12 @@ using PrettyTables using TimeStruct """ - generate_example_data(; use_rp::Bool=true, use_sc::Bool=true) + generate_example_test(; use_rp::Bool=true, use_sc::Bool=true) Generate the data based on the EMB_sink_source.jl example with posibilities for different time structures. """ -function generate_example_data(; use_rp::Bool = true, use_sc::Bool = true) +function generate_example_test(; use_rp::Bool = true, use_sc::Bool = true) @info "Generate case data - Sink-source example with non-tensorial time structure" # Define the different resources and their emission intensity in tCO2/MWh @@ -144,12 +144,12 @@ function generate_example_data(; use_rp::Bool = true, use_sc::Bool = true) end """ - run_case(; use_rp::Bool=true, use_sc::Bool=true) + run_test_case(; use_rp::Bool=true, use_sc::Bool=true) Generate the case and model data, run the model and show results in GUI """ -function run_case(; use_rp::Bool = true, use_sc::Bool = true) - case, model = generate_example_data(; use_rp, use_sc) +function run_test_case(; use_rp::Bool = true, use_sc::Bool = true) + case, model = generate_example_test(; use_rp, use_sc) optimizer = optimizer_with_attributes(HiGHS.Optimizer, MOI.Silent() => true) m = run_model(case, model, optimizer) gui = GUI( diff --git a/examples/Inputfiles/0.dat b/test/inputfiles/0.dat similarity index 100% rename from examples/Inputfiles/0.dat rename to test/inputfiles/0.dat diff --git a/examples/Inputfiles/0_1.dat b/test/inputfiles/0_1.dat similarity index 100% rename from examples/Inputfiles/0_1.dat rename to test/inputfiles/0_1.dat diff --git a/examples/Inputfiles/1.dat b/test/inputfiles/1.dat similarity index 100% rename from examples/Inputfiles/1.dat rename to test/inputfiles/1.dat diff --git a/examples/Inputfiles/10.dat b/test/inputfiles/10.dat similarity index 100% rename from examples/Inputfiles/10.dat rename to test/inputfiles/10.dat diff --git a/examples/Inputfiles/100.dat b/test/inputfiles/100.dat similarity index 100% rename from examples/Inputfiles/100.dat rename to test/inputfiles/100.dat diff --git a/examples/Inputfiles/charging.dat b/test/inputfiles/charging.dat similarity index 100% rename from examples/Inputfiles/charging.dat rename to test/inputfiles/charging.dat diff --git a/examples/Inputfiles/el cost.dat b/test/inputfiles/el cost.dat similarity index 100% rename from examples/Inputfiles/el cost.dat rename to test/inputfiles/el cost.dat diff --git a/examples/Inputfiles/el load.dat b/test/inputfiles/el load.dat similarity index 100% rename from examples/Inputfiles/el load.dat rename to test/inputfiles/el load.dat diff --git a/examples/Inputfiles/solar.dat b/test/inputfiles/solar.dat similarity index 100% rename from examples/Inputfiles/solar.dat rename to test/inputfiles/solar.dat diff --git a/test/runtests.jl b/test/runtests.jl index bc778dd..1fa0808 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,7 @@ using EnergyModelsGUI using Test using YAML +using Logging const TEST_ATOL = 1e-6 const EMGUI = EnergyModelsGUI @@ -8,9 +9,22 @@ const EMGUI = EnergyModelsGUI # Include function that can loop through all components and plot its data include("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") + # Add utilities needed for examples include("../examples/utils.jl") +logger_org = global_logger() +logger_new = ConsoleLogger(stderr, Logging.Warn) +global_logger(logger_new) + @testset "EnergyModelsGUI" verbose = true begin redirect_stdio(stdout = devnull) do # Run all Aqua tests @@ -22,10 +36,11 @@ include("../examples/utils.jl") # The following tests simply checks if the main examples can be run without errors include("test_examples.jl") - # Test miscellaneous functionalities - include("test_functionality.jl") + # The following tests results input and output functionality (saving and loading results) + include("test_results_IO.jl") # Test specific GUI functionalities related to interactivity include("test_interactivity.jl") end end +global_logger(logger_org) diff --git a/test/test_examples.jl b/test/test_examples.jl index c428ffb..37d2cf7 100644 --- a/test/test_examples.jl +++ b/test/test_examples.jl @@ -1,16 +1,18 @@ @testset "Run examples" verbose = true begin - exdir = joinpath(@__DIR__, "..", "examples") files = first(walkdir(exdir))[3] for file ∈ files if splitext(file)[2] == ".jl" && splitext(file)[1] != "utils" && - !(file == "case7.jl") # Skip case7 as this is tested in test_interactivity.jl + splitext(file)[1] != "generate_examples" @testset "Example $file" begin @info "Run example $file" - include(joinpath(exdir, file)) + gui = include(joinpath(exdir, file)) - @test termination_status(m) == MOI.OPTIMAL + @test termination_status(EMGUI.get_model(gui)) == MOI.OPTIMAL + + EMGUI.close(gui) end end end + Pkg.activate(env) end diff --git a/test/test_functionality.jl b/test/test_functionality.jl deleted file mode 100644 index 8113d95..0000000 --- a/test/test_functionality.jl +++ /dev/null @@ -1,49 +0,0 @@ -# Test specific miscellaneous functionalities -@testset "Test functionality" verbose = true begin - # Test GUI functionalities with the case7 example - include("../examples/case7.jl") - - # Test print functionalities of GUI structures to the REPL - @testset "Test Base.show() functions" begin - println(gui) - design = EMGUI.get_design(gui) - println(design) - components = EMGUI.get_components(design) - connections = EMGUI.get_connections(design) - println(components[1]) - println(connections[1]) - @test true - end - - @testset "Test customizing descriptive names" begin - path_to_descriptive_names = joinpath(@__DIR__, "..", "src", "descriptive_names.yml") - str1 = "" - str2 = "" - str3 = "" - str4 = "" - 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), - ), - :variables => Dict( # variables from the JuMP model - :stor_discharge_use => str5, - :trans_cap_rem => str6, - ), - ) - gui = GUI( - case; - path_to_descriptive_names = path_to_descriptive_names, - descriptive_names_dict = descriptive_names_dict, - ) - descriptive_names = EMGUI.get_var(gui, :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 - @test descriptive_names[:variables][:stor_discharge_use] == str5 - @test descriptive_names[:variables][:trans_cap_rem] == str6 - end -end diff --git a/test/test_interactivity.jl b/test/test_interactivity.jl index 53b64c7..8f78090 100644 --- a/test/test_interactivity.jl +++ b/test/test_interactivity.jl @@ -25,17 +25,76 @@ import EnergyModelsGUI: get_plotted_data, select_data! -# Test specific GUI functionalities -@testset "Test interactivity" verbose = true begin - # Test GUI interactivity with the case7 example - include("../examples/case7.jl") +# Load case 7 for testing +case, model, m, gui = run_case() + +root_design = get_root_design(gui) +components = get_components(root_design) +connections = get_connections(root_design) + +time_menu = get_menu(gui, :time) +available_data_menu = get_menu(gui, :available_data) +period_menu = get_menu(gui, :period) +representative_period_menu = get_menu(gui, :representative_period) +export_type_menu = get_menu(gui, :export_type) +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) + end - case, model, m, gui = run_case() + @testset "Test customizing descriptive names" begin + path_to_descriptive_names = joinpath(pkgdir(EMGUI), "src", "descriptive_names.yml") + str1 = "" + str2 = "" + str3 = "" + str4 = "" + 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), + ), + :variables => Dict( # variables from the JuMP model + :stor_discharge_use => str5, + :trans_cap_rem => str6, + ), + ) + gui2 = GUI( + case; + path_to_descriptive_names = path_to_descriptive_names, + 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 + @test descriptive_names[:variables][:stor_discharge_use] == str5 + @test descriptive_names[:variables][:trans_cap_rem] == str6 + EMGUI.close(gui2) + 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) + m = EMGUI.get_model(gui) for (i, t) ∈ enumerate(strategic_periods(T)) if haskey(m, :cap_capex) tot_capex_sp = sum(value.(m[:cap_capex][:, t])) / duration_strat(t) @@ -49,18 +108,6 @@ import EnergyModelsGUI: @test op_cost[i] ≈ tot_opex_sp end end - root_design = get_root_design(gui) - components = get_components(root_design) - connections = get_connections(root_design) - - time_menu = get_menu(gui, :time) - available_data_menu = get_menu(gui, :available_data) - period_menu = get_menu(gui, :period) - representative_period_menu = get_menu(gui, :representative_period) - export_type_menu = get_menu(gui, :export_type) - axes_menu = get_menu(gui, :axes) - - pin_plot_button = get_button(gui, :pin_plot) # Test color toggling @testset "Toggle colors" begin @@ -136,7 +183,7 @@ import EnergyModelsGUI: # Test the save button functionality @testset "get_button(gui,:save).clicks" begin - design_folder = joinpath(@__DIR__, "..", "examples", "design", "case7") + design_folder = joinpath(pkgdir(EMGUI), "examples", "design", "case7") root_design.file = joinpath(design_folder, "test_top_level.yml") for (i_area, area_design) ∈ enumerate(components) area_design.file = joinpath(design_folder, "test_Area $i_area.yml") @@ -299,26 +346,46 @@ import EnergyModelsGUI: @test ax_results.finallimits[].origin[1] == origin_x end + global_logger(logger_org) + valid_combinations = Dict( + "All" => ["jpg", "jpeg", "svg", "xlsx", "png", "lp", "mps"], + "Plots" => + ["bmp", "tif", "tiff", "jpg", "jpeg", "svg", "xlsx", "png", "lp", "mps"], + "Topo" => ["svg", "lp", "mps"], + ) @testset "get_button(gui,:export).clicks" begin - # Loop through all combinations of export options path = get_var(gui, :path_to_results) + + # Loop through all combinations of export options for i_axes ∈ range(1, length(axes_menu.options[])) + println("i_axes = $i_axes") axes_menu.i_selected = i_axes for i_type ∈ range(1, length(export_type_menu.options[])) + println(" i_type = $i_type") export_type_menu.i_selected = i_type - notify(get_button(gui, :export).clicks) + axes_str = axes_menu.selection[] + file_ending = export_type_menu.selection[] + filename = joinpath(path, axes_str * "." * file_ending) + if file_ending ∈ valid_combinations[axes_str] + msg = "Exported results to $filename" + elseif file_ending == "REPL" + notify(get_button(gui, :export).clicks) + continue + else + msg = "Exporting $(axes_str) to a $file_ending file is not supported" + end + @test_logs (:info, msg) EMGUI.export_to_file(gui) end end - for file_ending ∈ ["svg", "xlsx", "png", "lp", "mps"] - @test isfile(joinpath(path, "All." * file_ending)) - end - for file_ending ∈ ["bmp", "tif", "tiff", "jpg", "jpeg", "svg", "xlsx", "png"] - @test isfile(joinpath(path, "results." * file_ending)) - end - for file_ending ∈ ["svg"] - @test isfile(joinpath(path, "topo." * file_ending)) + + # Test if all valid files were created + for (axes_str, file_endings) ∈ valid_combinations + for file_ending ∈ file_endings + @test isfile(joinpath(path, "$axes_str.$file_ending")) + end end end + global_logger(logger_new) @testset "get_button(gui,:remove_plot).clicks" begin time_axis = time_menu.selection[] @@ -356,13 +423,17 @@ import EnergyModelsGUI: id_to_icon_map = Dict("Battery" => "Battery icon") # Use a non-existing icon id_to_icon_map = set_icons(id_to_icon_map) products = get_products(case) + nodes = get_nodes(case) + links = get_links(case) + areas = get_areas(case) + transmissions = get_transmissions(case) test_sink = RefSink( "Test multiple sink products", FixedProfile(0), Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e5)), Dict(products[1] => 1, products[2] => 1), ) - push!(get_nodes(case), test_sink) + push!(nodes, test_sink) test_source = RefSource( "Test multiple source products", FixedProfile(0), @@ -370,115 +441,126 @@ import EnergyModelsGUI: FixedProfile(0), Dict(products[1] => 1, products[2] => 1), ) - av = get_nodes(case)[1] - push!(get_nodes(case), test_source) - push!(get_links(case), Direct("Link to test source", test_source, av, Linear())) - push!(get_links(case), Direct("Link to test sink", av, test_sink, Linear())) - gui = GUI(case; id_to_icon_map = id_to_icon_map, scenarios_labels = ["Scenario 1"]) - components = get_components(get_root_design(gui)) + av = nodes[1] + push!(nodes, test_source) + push!(links, Direct("Link to test source", test_source, av, Linear())) + push!(links, Direct("Link to test sink", av, test_sink, Linear())) + case2 = Case( + get_time_struct(case), + products, + [nodes, links, areas, transmissions], + [[get_nodes, get_links], [get_areas, get_transmissions]], + ) + gui2 = GUI(case2; id_to_icon_map, scenarios_labels = ["Scenario 1"]) + components = get_components(get_root_design(gui2)) @test isempty(get_components(components[4])[3].id_to_icon_map["Battery"]) + EMGUI.close(gui2) end - # Test GUI interactivity with the example_test for different time structures - include("example_test.jl") - ## Run a case with no representative periods nor scenarios - case, model, m, gui = run_case(; use_rp = false, use_sc = false) - available_data_menu = get_menu(gui, :available_data) @testset "Test SP(OP)" begin + _, _, _, gui3 = run_test_case(; use_rp = false, use_sc = false) + available_data_menu = get_menu(gui3, :available_data) # Test plotting over operational periods - select_data!(gui, "emissions_total") - @test get_ax(gui, :results).scene.plots[1][1][][24][2] ≈ 0.655 atol = 1e-5 + select_data!(gui3, "emissions_total") + @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 0.655 atol = 1e-5 # Test plotting over strategic periods - select_data!(gui, "emissions_strategic") - @test get_ax(gui, :results).scene.plots[2][1][][3][2] ≈ 20799.525 atol = 1e-5 + select_data!(gui3, "emissions_strategic") + @test get_ax(gui3, :results).scene.plots[2][1][][3][2] ≈ 20799.525 atol = 1e-5 + EMGUI.close(gui3) end ## Run a case with scenarios but no representative periods - case, model, m, gui = run_case(; use_rp = false, use_sc = true) - available_data_menu = get_menu(gui, :available_data) @testset "Test SP(SC(OP))" begin + case, model, m, gui3 = run_test_case(; use_rp = false, use_sc = true) + available_data_menu = get_menu(gui3, :available_data) + # Test plotting over operational periods - select_data!(gui, "emissions_total") - get_menu(gui, :scenario).i_selected = 4 - @test get_ax(gui, :results).scene.plots[1][1][][24][2] ≈ 0.131 atol = 1e-5 + select_data!(gui3, "emissions_total") + get_menu(gui3, :scenario).i_selected = 4 + @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 0.131 atol = 1e-5 # Test plotting over strategic periods - select_data!(gui, "emissions_strategic") - @test get_ax(gui, :results).scene.plots[2][1][][3][2] ≈ 12937.30455 atol = 1e-5 + select_data!(gui3, "emissions_strategic") + @test get_ax(gui3, :results).scene.plots[2][1][][3][2] ≈ 12937.30455 atol = 1e-5 # Test plotting over scenarios - sink = get_components(get_root_design(gui))[2] - pick_component!(gui, sink; pick_topo_component = true) - update!(gui) - select_data!(gui, "penalty.deficit") - @test get_ax(gui, :results).scene.plots[3][1][][4][2] ≈ 200000 atol = 1e-5 + sink = get_components(get_root_design(gui3))[2] + pick_component!(gui3, sink; pick_topo_component = true) + update!(gui3) + select_data!(gui3, "penalty.deficit") + @test get_ax(gui3, :results).scene.plots[3][1][][4][2] ≈ 200000 atol = 1e-5 + EMGUI.close(gui3) end ## Run a case with representative periods but no scenarios - case, model, m, gui = run_case(; use_rp = true, use_sc = false) - available_data_menu = get_menu(gui, :available_data) @testset "Test SP(RP(OP))" begin + _, _, _, gui3 = run_test_case(; use_rp = true, use_sc = false) + available_data_menu = get_menu(gui3, :available_data) + # Test plotting over operational periods - select_data!(gui, "emissions_total") - get_menu(gui, :representative_period).i_selected = 2 - @test get_ax(gui, :results).scene.plots[1][1][][24][2] ≈ 1.048 atol = 1e-5 + select_data!(gui3, "emissions_total") + get_menu(gui3, :representative_period).i_selected = 2 + @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 1.048 atol = 1e-5 # Test plotting over strategic periods - select_data!(gui, "emissions_strategic") - @test get_ax(gui, :results).scene.plots[2][1][][2][2] ≈ 20601.06 atol = 1e-5 + select_data!(gui3, "emissions_strategic") + @test get_ax(gui3, :results).scene.plots[2][1][][2][2] ≈ 20601.06 atol = 1e-5 # Test plotting over representative periods - get_menu(gui, :period).i_selected = 3 - sink = get_components(get_root_design(gui))[2] - pick_component!(gui, sink; pick_topo_component = true) - update!(gui) - select_data!(gui, "penalty.deficit") - @test get_ax(gui, :results).scene.plots[3][1][][2][2] ≈ 2.0e6 atol = 1e-5 + get_menu(gui3, :period).i_selected = 3 + sink = get_components(get_root_design(gui3))[2] + pick_component!(gui3, sink; pick_topo_component = true) + update!(gui3) + select_data!(gui3, "penalty.deficit") + @test get_ax(gui3, :results).scene.plots[3][1][][2][2] ≈ 2.0e6 atol = 1e-5 + EMGUI.close(gui3) end ## Run a case with representative periods and scenarios - case, model, m, gui = run_case(; use_rp = true, use_sc = true) - available_data_menu = get_menu(gui, :available_data) - @testset "Test SP(RP(SC(OP)))" begin + _, _, _, gui3 = run_test_case(; use_rp = true, use_sc = true) + available_data_menu = get_menu(gui3, :available_data) + # Test plotting over operational periods - select_data!(gui, "emissions_total") + select_data!(gui3, "emissions_total") # Test updating menu with non-tensorial timestructure - get_menu(gui, :period).i_selected = 3 - @test get_ax(gui, :results).scene.plots[1][1][][24][2] ≈ 1.965 atol = 1e-5 - get_menu(gui, :representative_period).i_selected = 2 - @test get_ax(gui, :results).scene.plots[1][1][][24][2] ≈ 1.2576 atol = 1e-5 - get_menu(gui, :representative_period).i_selected = 1 - @test get_ax(gui, :results).scene.plots[1][1][][24][2] ≈ 1.965 atol = 1e-5 - get_menu(gui, :scenario).i_selected = 4 - @test get_ax(gui, :results).scene.plots[1][1][][24][2] ≈ 0.393 atol = 1e-5 - get_menu(gui, :period).i_selected = 2 - @test get_ax(gui, :results).scene.plots[1][1][][24][2] ≈ 0.262 atol = 1e-5 - get_menu(gui, :representative_period).i_selected = 2 - @test get_ax(gui, :results).scene.plots[1][1][][24][2] ≈ 1.048 atol = 1e-5 - get_menu(gui, :scenario).i_selected = 3 - @test get_ax(gui, :results).scene.plots[1][1][][24][2] ≈ 0.4192 atol = 1e-5 - get_menu(gui, :period).i_selected = 1 - @test get_ax(gui, :results).scene.plots[1][1][][24][2] ≈ 0.1965 atol = 1e-5 - - get_menu(gui, :period).i_selected = 3 - @test get_ax(gui, :results).scene.plots[1][1][][24][2] ≈ 0.9825 atol = 1e-5 - get_menu(gui, :representative_period).i_selected = 2 - @test get_ax(gui, :results).scene.plots[1][1][][24][2] ≈ 3.7728 atol = 1e-5 + get_menu(gui3, :period).i_selected = 3 + @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 1.965 atol = 1e-5 + get_menu(gui3, :representative_period).i_selected = 2 + @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 1.2576 atol = 1e-5 + get_menu(gui3, :representative_period).i_selected = 1 + @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 1.965 atol = 1e-5 + get_menu(gui3, :scenario).i_selected = 4 + @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 0.393 atol = 1e-5 + get_menu(gui3, :period).i_selected = 2 + @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 0.262 atol = 1e-5 + get_menu(gui3, :representative_period).i_selected = 2 + @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 1.048 atol = 1e-5 + get_menu(gui3, :scenario).i_selected = 3 + @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 0.4192 atol = 1e-5 + get_menu(gui3, :period).i_selected = 1 + @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 0.1965 atol = 1e-5 + + get_menu(gui3, :period).i_selected = 3 + @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 0.9825 atol = 1e-5 + get_menu(gui3, :representative_period).i_selected = 2 + @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 3.7728 atol = 1e-5 # Test plotting over strategic periods - select_data!(gui, "emissions_strategic") - @test get_ax(gui, :results).scene.plots[2][1][][2][2] ≈ 7648.6446 atol = 1e-5 + select_data!(gui3, "emissions_strategic") + @test get_ax(gui3, :results).scene.plots[2][1][][2][2] ≈ 7648.6446 atol = 1e-5 # Test plotting over scenarios - sink = get_components(get_root_design(gui))[2] - pick_component!(gui, sink; pick_topo_component = true) - update!(gui) - select_data!(gui, "penalty.deficit") - @test get_ax(gui, :results).scene.plots[3][1][][2][2] ≈ 4.0e6 atol = 1e-5 + sink = get_components(get_root_design(gui3))[2] + pick_component!(gui3, sink; pick_topo_component = true) + update!(gui3) + select_data!(gui3, "penalty.deficit") + @test get_ax(gui3, :results).scene.plots[3][1][][2][2] ≈ 4.0e6 atol = 1e-5 + EMGUI.close(gui3) end end +EMGUI.close(gui) diff --git a/test/test_results_IO.jl b/test/test_results_IO.jl new file mode 100644 index 0000000..f5b59d9 --- /dev/null +++ b/test/test_results_IO.jl @@ -0,0 +1,46 @@ +tmpdir = mktempdir(@__DIR__; prefix = "exported_files_") + +function get_case(file) + if file == "EMB_network.jl" + case, model = generate_example_network() + elseif file == "EMI_geography.jl" + case, model = generate_example_data_geo() + elseif file == "EMR_hydro_power.jl" + case, model = generate_example_hp() + end + return case, model +end + +@testset "Test reading model results from files" verbose = true begin + for file ∈ ["EMB_network.jl", "EMI_geography.jl", "EMR_hydro_power.jl"] + directory = joinpath(tmpdir, splitext(file)[1]) + if !ispath(directory) + mkdir(directory) + end + + # Save results for later testing + case, model = get_case(file) + optimizer = optimizer_with_attributes(HiGHS.Optimizer, MOI.Silent() => true) + m = run_model(case, model, optimizer) + + # Save results + EMGUI.save_results(m; directory) + + # Generate the GUI from saved files + gui = GUI(case; model = directory) + + # Test the value of the objective function + obj_value = objective_value(m) + m_df = EMGUI.get_model(gui) + @test obj_value ≈ EMGUI.get_obj_value(m_df) atol = 1e-6 + + # Test that all variables have the expected values + for var ∈ EMGUI.get_JuMP_names(gui) + if !isempty(m[var]) + vals = vec(EMGUI.get_values(m[var])) + @test all(isapprox.(vals, EMGUI.get_values(m_df[var]), atol = 1e-6)) + end + end + EMGUI.close(gui) + end +end diff --git a/test/utils.jl b/test/utils.jl index f4f43be..211b261 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -3,9 +3,20 @@ Loop through all components of get_root_design(gui) and display all available data. """ -function run_through_all(gui::GUI; break_after_first::Bool = true) +function run_through_all( + gui::GUI; + break_after_first::Bool = true, + sleep_time::Float64 = 0.1, +) @info "Running through all components" - return run_through_all(gui, get_root_design(gui), break_after_first) + + run_through_all( + gui, + EMGUI.get_root_design(gui), + break_after_first, + 1, + sleep_time, + ) end """ @@ -13,37 +24,67 @@ end Loop through all components of design and display all available data. """ -function run_through_all(gui::GUI, design::EnergySystemDesign, break_after_first::Bool) - available_data_menu = get_menu(gui, :available_data) - for component ∈ get_components(design) - clear_selection(gui; clear_topo = true) - pick_component!(gui, component; pick_topo_component = true) +function run_through_all( + gui::GUI, + design::EnergySystemDesign, + break_after_first::Bool, + level::Int, + sleep_time::Float64, +) + indent_spacing = " " + available_data_menu = EMGUI.get_menu(gui, :available_data) + for component ∈ EMGUI.get_components(design) + @info indent_spacing^level * + "Running through component $(EMGUI.get_ref_element(component))" + EMGUI.clear_selection(gui; clear_topo = true) + EMGUI.pick_component!(gui, component; pick_topo_component = true) - if isempty(component.components) # no sub system found - update!(gui) - for i_selected ∈ 1:length(available_data_menu.options[]) - available_data_menu.i_selected = i_selected - if break_after_first - break - end - end - else - run_through_all(gui, component, break_after_first) + EMGUI.update!(gui) + run_through_menu( + available_data_menu, + indent_spacing, + level, + break_after_first, + sleep_time, + ) + if !isempty(component.components) # no sub system found + notify(EMGUI.get_button(gui, :open).clicks) + run_through_all(gui, component, break_after_first, level + 1, sleep_time) + notify(EMGUI.get_button(gui, :up).clicks) end if break_after_first break end end - for connection ∈ get_connections(design) - clear_selection(gui; clear_topo = true) - pick_component!(gui, connection; pick_topo_component = true) - update!(gui) - for i_selected ∈ 1:length(available_data_menu.options[]) - available_data_menu.i_selected = i_selected - if break_after_first - break - end + for connection ∈ EMGUI.get_connections(design) + @info indent_spacing^level * + "Running through connection $(EMGUI.get_element(connection))" + EMGUI.clear_selection(gui; clear_topo = true) + EMGUI.pick_component!(gui, connection; pick_topo_component = true) + EMGUI.update!(gui) + run_through_menu( + available_data_menu, + indent_spacing, + level, + break_after_first, + sleep_time, + ) + if break_after_first + break end + end +end + +function run_through_menu( + available_data_menu::EMGUI.Menu, + indent_spacing::String, + level::Int, + break_after_first::Bool, + sleep_time::Float64, +) + for i_selected ∈ 1:length(available_data_menu.options[]) + available_data_menu.i_selected = i_selected + sleep(sleep_time) if break_after_first break end