diff --git a/NEWS.md b/NEWS.md index 691c0d6..faa0148 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,13 @@ # Release notes +## Version 0.6.1 (2025-12-17) + +### Enhancements + +* Improved testing of `descriptive_names` and added more names from other packages. +* Improved documentation. + + ## Version 0.6.0 (2025-12-15) ### Bugfix diff --git a/docs/make.jl b/docs/make.jl index a8bb672..864c3fd 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -50,6 +50,8 @@ makedocs(; "Customize colors"=>"how-to/customize-colors.md", "Customize icons"=>"how-to/customize-icons.md", "Customize descriptive_names"=>"how-to/customize-descriptive_names.md", + "Improve performance"=>"how-to/improve-performance.md", + "Use custom backgroun map"=>"how-to/use-custom-background-map.md", ], "Library" => Any[ "Public"=>"library/public.md", diff --git a/docs/src/figures/NUTS2_illustration.png b/docs/src/figures/NUTS2_illustration.png new file mode 100644 index 0000000..500510a Binary files /dev/null and b/docs/src/figures/NUTS2_illustration.png differ diff --git a/docs/src/how-to/customize-descriptive_names.md b/docs/src/how-to/customize-descriptive_names.md index f615649..b7bea3f 100644 --- a/docs/src/how-to/customize-descriptive_names.md +++ b/docs/src/how-to/customize-descriptive_names.md @@ -2,9 +2,9 @@ `EnergyModelsGUI` provides a set of descriptive names for case input structures and assosiated JuMP variables. These can be found in `src/descriptive_names.yml`. These descriptions are extended/overwritten with EMX -packages having a `descriptive_names.yml` file in a `ext/EMGUIExt` folder of its repository. That is, -if you want to provide descriptive names for your EMX package, add a `.yml` file in this location, with the -same structure as `src/descriptive_names.yml`. +packages having a `descriptive_names.yml` file in a `ext/EMGUIExt` folder of its repository (having a module +name starting with `EnergyModel`). That is, if you want to provide descriptive names for your EMX package, +add a `.yml` file in this location, with the same structure as `src/descriptive_names.yml`. It can be convenient to provide a user defined file in addition. If you have this file located at `path_to_descriptive_names`, simply add it using @@ -18,13 +18,15 @@ If you instead (or in addition) want to provide descriptive names through a `Dic ```julia descriptive_names_dict = Dict( :structures => Dict( # Input parameter from the case Dict - :RefStatic => Dict( - :trans_cap => "New description for `trans_cap`", - :opex_fixed => "New description for `opex_fixed`", - ), - :RefDynamic => Dict( - :opex_var => "New description for `opex_var`", - :directions => "New description for `directions`", + :EnergyModelsGeography => Dict( # Input parameter from the case Dict + :RefStatic => Dict( + :trans_cap => "New description for `trans_cap`", + :opex_fixed => "New description for `opex_fixed`", + ), + :RefDynamic => Dict( + :opex_var => "New description for `opex_var`", + :directions => "New description for `directions`", + ), ), ), :variables => Dict( # variables from the JuMP model @@ -34,7 +36,6 @@ descriptive_names_dict = Dict( ) gui = GUI( case; - path_to_descriptive_names=path_to_descriptive_names, descriptive_names_dict=descriptive_names_dict, ) ``` diff --git a/docs/src/how-to/improve-performance.md b/docs/src/how-to/improve-performance.md new file mode 100644 index 0000000..9a7d8e5 --- /dev/null +++ b/docs/src/how-to/improve-performance.md @@ -0,0 +1,31 @@ +# [Improve performance](@id how_to-improve_performance) + +Due to the just-in-time (JIT) compilation of Julia, the instantiation of the `EnergyModelsGUI` window takes some time (but reopening the window will take less time). +This also includes interactive features in the GUI (creating the first plot is a lot slower than creating subsequent plots). + +That being said, it is possible to boost startup time by turning of redundant features. +One can for example plot sub-areas only on demand (which for large system significantly reduces setup of the `GUI`) through + +```julia +gui = GUI(case; pre_plot_sub_components = false) +``` + +If there is no need to use the background map when using `EnergyModelsGeography` one can skip the usage of `GeoMakie` (this will also increase performance) + +```julia +gui = GUI(case; use_geomakie = false) +``` + +If the user do not see any usage of the `DataInspector` tool provided by `Makie` (which enables information of plot objects upon hovering with the mouse) one could use the `enable_data_inspector` toogle to further improve performance + +```julia +gui = GUI(case; enable_data_inspector = false) +``` + +It is also possible to use a simplified plotting of the `Link`s/`Transmission`s using the `simplified_connection_plotting` which improves performance slightly. +This option is however more motivated by simplified visuals. +One can also use `simplify_all_levels` to have this simplified plotting on all levels (not just the top level). + +```julia +gui = GUI(case; simplified_connection_plotting = true, simplify_all_levels = true) +``` \ No newline at end of file diff --git a/docs/src/how-to/use-custom-background-map.md b/docs/src/how-to/use-custom-background-map.md new file mode 100644 index 0000000..55843c5 --- /dev/null +++ b/docs/src/how-to/use-custom-background-map.md @@ -0,0 +1,13 @@ +# [Use custom backgruond map](@id how_to-use_custom_backgruond_map) + +The GUI enables user defined background maps in `.geojson` format through the `GUI` constructor parameter `String::map_boundary_file`. One could for example download NUTS boundaries as GeoJSON from [datahub.io](https://datahub.io/core/geo-nuts-administrative-boundaries), save this file at a desired location and use this file path as `map_boundary_file`. + +Downloading [NUTS2](https://r2.datahub.io/clt98mkvt000ql70811z8xj6l/main/raw/data/NUTS_RG_60M_2024_4326_LEVL_2.geojson), one can with a EMX-case variable `case` + +```julia +gui = GUI(case; map_boundary_file = joinpath(@__DIR__, "NUTS_RG_60M_2024_4326_LEVL_2.geojson")) +``` + +get something like + +![NUTS2 illustration](../figures/NUTS2_illustration.png) diff --git a/src/descriptive_names.yml b/src/descriptive_names.yml index 584d52e..1af39ba 100644 --- a/src/descriptive_names.yml +++ b/src/descriptive_names.yml @@ -93,6 +93,30 @@ structures: AbstractBatteryLife: stack_cost: "Relative cost for replacing a battery stack" + EnergyModelsHeat: + ## link.jl + DHPipe: + cap: "Heat transport capacity of the pipe" + t_ground: "Ground temperature in °C" + + ## node.jl + HeatPump: + t_source: "Heat source temperature" + t_sink: "Heat sink temperature" + eff_carnot: "Carnot Efficiency" + + ## resource.jl + ResourceHeat: + t_supply: "Supply temperature in °C" + t_return: "Return temperature in °C" + + PinchData: + T_SH_hot: "Hot temperature of surplus heat source in °C" + T_SH_cold: "Cold temperature of surplus heat source in °C" + ΔT_min: "Minimum temperature difference between surplus source and DH network in °C" + T_DH_hot: "Hot temperature of district heating network in °C" + T_DH_cold: "Cold temperature of district heating network in °C" + EnergyModelsHydrogen: ## node.jl AbstractElectrolyzer: @@ -227,6 +251,23 @@ variables: # EnergyModelsCO2 stor_level_Δ_sp: "Increase in `stor_level` during a strategic period" + # EnergyModelsHeat + dh_pipe_loss: "Heat losses in DH pipes" + + # EnergyModelsLanguageInterface + buildings_surplus: "Surplus delivered to a sink, i.e., oversatisfied demand" + buildings_deficit: "Deficit in a sink, i.e., not satisfied demand" + solar_curtailment: "Curtailment of a solar energy source" + solar_cap_use: "Absolute capacity utilization" + + # EnergyModelsFlex + input_frac_strat: "Input resource fraction" + load_shift_from: "Load shift from" + load_shift_to: "Load shift to" + load_shifted: "Load shifted" + sink_surplus_p: "Penalties for surplus of resource" + sink_deficit_p: "Penalties for deficits of resource" + # Overview of total quantities and their components total: opex_fields: diff --git a/test/Project.toml b/test/Project.toml index 3c5d54d..220179a 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,7 +1,10 @@ [deps] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" EnergyModelsBase = "5d7e687e-f956-46f3-9045-6f5a5fd49f50" +EnergyModelsCO2 = "84b3f4d7-d799-4a5d-b06c-25c90dcfcad7" EnergyModelsGeography = "3f775d88-a4da-46c4-a2cc-aa9f16db6708" +EnergyModelsHeat = "ad1b8b27-e232-4da9-b498-bea9c19a30d7" +EnergyModelsHydrogen = "44855f8b-b147-4985-ac18-48817d03c548" EnergyModelsInvestments = "fca3f8eb-b383-437d-8e7b-aac76bb2004f" EnergyModelsRenewableProducers = "b007c34f-ba52-4995-ba37-fffe79fbde35" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" @@ -10,6 +13,7 @@ JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" +SCIP = "82193955-e24f-5292-bf16-6f2c5261a85f" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TimeStruct = "f9ed5ce0-9f41-4eaa-96da-f38ab8df101c" YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" diff --git a/test/design/example_all_structures/Coal area.yml b/test/design/example_all_structures/Coal area.yml new file mode 100644 index 0000000..07f0c9d --- /dev/null +++ b/test/design/example_all_structures/Coal area.yml @@ -0,0 +1,15 @@ +n_Reg_1-CO2_storage_geo: + y: 46.26174 + x: 5.0772 +n_Reg_1-Coal_source: + y: 46.26174 + x: 6.92496 +n_Reg_1-Coal_power_plant: + y: 45.17195 + x: 5.29398 +n_Reg_1-Electricity_demand: + y: 46.87906 + x: 6.00108 +n_Reg_1-Availability: + y: 45.87906 + x: 6.00108 diff --git a/test/design/example_all_structures/Natural gas area.yml b/test/design/example_all_structures/Natural gas area.yml new file mode 100644 index 0000000..2fabc16 --- /dev/null +++ b/test/design/example_all_structures/Natural gas area.yml @@ -0,0 +1,129 @@ +n_CO2_storage_big: + y: 45.76981 + x: 8.47282 +n_Reg_2-NG_source: + y: 44.80648 + x: 8.57883 +n_heat_demand: + y: 46.08825 + x: 10.21651 +"n_hydrogen storage": + y: 45.4147 + x: 8.44048 +n_CO2_storage_small: + y: 46.27751 + x: 9.99404 +n_ss_inv_electricity_demand: + y: 46.1934 + x: 10.11129 +"n_reserve down demand": + y: 46.42308 + x: 10.07954 +n_market_buy: + y: 45.46697 + x: 9.71667 +n_hydro_pump: + y: 44.64628 + x: 10.23688 +n_balancing_source: + y: 46.04724 + x: 8.55851 +n_hydro_generator_down: + y: 44.32898 + x: 10.13109 +n_water_source: + y: 45.57435 + x: 10.76056 +n_hydro_av: + y: 45.53768 + x: 9.8735 +n_heat_pump: + y: 46.3376 + x: 9.41381 +n_hydro_generator_up: + y: 45.1698 + x: 10.21044 +n_wind_simple: + y: 46.2075 + x: 8.45478 +n_ss_electricity_demand: + y: 45.7621 + x: 10.07954 +n_hub-h2_demand: + y: 45.86594 + x: 10.2105 +n_battery: + y: 46.42614 + x: 9.93808 +n_CCGT_retrofittable: + y: 45.60759 + x: 8.69229 +n_PEM: + y: 45.21135 + x: 8.43592 +n_ocean: + y: 43.95617 + x: 10.14167 +n_TES: + y: 46.49982 + x: 9.57916 +n_market_sale: + y: 45.4147 + x: 10.22099 +n_ss_electricity_source: + y: 45.94354 + x: 8.45164 +n_hub-electricity_source: + y: 45.00822 + x: 8.48167 +n_district_heat_source: + y: 46.50915 + x: 9.30152 +n_NG_source_for_CCGT: + y: 45.59557 + x: 8.45479 +n_hub: + y: 45.57898 + x: 9.28942 +n_ss_inv_electricity_source: + y: 46.33006 + x: 8.52708 +n_h2_demand_reformer: + y: 45.65866 + x: 10.20449 +n_Reg_2-Availability: + y: 45.3385 + x: 9.42555 +n_CCS_unit: + y: 45.73977 + x: 8.69229 +n_CO2_source_negative: + y: 46.44946 + x: 8.67481 +n_hydro_reservoir_up: + y: 45.17244 + x: 10.54897 +n_reformer: + y: 46.58908 + x: 9.83352 +n_Reg_2-ng+CCS_power_plant: + y: 44.70925 + x: 9.43732 +n_electricity_demand_big: + y: 45.94706 + x: 10.09024 +n_hydropower_simple: + y: 46.5186 + x: 8.94828 +n_hydro_gate: + y: 44.90462 + x: 10.5442 +n_Reg_2-Electricity_demand: + y: 44.89218 + x: 9.73437 +n_CO2_storage_reformer_sink: + y: 46.58908 + x: 10.08261 +n_hydro_reservoir_down: + y: 44.61719 + x: 10.50137 diff --git a/test/design/example_all_structures/top_level.yml b/test/design/example_all_structures/top_level.yml new file mode 100644 index 0000000..04fc597 --- /dev/null +++ b/test/design/example_all_structures/top_level.yml @@ -0,0 +1,6 @@ +"Coal area": + y: 45.87906 + x: 6.00108 +"Natural gas area": + y: 45.3385 + x: 9.42555 diff --git a/test/example_all_structures.jl b/test/example_all_structures.jl new file mode 100644 index 0000000..77bc5df --- /dev/null +++ b/test/example_all_structures.jl @@ -0,0 +1,733 @@ +using JuMP +using SCIP +using TimeStruct + +using EnergyModelsBase +using EnergyModelsGeography +using EnergyModelsHydrogen +using EnergyModelsCO2 +using EnergyModelsRenewableProducers +using EnergyModelsHeat +using EnergyModelsInvestments + +const EMB = EnergyModelsBase +const EMG = EnergyModelsGeography +const EMH2 = EnergyModelsHydrogen +const EMCO2 = EnergyModelsCO2 +const EMR = EnergyModelsRenewableProducers +const EMHeat = EnergyModelsHeat +const EMI = EnergyModelsInvestments +const TS = TimeStruct + +""" +All-in-one-case: combines all nodes from the main EMX packages. + +This comprehensive example includes: +- Two areas (EnergyModelsGeography) transferring `Power` and `CO2` with investment + transmission corridors: power line (ContinuousInvestment) + CO2 pipeline (SemiContinuousInvestment) +- One dedicated water-power cascaded hydro subsystem (detailed hydropower), connected to the hub via `Power`. +- CO2 retrofit chain + standalone CO2 source/storage chain, both connected to hub via `CO2` and `Power`. +- Hydrogen chain (electrolyzer + H2 storage), connected to hub via `Power` and `H2`. +- Battery reserve chain + simple nondispatchable RES chain + simple hydro balancing chain, all connected to hub via `Power`. +- District heating chain connected to hub via `Power` + `Heat`. +- Reformer block connected via hub for `NG`/`Power`/`H2`/`CO2`. +- Sink-source examples: both operational and investment variants, connected via hub for `Power`. + +All subsystems are connected through the global multi-carrier hub, demonstrating integration of: +- EnergyModelsBase v0.9.4 +- EnergyModelsCO2 v0.7.6 +- EnergyModelsGeography v0.11.4 +- EnergyModelsHeat v0.1.4 +- EnergyModelsHydrogen v0.8.3 +- EnergyModelsInvestments v0.8.1 +- EnergyModelsRenewableProducers v0.6.7 +""" +function generate_all_in_one_case() + # ----------------------------- + # 1) Resources (unified naming) + # ----------------------------- + CO2 = ResourceEmit("CO2", 1.0) + CO2_proxy = ResourceCarrier("CO2 proxy", 0.0) + Power = ResourceCarrier("Power", 0.0) + H2 = ResourceCarrier("H2", 0.0) + NG = ResourceCarrier("NG", 0.2) + Coal = ResourceCarrier("Coal", 0.35) + Water = ResourceCarrier("Water", 0.0) + reserve_down = ResourceCarrier("reserve down", 0.0) + + HeatLT = ResourceHeat("HeatLT", 30.0, 30.0) + HeatHT = ResourceHeat("HeatHT", 80.0, 30.0) + + products = [Power, H2, CO2, CO2_proxy, NG, Coal, HeatLT, HeatHT, Water, reserve_down] + + # ----------------------------- + # 2) Time structure (keep smaller for SCIP) + # ----------------------------- + op_duration = 3 # hours + op_number = 8 # 8 periods (keeps MILP manageable) + operational_periods = SimpleTimes(op_number, op_duration) + op_per_strat = 8760 + T = TwoLevel(2, 1, operational_periods; op_per_strat) # 2 strategic periods + + prof_n(x) = OperationalProfile(fill(x, op_number)) + # helper: repeat a vector to length op_number + function repeat_to_len(v::Vector{<:Number}, n::Int) + out = Float64[] + while length(out) < n + append!(out, Float64.(v)) + end + return OperationalProfile(out[1:n]) + end + + # ----------------------------- + # 3) Investment model (so we can include investment nodes + corridors) + # ----------------------------- + model = InvestmentModel( + Dict( + CO2 => FixedProfile(1e12), + ), + Dict( + CO2 => FixedProfile(0.0), + ), + CO2, + 0.07, + ) + + # ----------------------------- + # 4) Global hub + # ----------------------------- + hub = GenAvailability( + "hub", + [Power, H2, CO2, CO2_proxy, NG, Coal, HeatLT, HeatHT, reserve_down], + ) + + nodes = EMB.Node[hub] + links = EMB.Link[] + + # ============================================================ + # A) Hydrogen: electrolyzer + H2 storage + H2 demand + power source + # ============================================================ + el_source = RefSource( + "hub-electricity_source", + FixedProfile(200), + prof_n(30.0), + FixedProfile(0.0), + Dict(Power => 1.0), + ) + pem = Electrolyzer( + "PEM", + FixedProfile(100), + FixedProfile(5), + FixedProfile(0), + Dict(Power => 1), + Dict(H2 => 0.69), + ExtensionData[], + LoadLimits(0, 1), + 0.1, + FixedProfile(1.5e5), + 65000, + ) + h2_store = HydrogenStorage{CyclicStrategic}( + "hydrogen storage", + StorCap(FixedProfile(30)), + StorCap(FixedProfile(600)), + H2, Power, + 2.0, 20.0, + 30.0, 45.0, 150.0, + ) + h2_demand = RefSink( + "hub-h2_demand", + repeat_to_len([0, 10, 50, 30], op_number), + Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(200)), + Dict(H2 => 1.0), + ) + + append!(nodes, [el_source, pem, h2_store, h2_demand]) + + # source -> hub (sources cannot take inputs) + push!(links, Direct("el_source-to-hub", el_source, hub, Linear())) + push!(links, Direct("hub-to-pem", hub, pem, Linear())) + push!(links, Direct("pem-to-hub", pem, hub, Linear())) + push!(links, Direct("hub-to-h2_store", hub, h2_store, Linear())) + push!(links, Direct("h2_store-to-hub", h2_store, hub, Linear())) + push!(links, Direct("hub-to-h2_demand", hub, h2_demand, Linear())) + + # ============================================================ + # B) Battery reserve system + # ============================================================ + batt = ReserveBattery{CyclicStrategic}( + "battery", + StorCap(FixedProfile(30)), + StorCap(FixedProfile(80)), + StorCap(FixedProfile(30)), + Power, + Dict(Power => 0.9), + Dict(Power => 0.9), + CycleLife(900, 0.2, FixedProfile(2e5)), + ResourceCarrier[], + [reserve_down], + ) + reserve_down_sink = RefSink( + "reserve down demand", + FixedProfile(10), + Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e2)), + Dict(reserve_down => 1), + ) + append!(nodes, [batt, reserve_down_sink]) + + push!(links, Direct("hub-batt", hub, batt, Linear())) + push!(links, Direct("batt-hub", batt, hub, Linear())) + push!(links, Direct("batt-reserve_down", batt, reserve_down_sink, Linear())) + + # ============================================================ + # C) Simple NonDisRES + balancing source + # ============================================================ + bal_source = RefSource( + "balancing_source", + FixedProfile(50), + prof_n(60.0), + FixedProfile(0.0), + Dict(Power => 1), + ) + wind_simple = NonDisRES( + "wind_simple", + FixedProfile(80), + repeat_to_len([0.9, 0.4, 0.1, 0.8], op_number), + FixedProfile(10), + FixedProfile(0), + Dict(Power => 1), + ) + append!(nodes, [bal_source, wind_simple]) + + push!(links, Direct("bal_source-to-hub", bal_source, hub, Linear())) + push!(links, Direct("wind_simple-to-hub", wind_simple, hub, Linear())) + + # ============================================================ + # D) Simple HydroStor balancing + # ============================================================ + hydro_simple = HydroStor{CyclicStrategic}( + "hydropower_simple", + StorCapOpexFixed(FixedProfile(200), FixedProfile(0.0)), + StorCapOpexVar(FixedProfile(30), FixedProfile(0.0)), + FixedProfile(50), + FixedProfile(5), + FixedProfile(0.0), + Power, + Dict(Power => 0.9), + Dict(Power => 1.0), + Data[], + ) + append!(nodes, [hydro_simple]) + + push!(links, Direct("hub-hydro_simple", hub, hydro_simple, Linear())) + push!(links, Direct("hydro_simple-hub", hydro_simple, hub, Linear())) + + # ============================================================ + # E) District heating + # ============================================================ + dh_source = RefSource( + "district_heat_source", + FixedProfile(60), + FixedProfile(10), + FixedProfile(0), + Dict(HeatLT => 1), + ) + heat_pump = HeatPump( + "heat_pump", + FixedProfile(40), + 0, + EMHeat.t_supply(HeatLT), + EMHeat.t_supply(HeatHT), + FixedProfile(0.5), + HeatLT, + Power, + FixedProfile(0), + FixedProfile(0), + Dict(HeatHT => 1), + ) + tes = BoundRateTES{CyclicRepresentative}( + "TES", + StorCap(FixedProfile(400)), + HeatHT, + 0.02, + 0.05, + 0.15, + Dict(HeatHT => 1), + Dict(HeatHT => 1), + ) + heat_demand = RefSink( + "heat_demand", + repeat_to_len([0, 30, 10, 50], op_number), + Dict(:surplus => FixedProfile(100), :deficit => FixedProfile(1000)), + Dict(HeatHT => 1), + ) + append!(nodes, [dh_source, heat_pump, tes, heat_demand]) + + push!(links, Direct("dh_source-to-heat_pump", dh_source, heat_pump, Linear())) + push!(links, Direct("hub-to-heat_pump_power", hub, heat_pump, Linear())) + push!(links, Direct("heat_pump-to-hub_heatHT", heat_pump, hub, Linear())) + push!(links, Direct("hub-to-heat_demand", hub, heat_demand, Linear())) + push!(links, Direct("heat_pump-to-TES", heat_pump, tes, Linear())) + push!(links, Direct("TES-to-hub", tes, hub, Linear())) + + push!( + links, + DHPipe( + "dh_pipe_source_to_hp", + dh_source, + heat_pump, + FixedProfile(60), + 2_000_000.0, + 0.025e-6, + FixedProfile(10.0), + HeatLT, + ), + ) + + # ============================================================ + # F) CO2 retrofit chain + # ============================================================ + ng_source_ccgt = RefSource( + "NG_source_for_CCGT", + FixedProfile(2000), + FixedProfile(5.5), + FixedProfile(0), + Dict(NG => 1), + ) + ccgt = RefNetworkNodeRetrofit( + "CCGT_retrofittable", + FixedProfile(800), + FixedProfile(5.5), + FixedProfile(0), + Dict(NG => 1.66), + Dict(Power => 1), + CO2_proxy, + Data[CaptureEnergyEmissions(1.0)], + ) + ccs = CCSRetroFit( + "CCS_unit", + FixedProfile(400), + FixedProfile(0), + FixedProfile(0), + Dict(NG => 1.0, CO2_proxy => 0), + Dict(CO2 => 0), + CO2_proxy, + Data[CaptureEnergyEmissions(0.9)], + ) + co2_store_big = CO2Storage( + "CO2_storage_big", + StorCapOpex(FixedProfile(400), FixedProfile(9.1), FixedProfile(0)), + StorCap(FixedProfile(1e8)), + CO2, + Dict(CO2 => 1), + ) + el_demand_big = RefSink( + "electricity_demand_big", + FixedProfile(200), + Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e4)), + Dict(Power => 1), + ) + append!(nodes, [ng_source_ccgt, ccgt, ccs, co2_store_big, el_demand_big]) + + # correct direction: source -> user + push!(links, Direct("ng_source_ccgt-to-ccgt", ng_source_ccgt, ccgt, Linear())) + push!(links, Direct("ccgt-to-hub_power", ccgt, hub, Linear())) + push!(links, Direct("ccgt-to-ccs", ccgt, ccs, Linear())) + push!(links, Direct("hub-to-ccs_fuel", hub, ccs, Linear())) + push!(links, Direct("ccs-to-co2_store_big", ccs, co2_store_big, Linear())) + push!(links, Direct("hub-to-el_demand_big", hub, el_demand_big, Linear())) + + # ============================================================ + # G) Standalone CO2 source + CO2 storage (connected via hub) + # ============================================================ + co2_src = CO2Source( + "CO2_source_negative", + FixedProfile(50), + StrategicProfile([-30, -20]), + FixedProfile(1), + Dict(CO2 => 1), + ) + co2_store_small = CO2Storage( + "CO2_storage_small", + StorCapOpex(FixedProfile(50), FixedProfile(9.1), FixedProfile(1)), + StorCap(FixedProfile(1e6)), + CO2, + Dict(CO2 => 1), + ) + append!(nodes, [co2_src, co2_store_small]) + + # connect the CO2 source to the hub (direction matters) + push!(links, Direct("co2_src-to-hub", co2_src, hub, Linear())) + # send CO2 from hub into the storage (do NOT assume storage can dispatch back out) + push!(links, Direct("hub-to-co2_store_small", hub, co2_store_small, Linear())) + + # ============================================================ + # H) Geographic 2-area network (existing) + investment corridor modes + # ============================================================ + 𝒫_geo = [NG, Coal, Power, CO2] + + reg1_av = GeoAvailability("Reg_1-Availability", 𝒫_geo) + reg1_coal_src = RefSource( + "Reg_1-Coal_source", + FixedProfile(100), + FixedProfile(9), + FixedProfile(0), + Dict(Coal => 1), + ) + reg1_coal_pp = RefNetworkNode( + "Reg_1-Coal_power_plant", + FixedProfile(25), + FixedProfile(6), + FixedProfile(0), + Dict(Coal => 2.5), + Dict(Power => 1), + [EmissionsEnergy()], + ) + reg1_co2_stor = RefStorage{AccumulatingEmissions}( + "Reg_1-CO2_storage_geo", + StorCapOpex(FixedProfile(60), FixedProfile(9.1), FixedProfile(0)), + StorCap(FixedProfile(600)), + CO2, + Dict(CO2 => 1, Power => 0.02), + Dict(CO2 => 1), + ) + reg1_dem = RefSink( + "Reg_1-Electricity_demand", + FixedProfile(10), + Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e3)), + Dict(Power => 1), + ) + area_1 = RefArea(1, "Coal area", 6.62, 51.04, reg1_av) + + reg2_av = GeoAvailability("Reg_2-Availability", 𝒫_geo) + reg2_ng_src = RefSource( + "Reg_2-NG_source", + FixedProfile(100), + FixedProfile(30), + FixedProfile(0), + Dict(NG => 1), + ) + reg2_ng_ccs_pp = RefNetworkNode( + "Reg_2-ng+CCS_power_plant", + FixedProfile(25), + FixedProfile(5.5), + FixedProfile(0), + Dict(NG => 2.0), + Dict(Power => 1, CO2 => 0), + [CaptureEnergyEmissions(0.9)], + ) + reg2_dem = RefSink( + "Reg_2-Electricity_demand", + repeat_to_len([10, 20, 30, 20], op_number), + Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(5e2)), + Dict(Power => 1), + ) + area_2 = RefArea(2, "Natural gas area", 6.83, 53.45, reg2_av) + + append!( + nodes, + [reg1_av, reg1_coal_src, reg1_coal_pp, reg1_co2_stor, reg1_dem, + reg2_av, reg2_ng_src, reg2_ng_ccs_pp, reg2_dem], + ) + + append!( + links, + [ + Direct("Reg_1-av-coal_pp", reg1_av, reg1_coal_pp, Linear()), + Direct("Reg_1-av-CO2_stor", reg1_av, reg1_co2_stor, Linear()), + Direct("Reg_1-av-demand", reg1_av, reg1_dem, Linear()), + Direct("Reg_1-coal_src-av", reg1_coal_src, reg1_av, Linear()), + Direct("Reg_1-coal_pp-av", reg1_coal_pp, reg1_av, Linear()), + Direct("Reg_2-av-NG_pp", reg2_av, reg2_ng_ccs_pp, Linear()), + Direct("Reg_2-av-demand", reg2_av, reg2_dem, Linear()), + Direct("Reg_2-NG_src-av", reg2_ng_src, reg2_av, Linear()), + Direct("Reg_2-NG_pp-av", reg2_ng_ccs_pp, reg2_av, Linear()), + ], + ) + + # --- Investment transmission modes (from investments.jl) --- + power_inv_data = SingleInvData( + FixedProfile(150 * 1e3), + FixedProfile(60), + ContinuousInvestment(FixedProfile(0), FixedProfile(30)), + ) + power_line = RefStatic( + "power_line", + Power, + FixedProfile(0), # initial = 0, invest to build + FixedProfile(0.02), + FixedProfile(0), + FixedProfile(0), + 2, + [power_inv_data], + ) + + co2_pipe_inv_data = SingleInvData( + FixedProfile(260 * 1e3), + FixedProfile(40), + SemiContinuousInvestment(FixedProfile(5), FixedProfile(20)), + ) + co2_pipe = PipeSimple( + "co2_pipeline", + CO2, + CO2, + Power, + FixedProfile(0.01), + FixedProfile(0), # initial = 0, invest to build + FixedProfile(0), + FixedProfile(0), + FixedProfile(0), + [co2_pipe_inv_data], + ) + + transmissions = [Transmission(area_2, area_1, [power_line, co2_pipe])] + areas = [area_1, area_2] + + # Connect geo subsystem to global hub + push!(links, Direct("hub-to-Reg_2_av", hub, reg2_av, Linear())) + push!(links, Direct("Reg_2_av-to-hub", reg2_av, hub, Linear())) + + # ============================================================ + # I) Detailed cascaded hydropower (power-only coupling to hub) + # ============================================================ + m3s_to_mm3 = 3.6e-3 + + water_source = RefSource( + "water_source", + FixedProfile(0), + FixedProfile(0), + FixedProfile(0), + Dict(Water => 1.0), + ) + + reservoir_up = HydroReservoir{CyclicStrategic}( + "hydro_reservoir_up", + StorCap(FixedProfile(10)), + FixedProfile(10*m3s_to_mm3), + Water, + ) + reservoir_down = HydroReservoir{CyclicStrategic}( + "hydro_reservoir_down", + StorCap(FixedProfile(10)), + FixedProfile(0), + Water, + ) + + hydro_gen_cap = 20.0 + gen_up = HydroGenerator( + "hydro_generator_up", + FixedProfile(hydro_gen_cap), + PqPoints([0, 10, 20] / hydro_gen_cap, [0, 10, 22] * m3s_to_mm3 / hydro_gen_cap), + FixedProfile(0), FixedProfile(0), + Power, Water, + ) + gen_down = HydroGenerator( + "hydro_generator_down", + FixedProfile(hydro_gen_cap), + PqPoints([0, 10, 20] / hydro_gen_cap, [0, 10, 22] * m3s_to_mm3 / hydro_gen_cap), + FixedProfile(0), FixedProfile(0), + Power, Water, + ) + + pump_cap = 30.0 + pump = HydroPump( + "hydro_pump", + FixedProfile(pump_cap), + PqPoints([0, 15, 30] / pump_cap, [0, 12, 20] * m3s_to_mm3 / pump_cap), + FixedProfile(0), FixedProfile(0), + Power, Water, + ) + + gate = HydroGate( + "hydro_gate", + FixedProfile(20*m3s_to_mm3), + FixedProfile(0), + FixedProfile(0), + Water, + ) + ocean = RefSink( + "ocean", + FixedProfile(0), + Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(0)), + Dict(Water => 1.0), + ) + + hydro_av = GenAvailability("hydro_av", [Power]) + + price4 = [10, 60, 15, 62] + market_sale = RefSink( + "market_sale", + FixedProfile(0), + Dict(:surplus => repeat_to_len(-price4, op_number), + :deficit => repeat_to_len(price4, op_number)), + Dict(Power => 1.0), + ) + market_buy = RefSource( + "market_buy", + FixedProfile(1000), + repeat_to_len(price4 .+ 0.01, op_number), + FixedProfile(0), + Dict(Power => 1.0), + ) + + append!( + nodes, + [ + water_source, + reservoir_up, + reservoir_down, + gen_up, + gen_down, + pump, + gate, + ocean, + hydro_av, + market_sale, + market_buy, + ], + ) + + append!( + links, + [ + Direct("water_source-res_up", water_source, reservoir_up, Linear()), + Direct("res_up-gen_up", reservoir_up, gen_up, Linear()), + Direct("res_up-gate", reservoir_up, gate, Linear()), + Direct("gen_up-res_down", gen_up, reservoir_down, Linear()), + Direct("gate-res_down", gate, reservoir_down, Linear()), + Direct("res_down-pump", reservoir_down, pump, Linear()), + Direct("pump-res_up", pump, reservoir_up, Linear()), + Direct("res_down-gen_down", reservoir_down, gen_down, Linear()), + Direct("gen_down-ocean", gen_down, ocean, Linear()), + Direct("gen_up-hydro_av", gen_up, hydro_av, Linear()), + Direct("gen_down-hydro_av", gen_down, hydro_av, Linear()), + Direct("hydro_av-pump", hydro_av, pump, Linear()), + Direct("hydro_av-market_sale", hydro_av, market_sale, Linear()), + Direct("market_buy-hydro_av", market_buy, hydro_av, Linear()), + ], + ) + + push!(links, Direct("hydro_av-to-hub", hydro_av, hub, Linear())) + push!(links, Direct("hub-to-hydro_av", hub, hydro_av, Linear())) + + # ============================================================ + # J) Reformer block (MILP, needs SCIP) – connected via hub + # ============================================================ + reformer = Reformer( + "reformer", + FixedProfile(30), + FixedProfile(5), + FixedProfile(0), + Dict(NG => 1.25, Power => 0.11), + Dict(H2 => 1.0, CO2 => 0), + ExtensionData[CaptureEnergyEmissions(0.92)], + LoadLimits(0.2, 1.0), + CommitParameters(FixedProfile(0.2), FixedProfile(3)), + CommitParameters(FixedProfile(0.2), FixedProfile(3)), + CommitParameters(FixedProfile(0.02), FixedProfile(4)), + RampBi(FixedProfile(0.1)), + ) + h2_demand_ref = RefSink( + "h2_demand_reformer", + repeat_to_len([0, 5, 10, 5], op_number), + Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(150)), + Dict(H2 => 1), + ) + co2_sink_ref = RefSink( + "CO2_storage_reformer_sink", + FixedProfile(0), + Dict(:surplus => FixedProfile(9.1), :deficit => FixedProfile(20)), + Dict(CO2 => 1), + ) + append!(nodes, [reformer, h2_demand_ref, co2_sink_ref]) + + push!(links, Direct("hub-to-reformer", hub, reformer, Linear())) + push!(links, Direct("reformer-to-hub", reformer, hub, Linear())) + push!(links, Direct("hub-to-h2_demand_reformer", hub, h2_demand_ref, Linear())) + push!(links, Direct("reformer-to-co2_sink_ref", reformer, co2_sink_ref, Linear())) + + # ============================================================ + # K) Sink-source examples merged in through the hub + # - operational sink_source.jl + # - investment sink_source_invest.jl + # ============================================================ + + # (K1) plain sink-source + ss_source = RefSource( + "ss_electricity_source", + FixedProfile(50), + FixedProfile(30), + FixedProfile(0), + Dict(Power => 1), + ) + ss_demand = RefSink( + "ss_electricity_demand", + repeat_to_len([20, 30, 40, 30], op_number), + Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), + Dict(Power => 1), + ) + append!(nodes, [ss_source, ss_demand]) + push!(links, Direct("ss_source-to-hub", ss_source, hub, Linear())) + push!(links, Direct("hub-to-ss_demand", hub, ss_demand, Linear())) + + # (K2) investment sink-source: source starts at 0 and can be built + lifetime = FixedProfile(15) + inv_data_source = SingleInvData( + FixedProfile(300 * 1e3), + FixedProfile(50), + ContinuousInvestment(FixedProfile(0), FixedProfile(30)), + RollingLife(lifetime), + ) + ss_inv_source = RefSource( + "ss_inv_electricity_source", + FixedProfile(0), + FixedProfile(10), + FixedProfile(5), + Dict(Power => 1), + [inv_data_source], + ) + ss_inv_demand = RefSink( + "ss_inv_electricity_demand", + repeat_to_len([20, 30, 40, 30], op_number), + Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), + Dict(Power => 1), + ) + append!(nodes, [ss_inv_source, ss_inv_demand]) + push!(links, Direct("ss_inv_source-to-hub", ss_inv_source, hub, Linear())) + push!(links, Direct("hub-to-ss_inv_demand", hub, ss_inv_demand, Linear())) + + # ----------------------------- + # FINAL Case assembly + # ----------------------------- + case = Case( + T, + products, + [nodes, links, areas, transmissions], + [[get_nodes, get_links], [get_areas, get_transmissions]], + ) + + return case, model +end + +function run_all_in_one_case() + case, model = generate_all_in_one_case() + + optimizer = optimizer_with_attributes( + SCIP.Optimizer, + MOI.Silent() => true, + "display/verblevel" => 0, + "limits/time" => 180.0, + "limits/gap" => 0.02, + ) + + m = run_model(case, model, optimizer) + + # The case can be visualized with + gui = GUI( + case; + model = m, + design_path = joinpath(@__DIR__, "design/example_all_structures"), + ) + + return case, model, m, gui +end diff --git a/test/runtests.jl b/test/runtests.jl index 540fa88..002435c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -21,6 +21,7 @@ Pkg.activate(env) include(joinpath(testdir, "case7.jl")) include(joinpath(testdir, "EMI_geography_2.jl")) include(joinpath(testdir, "example_test.jl")) +include(joinpath(testdir, "example_all_structures.jl")) # Add utilities needed for examples include(joinpath(exdir, "utils.jl")) diff --git a/test/test_descriptive_names.jl b/test/test_descriptive_names.jl index 3ebd6af..16b6de6 100644 --- a/test/test_descriptive_names.jl +++ b/test/test_descriptive_names.jl @@ -2,6 +2,9 @@ const EMB = EnergyModelsBase const EMI = EnergyModelsInvestments const EMG = EnergyModelsGeography const EMRP = EnergyModelsRenewableProducers +const EMH2 = EnergyModelsHydrogen +const EMH = EnergyModelsHeat +const EMC = EnergyModelsCO2 case, model, m, gui = run_case() @@ -78,22 +81,16 @@ case, model, m, gui = run_case() @testset "Test existence of descriptive names for all available EMX-packages" begin # Check that no descriptive names are empty for types descriptive_names = create_descriptive_names() - types_map = get_descriptive_names([EMB, EMI, EMG, EMRP], descriptive_names) + types_map = + get_descriptive_names([EMB, EMI, EMG, EMRP, EMH2, EMH, EMC], descriptive_names) @test !any(any(isempty.(values(a))) for a ∈ values(types_map)) - case, model = generate_example_data_geo() - m = create_model(case, model) + # Check that no warnings are logged when running a full case + @test_logs min_level=Logging.Error case, _, m, gui = run_all_in_one_case() # Check that no descriptive names are empty for variables variables_map = get_descriptive_names(m, descriptive_names) @test !any(any(isempty.(values(a))) for a ∈ values(variables_map)) - - case, model = generate_example_hp() - m = create_model(case, model) - - # Check that no descriptive names are empty for variables for EMRP - variables_map_EMRP = get_descriptive_names(m, descriptive_names) - @test !any(any(isempty.(values(a))) for a ∈ values(variables_map_EMRP)) end end EMGUI.close(gui)