From a0d1cafa3084b9aa43dd61a094b768b6f8a02a3b Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Thu, 21 Mar 2024 16:02:28 -0400 Subject: [PATCH 01/59] Renamed DC-OPF folder to 9_IEEE_9_bus_DC_OPF --- .../{IEEE_9_bus_DC_OPF => 9_IEEE_9_bus_DC_OPF}/README.md | 0 example_systems/{IEEE_9_bus_DC_OPF => 9_IEEE_9_bus_DC_OPF}/Run.jl | 0 .../resources/Thermal.csv | 0 .../settings/cplex_settings.yml | 0 .../settings/genx_settings.yml | 0 .../settings/gurobi_settings.yml | 0 .../settings/highs_settings.yml | 0 .../settings/time_domain_reduction_settings.yml | 0 .../system/Demand_data.csv | 0 .../system/Fuels_data.csv | 0 .../system/Generators_variability.csv | 0 .../{IEEE_9_bus_DC_OPF => 9_IEEE_9_bus_DC_OPF}/system/Network.csv | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename example_systems/{IEEE_9_bus_DC_OPF => 9_IEEE_9_bus_DC_OPF}/README.md (100%) rename example_systems/{IEEE_9_bus_DC_OPF => 9_IEEE_9_bus_DC_OPF}/Run.jl (100%) rename example_systems/{IEEE_9_bus_DC_OPF => 9_IEEE_9_bus_DC_OPF}/resources/Thermal.csv (100%) rename example_systems/{IEEE_9_bus_DC_OPF => 9_IEEE_9_bus_DC_OPF}/settings/cplex_settings.yml (100%) rename example_systems/{IEEE_9_bus_DC_OPF => 9_IEEE_9_bus_DC_OPF}/settings/genx_settings.yml (100%) rename example_systems/{IEEE_9_bus_DC_OPF => 9_IEEE_9_bus_DC_OPF}/settings/gurobi_settings.yml (100%) rename example_systems/{IEEE_9_bus_DC_OPF => 9_IEEE_9_bus_DC_OPF}/settings/highs_settings.yml (100%) rename example_systems/{IEEE_9_bus_DC_OPF => 9_IEEE_9_bus_DC_OPF}/settings/time_domain_reduction_settings.yml (100%) rename example_systems/{IEEE_9_bus_DC_OPF => 9_IEEE_9_bus_DC_OPF}/system/Demand_data.csv (100%) rename example_systems/{IEEE_9_bus_DC_OPF => 9_IEEE_9_bus_DC_OPF}/system/Fuels_data.csv (100%) rename example_systems/{IEEE_9_bus_DC_OPF => 9_IEEE_9_bus_DC_OPF}/system/Generators_variability.csv (100%) rename example_systems/{IEEE_9_bus_DC_OPF => 9_IEEE_9_bus_DC_OPF}/system/Network.csv (100%) diff --git a/example_systems/IEEE_9_bus_DC_OPF/README.md b/example_systems/9_IEEE_9_bus_DC_OPF/README.md similarity index 100% rename from example_systems/IEEE_9_bus_DC_OPF/README.md rename to example_systems/9_IEEE_9_bus_DC_OPF/README.md diff --git a/example_systems/IEEE_9_bus_DC_OPF/Run.jl b/example_systems/9_IEEE_9_bus_DC_OPF/Run.jl similarity index 100% rename from example_systems/IEEE_9_bus_DC_OPF/Run.jl rename to example_systems/9_IEEE_9_bus_DC_OPF/Run.jl diff --git a/example_systems/IEEE_9_bus_DC_OPF/resources/Thermal.csv b/example_systems/9_IEEE_9_bus_DC_OPF/resources/Thermal.csv similarity index 100% rename from example_systems/IEEE_9_bus_DC_OPF/resources/Thermal.csv rename to example_systems/9_IEEE_9_bus_DC_OPF/resources/Thermal.csv diff --git a/example_systems/IEEE_9_bus_DC_OPF/settings/cplex_settings.yml b/example_systems/9_IEEE_9_bus_DC_OPF/settings/cplex_settings.yml similarity index 100% rename from example_systems/IEEE_9_bus_DC_OPF/settings/cplex_settings.yml rename to example_systems/9_IEEE_9_bus_DC_OPF/settings/cplex_settings.yml diff --git a/example_systems/IEEE_9_bus_DC_OPF/settings/genx_settings.yml b/example_systems/9_IEEE_9_bus_DC_OPF/settings/genx_settings.yml similarity index 100% rename from example_systems/IEEE_9_bus_DC_OPF/settings/genx_settings.yml rename to example_systems/9_IEEE_9_bus_DC_OPF/settings/genx_settings.yml diff --git a/example_systems/IEEE_9_bus_DC_OPF/settings/gurobi_settings.yml b/example_systems/9_IEEE_9_bus_DC_OPF/settings/gurobi_settings.yml similarity index 100% rename from example_systems/IEEE_9_bus_DC_OPF/settings/gurobi_settings.yml rename to example_systems/9_IEEE_9_bus_DC_OPF/settings/gurobi_settings.yml diff --git a/example_systems/IEEE_9_bus_DC_OPF/settings/highs_settings.yml b/example_systems/9_IEEE_9_bus_DC_OPF/settings/highs_settings.yml similarity index 100% rename from example_systems/IEEE_9_bus_DC_OPF/settings/highs_settings.yml rename to example_systems/9_IEEE_9_bus_DC_OPF/settings/highs_settings.yml diff --git a/example_systems/IEEE_9_bus_DC_OPF/settings/time_domain_reduction_settings.yml b/example_systems/9_IEEE_9_bus_DC_OPF/settings/time_domain_reduction_settings.yml similarity index 100% rename from example_systems/IEEE_9_bus_DC_OPF/settings/time_domain_reduction_settings.yml rename to example_systems/9_IEEE_9_bus_DC_OPF/settings/time_domain_reduction_settings.yml diff --git a/example_systems/IEEE_9_bus_DC_OPF/system/Demand_data.csv b/example_systems/9_IEEE_9_bus_DC_OPF/system/Demand_data.csv similarity index 100% rename from example_systems/IEEE_9_bus_DC_OPF/system/Demand_data.csv rename to example_systems/9_IEEE_9_bus_DC_OPF/system/Demand_data.csv diff --git a/example_systems/IEEE_9_bus_DC_OPF/system/Fuels_data.csv b/example_systems/9_IEEE_9_bus_DC_OPF/system/Fuels_data.csv similarity index 100% rename from example_systems/IEEE_9_bus_DC_OPF/system/Fuels_data.csv rename to example_systems/9_IEEE_9_bus_DC_OPF/system/Fuels_data.csv diff --git a/example_systems/IEEE_9_bus_DC_OPF/system/Generators_variability.csv b/example_systems/9_IEEE_9_bus_DC_OPF/system/Generators_variability.csv similarity index 100% rename from example_systems/IEEE_9_bus_DC_OPF/system/Generators_variability.csv rename to example_systems/9_IEEE_9_bus_DC_OPF/system/Generators_variability.csv diff --git a/example_systems/IEEE_9_bus_DC_OPF/system/Network.csv b/example_systems/9_IEEE_9_bus_DC_OPF/system/Network.csv similarity index 100% rename from example_systems/IEEE_9_bus_DC_OPF/system/Network.csv rename to example_systems/9_IEEE_9_bus_DC_OPF/system/Network.csv From cb34958f0f51aa257a065c5da47d2ed0c334fb22 Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Thu, 21 Mar 2024 16:13:40 -0400 Subject: [PATCH 02/59] Renamed c2 capture and retrofit folders to have identical naming convention; Edited CHANGELOG to have better description of changed example folder --- CHANGELOG.md | 3 +++ .../README.md | 0 .../Run.jl | 0 .../policies/CO2_cap.csv | 0 .../policies/Minimum_capacity_requirement.csv | 0 .../resources/Storage.csv | 0 .../resources/Thermal.csv | 0 .../resources/Vre.csv | 0 .../Resource_minimum_capacity_requirement.csv | 0 .../settings/clp_settings.yml | 0 .../settings/cplex_settings.yml | 0 .../settings/genx_settings.yml | 0 .../settings/gurobi_settings.yml | 0 .../settings/highs_settings.yml | 0 .../settings/time_domain_reduction_settings.yml | 0 .../system/Demand_data.csv | 0 .../system/Fuels_data.csv | 0 .../system/Generators_variability.csv | 0 .../system/Network.csv | 0 .../README.md | 0 .../Run.jl | 0 .../policies/CO2_cap.csv | 0 .../policies/Capacity_reserve_margin.csv | 0 .../policies/Energy_share_requirement.csv | 0 .../policies/Minimum_capacity_requirement.csv | 0 .../resources/Storage.csv | 0 .../resources/Thermal.csv | 0 .../resources/Vre.csv | 0 .../Resource_minimum_capacity_requirement.csv | 0 .../settings/clp_settings.yml | 0 .../settings/cplex_settings.yml | 0 .../settings/genx_settings.yml | 0 .../settings/gurobi_settings.yml | 0 .../settings/highs_settings.yml | 0 .../settings/time_domain_reduction_settings.yml | 0 .../system/Demand_data.csv | 0 .../system/Fuels_data.csv | 0 .../system/Generators_variability.csv | 0 .../system/Network.csv | 0 39 files changed, 3 insertions(+) rename example_systems/{3_three_zone_w_co2_capture => 3_three_zones_w_co2_capture}/README.md (100%) rename example_systems/{3_three_zone_w_co2_capture => 3_three_zones_w_co2_capture}/Run.jl (100%) rename example_systems/{3_three_zone_w_co2_capture => 3_three_zones_w_co2_capture}/policies/CO2_cap.csv (100%) rename example_systems/{3_three_zone_w_co2_capture => 3_three_zones_w_co2_capture}/policies/Minimum_capacity_requirement.csv (100%) rename example_systems/{3_three_zone_w_co2_capture => 3_three_zones_w_co2_capture}/resources/Storage.csv (100%) rename example_systems/{3_three_zone_w_co2_capture => 3_three_zones_w_co2_capture}/resources/Thermal.csv (100%) rename example_systems/{3_three_zone_w_co2_capture => 3_three_zones_w_co2_capture}/resources/Vre.csv (100%) rename example_systems/{3_three_zone_w_co2_capture => 3_three_zones_w_co2_capture}/resources/policy_assignments/Resource_minimum_capacity_requirement.csv (100%) rename example_systems/{3_three_zone_w_co2_capture => 3_three_zones_w_co2_capture}/settings/clp_settings.yml (100%) rename example_systems/{3_three_zone_w_co2_capture => 3_three_zones_w_co2_capture}/settings/cplex_settings.yml (100%) rename example_systems/{3_three_zone_w_co2_capture => 3_three_zones_w_co2_capture}/settings/genx_settings.yml (100%) rename example_systems/{3_three_zone_w_co2_capture => 3_three_zones_w_co2_capture}/settings/gurobi_settings.yml (100%) rename example_systems/{3_three_zone_w_co2_capture => 3_three_zones_w_co2_capture}/settings/highs_settings.yml (100%) rename example_systems/{3_three_zone_w_co2_capture => 3_three_zones_w_co2_capture}/settings/time_domain_reduction_settings.yml (100%) rename example_systems/{3_three_zone_w_co2_capture => 3_three_zones_w_co2_capture}/system/Demand_data.csv (100%) rename example_systems/{3_three_zone_w_co2_capture => 3_three_zones_w_co2_capture}/system/Fuels_data.csv (100%) rename example_systems/{3_three_zone_w_co2_capture => 3_three_zones_w_co2_capture}/system/Generators_variability.csv (100%) rename example_systems/{3_three_zone_w_co2_capture => 3_three_zones_w_co2_capture}/system/Network.csv (100%) rename example_systems/{8_three_zone_w_retrofit => 8_three_zones_w_retrofit}/README.md (100%) rename example_systems/{8_three_zone_w_retrofit => 8_three_zones_w_retrofit}/Run.jl (100%) rename example_systems/{8_three_zone_w_retrofit => 8_three_zones_w_retrofit}/policies/CO2_cap.csv (100%) rename example_systems/{8_three_zone_w_retrofit => 8_three_zones_w_retrofit}/policies/Capacity_reserve_margin.csv (100%) rename example_systems/{8_three_zone_w_retrofit => 8_three_zones_w_retrofit}/policies/Energy_share_requirement.csv (100%) rename example_systems/{8_three_zone_w_retrofit => 8_three_zones_w_retrofit}/policies/Minimum_capacity_requirement.csv (100%) rename example_systems/{8_three_zone_w_retrofit => 8_three_zones_w_retrofit}/resources/Storage.csv (100%) rename example_systems/{8_three_zone_w_retrofit => 8_three_zones_w_retrofit}/resources/Thermal.csv (100%) rename example_systems/{8_three_zone_w_retrofit => 8_three_zones_w_retrofit}/resources/Vre.csv (100%) rename example_systems/{8_three_zone_w_retrofit => 8_three_zones_w_retrofit}/resources/policy_assignments/Resource_minimum_capacity_requirement.csv (100%) rename example_systems/{8_three_zone_w_retrofit => 8_three_zones_w_retrofit}/settings/clp_settings.yml (100%) rename example_systems/{8_three_zone_w_retrofit => 8_three_zones_w_retrofit}/settings/cplex_settings.yml (100%) rename example_systems/{8_three_zone_w_retrofit => 8_three_zones_w_retrofit}/settings/genx_settings.yml (100%) rename example_systems/{8_three_zone_w_retrofit => 8_three_zones_w_retrofit}/settings/gurobi_settings.yml (100%) rename example_systems/{8_three_zone_w_retrofit => 8_three_zones_w_retrofit}/settings/highs_settings.yml (100%) rename example_systems/{8_three_zone_w_retrofit => 8_three_zones_w_retrofit}/settings/time_domain_reduction_settings.yml (100%) rename example_systems/{8_three_zone_w_retrofit => 8_three_zones_w_retrofit}/system/Demand_data.csv (100%) rename example_systems/{8_three_zone_w_retrofit => 8_three_zones_w_retrofit}/system/Fuels_data.csv (100%) rename example_systems/{8_three_zone_w_retrofit => 8_three_zones_w_retrofit}/system/Generators_variability.csv (100%) rename example_systems/{8_three_zone_w_retrofit => 8_three_zones_w_retrofit}/system/Network.csv (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0fdf495d9..e2387f92dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `operational_reserves` (#641). - New folder structure for a GenX case. The input files are now organized in the following folders: `settings`, `policies`, `resources` and `system`. The examples and tests have been updated to reflect this change. +- New folder structure implemented for `example_system`. This folder now consists of nine separate folders each pertaining to a different case study example, + ranging from the ISONE three zones, with singlestage, multistage, electrolyzers, all the way to the 9 bus IEEE case for running DC-OPF. + ### Deprecated - The above `load` keys, which generally refer to electrical demand, are being deprecated. diff --git a/example_systems/3_three_zone_w_co2_capture/README.md b/example_systems/3_three_zones_w_co2_capture/README.md similarity index 100% rename from example_systems/3_three_zone_w_co2_capture/README.md rename to example_systems/3_three_zones_w_co2_capture/README.md diff --git a/example_systems/3_three_zone_w_co2_capture/Run.jl b/example_systems/3_three_zones_w_co2_capture/Run.jl similarity index 100% rename from example_systems/3_three_zone_w_co2_capture/Run.jl rename to example_systems/3_three_zones_w_co2_capture/Run.jl diff --git a/example_systems/3_three_zone_w_co2_capture/policies/CO2_cap.csv b/example_systems/3_three_zones_w_co2_capture/policies/CO2_cap.csv similarity index 100% rename from example_systems/3_three_zone_w_co2_capture/policies/CO2_cap.csv rename to example_systems/3_three_zones_w_co2_capture/policies/CO2_cap.csv diff --git a/example_systems/3_three_zone_w_co2_capture/policies/Minimum_capacity_requirement.csv b/example_systems/3_three_zones_w_co2_capture/policies/Minimum_capacity_requirement.csv similarity index 100% rename from example_systems/3_three_zone_w_co2_capture/policies/Minimum_capacity_requirement.csv rename to example_systems/3_three_zones_w_co2_capture/policies/Minimum_capacity_requirement.csv diff --git a/example_systems/3_three_zone_w_co2_capture/resources/Storage.csv b/example_systems/3_three_zones_w_co2_capture/resources/Storage.csv similarity index 100% rename from example_systems/3_three_zone_w_co2_capture/resources/Storage.csv rename to example_systems/3_three_zones_w_co2_capture/resources/Storage.csv diff --git a/example_systems/3_three_zone_w_co2_capture/resources/Thermal.csv b/example_systems/3_three_zones_w_co2_capture/resources/Thermal.csv similarity index 100% rename from example_systems/3_three_zone_w_co2_capture/resources/Thermal.csv rename to example_systems/3_three_zones_w_co2_capture/resources/Thermal.csv diff --git a/example_systems/3_three_zone_w_co2_capture/resources/Vre.csv b/example_systems/3_three_zones_w_co2_capture/resources/Vre.csv similarity index 100% rename from example_systems/3_three_zone_w_co2_capture/resources/Vre.csv rename to example_systems/3_three_zones_w_co2_capture/resources/Vre.csv diff --git a/example_systems/3_three_zone_w_co2_capture/resources/policy_assignments/Resource_minimum_capacity_requirement.csv b/example_systems/3_three_zones_w_co2_capture/resources/policy_assignments/Resource_minimum_capacity_requirement.csv similarity index 100% rename from example_systems/3_three_zone_w_co2_capture/resources/policy_assignments/Resource_minimum_capacity_requirement.csv rename to example_systems/3_three_zones_w_co2_capture/resources/policy_assignments/Resource_minimum_capacity_requirement.csv diff --git a/example_systems/3_three_zone_w_co2_capture/settings/clp_settings.yml b/example_systems/3_three_zones_w_co2_capture/settings/clp_settings.yml similarity index 100% rename from example_systems/3_three_zone_w_co2_capture/settings/clp_settings.yml rename to example_systems/3_three_zones_w_co2_capture/settings/clp_settings.yml diff --git a/example_systems/3_three_zone_w_co2_capture/settings/cplex_settings.yml b/example_systems/3_three_zones_w_co2_capture/settings/cplex_settings.yml similarity index 100% rename from example_systems/3_three_zone_w_co2_capture/settings/cplex_settings.yml rename to example_systems/3_three_zones_w_co2_capture/settings/cplex_settings.yml diff --git a/example_systems/3_three_zone_w_co2_capture/settings/genx_settings.yml b/example_systems/3_three_zones_w_co2_capture/settings/genx_settings.yml similarity index 100% rename from example_systems/3_three_zone_w_co2_capture/settings/genx_settings.yml rename to example_systems/3_three_zones_w_co2_capture/settings/genx_settings.yml diff --git a/example_systems/3_three_zone_w_co2_capture/settings/gurobi_settings.yml b/example_systems/3_three_zones_w_co2_capture/settings/gurobi_settings.yml similarity index 100% rename from example_systems/3_three_zone_w_co2_capture/settings/gurobi_settings.yml rename to example_systems/3_three_zones_w_co2_capture/settings/gurobi_settings.yml diff --git a/example_systems/3_three_zone_w_co2_capture/settings/highs_settings.yml b/example_systems/3_three_zones_w_co2_capture/settings/highs_settings.yml similarity index 100% rename from example_systems/3_three_zone_w_co2_capture/settings/highs_settings.yml rename to example_systems/3_three_zones_w_co2_capture/settings/highs_settings.yml diff --git a/example_systems/3_three_zone_w_co2_capture/settings/time_domain_reduction_settings.yml b/example_systems/3_three_zones_w_co2_capture/settings/time_domain_reduction_settings.yml similarity index 100% rename from example_systems/3_three_zone_w_co2_capture/settings/time_domain_reduction_settings.yml rename to example_systems/3_three_zones_w_co2_capture/settings/time_domain_reduction_settings.yml diff --git a/example_systems/3_three_zone_w_co2_capture/system/Demand_data.csv b/example_systems/3_three_zones_w_co2_capture/system/Demand_data.csv similarity index 100% rename from example_systems/3_three_zone_w_co2_capture/system/Demand_data.csv rename to example_systems/3_three_zones_w_co2_capture/system/Demand_data.csv diff --git a/example_systems/3_three_zone_w_co2_capture/system/Fuels_data.csv b/example_systems/3_three_zones_w_co2_capture/system/Fuels_data.csv similarity index 100% rename from example_systems/3_three_zone_w_co2_capture/system/Fuels_data.csv rename to example_systems/3_three_zones_w_co2_capture/system/Fuels_data.csv diff --git a/example_systems/3_three_zone_w_co2_capture/system/Generators_variability.csv b/example_systems/3_three_zones_w_co2_capture/system/Generators_variability.csv similarity index 100% rename from example_systems/3_three_zone_w_co2_capture/system/Generators_variability.csv rename to example_systems/3_three_zones_w_co2_capture/system/Generators_variability.csv diff --git a/example_systems/3_three_zone_w_co2_capture/system/Network.csv b/example_systems/3_three_zones_w_co2_capture/system/Network.csv similarity index 100% rename from example_systems/3_three_zone_w_co2_capture/system/Network.csv rename to example_systems/3_three_zones_w_co2_capture/system/Network.csv diff --git a/example_systems/8_three_zone_w_retrofit/README.md b/example_systems/8_three_zones_w_retrofit/README.md similarity index 100% rename from example_systems/8_three_zone_w_retrofit/README.md rename to example_systems/8_three_zones_w_retrofit/README.md diff --git a/example_systems/8_three_zone_w_retrofit/Run.jl b/example_systems/8_three_zones_w_retrofit/Run.jl similarity index 100% rename from example_systems/8_three_zone_w_retrofit/Run.jl rename to example_systems/8_three_zones_w_retrofit/Run.jl diff --git a/example_systems/8_three_zone_w_retrofit/policies/CO2_cap.csv b/example_systems/8_three_zones_w_retrofit/policies/CO2_cap.csv similarity index 100% rename from example_systems/8_three_zone_w_retrofit/policies/CO2_cap.csv rename to example_systems/8_three_zones_w_retrofit/policies/CO2_cap.csv diff --git a/example_systems/8_three_zone_w_retrofit/policies/Capacity_reserve_margin.csv b/example_systems/8_three_zones_w_retrofit/policies/Capacity_reserve_margin.csv similarity index 100% rename from example_systems/8_three_zone_w_retrofit/policies/Capacity_reserve_margin.csv rename to example_systems/8_three_zones_w_retrofit/policies/Capacity_reserve_margin.csv diff --git a/example_systems/8_three_zone_w_retrofit/policies/Energy_share_requirement.csv b/example_systems/8_three_zones_w_retrofit/policies/Energy_share_requirement.csv similarity index 100% rename from example_systems/8_three_zone_w_retrofit/policies/Energy_share_requirement.csv rename to example_systems/8_three_zones_w_retrofit/policies/Energy_share_requirement.csv diff --git a/example_systems/8_three_zone_w_retrofit/policies/Minimum_capacity_requirement.csv b/example_systems/8_three_zones_w_retrofit/policies/Minimum_capacity_requirement.csv similarity index 100% rename from example_systems/8_three_zone_w_retrofit/policies/Minimum_capacity_requirement.csv rename to example_systems/8_three_zones_w_retrofit/policies/Minimum_capacity_requirement.csv diff --git a/example_systems/8_three_zone_w_retrofit/resources/Storage.csv b/example_systems/8_three_zones_w_retrofit/resources/Storage.csv similarity index 100% rename from example_systems/8_three_zone_w_retrofit/resources/Storage.csv rename to example_systems/8_three_zones_w_retrofit/resources/Storage.csv diff --git a/example_systems/8_three_zone_w_retrofit/resources/Thermal.csv b/example_systems/8_three_zones_w_retrofit/resources/Thermal.csv similarity index 100% rename from example_systems/8_three_zone_w_retrofit/resources/Thermal.csv rename to example_systems/8_three_zones_w_retrofit/resources/Thermal.csv diff --git a/example_systems/8_three_zone_w_retrofit/resources/Vre.csv b/example_systems/8_three_zones_w_retrofit/resources/Vre.csv similarity index 100% rename from example_systems/8_three_zone_w_retrofit/resources/Vre.csv rename to example_systems/8_three_zones_w_retrofit/resources/Vre.csv diff --git a/example_systems/8_three_zone_w_retrofit/resources/policy_assignments/Resource_minimum_capacity_requirement.csv b/example_systems/8_three_zones_w_retrofit/resources/policy_assignments/Resource_minimum_capacity_requirement.csv similarity index 100% rename from example_systems/8_three_zone_w_retrofit/resources/policy_assignments/Resource_minimum_capacity_requirement.csv rename to example_systems/8_three_zones_w_retrofit/resources/policy_assignments/Resource_minimum_capacity_requirement.csv diff --git a/example_systems/8_three_zone_w_retrofit/settings/clp_settings.yml b/example_systems/8_three_zones_w_retrofit/settings/clp_settings.yml similarity index 100% rename from example_systems/8_three_zone_w_retrofit/settings/clp_settings.yml rename to example_systems/8_three_zones_w_retrofit/settings/clp_settings.yml diff --git a/example_systems/8_three_zone_w_retrofit/settings/cplex_settings.yml b/example_systems/8_three_zones_w_retrofit/settings/cplex_settings.yml similarity index 100% rename from example_systems/8_three_zone_w_retrofit/settings/cplex_settings.yml rename to example_systems/8_three_zones_w_retrofit/settings/cplex_settings.yml diff --git a/example_systems/8_three_zone_w_retrofit/settings/genx_settings.yml b/example_systems/8_three_zones_w_retrofit/settings/genx_settings.yml similarity index 100% rename from example_systems/8_three_zone_w_retrofit/settings/genx_settings.yml rename to example_systems/8_three_zones_w_retrofit/settings/genx_settings.yml diff --git a/example_systems/8_three_zone_w_retrofit/settings/gurobi_settings.yml b/example_systems/8_three_zones_w_retrofit/settings/gurobi_settings.yml similarity index 100% rename from example_systems/8_three_zone_w_retrofit/settings/gurobi_settings.yml rename to example_systems/8_three_zones_w_retrofit/settings/gurobi_settings.yml diff --git a/example_systems/8_three_zone_w_retrofit/settings/highs_settings.yml b/example_systems/8_three_zones_w_retrofit/settings/highs_settings.yml similarity index 100% rename from example_systems/8_three_zone_w_retrofit/settings/highs_settings.yml rename to example_systems/8_three_zones_w_retrofit/settings/highs_settings.yml diff --git a/example_systems/8_three_zone_w_retrofit/settings/time_domain_reduction_settings.yml b/example_systems/8_three_zones_w_retrofit/settings/time_domain_reduction_settings.yml similarity index 100% rename from example_systems/8_three_zone_w_retrofit/settings/time_domain_reduction_settings.yml rename to example_systems/8_three_zones_w_retrofit/settings/time_domain_reduction_settings.yml diff --git a/example_systems/8_three_zone_w_retrofit/system/Demand_data.csv b/example_systems/8_three_zones_w_retrofit/system/Demand_data.csv similarity index 100% rename from example_systems/8_three_zone_w_retrofit/system/Demand_data.csv rename to example_systems/8_three_zones_w_retrofit/system/Demand_data.csv diff --git a/example_systems/8_three_zone_w_retrofit/system/Fuels_data.csv b/example_systems/8_three_zones_w_retrofit/system/Fuels_data.csv similarity index 100% rename from example_systems/8_three_zone_w_retrofit/system/Fuels_data.csv rename to example_systems/8_three_zones_w_retrofit/system/Fuels_data.csv diff --git a/example_systems/8_three_zone_w_retrofit/system/Generators_variability.csv b/example_systems/8_three_zones_w_retrofit/system/Generators_variability.csv similarity index 100% rename from example_systems/8_three_zone_w_retrofit/system/Generators_variability.csv rename to example_systems/8_three_zones_w_retrofit/system/Generators_variability.csv diff --git a/example_systems/8_three_zone_w_retrofit/system/Network.csv b/example_systems/8_three_zones_w_retrofit/system/Network.csv similarity index 100% rename from example_systems/8_three_zone_w_retrofit/system/Network.csv rename to example_systems/8_three_zones_w_retrofit/system/Network.csv From af7d1df96d9ac6e678b46d72760a64c618e3eb59 Mon Sep 17 00:00:00 2001 From: Yuheng Zhang <55777837+Betristor@users.noreply.github.com> Date: Mon, 25 Mar 2024 12:39:33 +0800 Subject: [PATCH 03/59] Add ObjScale setting to default_settings() function --- src/configure_settings/configure_settings.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/configure_settings/configure_settings.jl b/src/configure_settings/configure_settings.jl index 5d59a8a91b..8706c20b96 100644 --- a/src/configure_settings/configure_settings.jl +++ b/src/configure_settings/configure_settings.jl @@ -32,6 +32,7 @@ function default_settings() "ResourcePoliciesFolder" => "policy_assignments", "SystemFolder" => "system", "PoliciesFolder" => "policies", + "ObjScale" => 1, ) end @@ -66,7 +67,7 @@ end function validate_settings!(settings::Dict{Any,Any}) # Check for any settings combinations that are not allowed. # If we find any then make a response and issue a note to the user. - + # make WriteOutputs setting lowercase and check for valid value settings["WriteOutputs"] = lowercase(settings["WriteOutputs"]) @assert settings["WriteOutputs"] ∈ ["annual", "full"] @@ -144,7 +145,7 @@ function default_writeoutput() end function configure_writeoutput(output_settings_path::String, settings::Dict) - + writeoutput = default_writeoutput() # don't write files with hourly data if settings["WriteOutputs"] == "annual" From ecea54ce22b96911d51687c957bb5fd2a777f7ee Mon Sep 17 00:00:00 2001 From: Yuheng Zhang <55777837+Betristor@users.noreply.github.com> Date: Mon, 25 Mar 2024 12:40:02 +0800 Subject: [PATCH 04/59] Add objective function scaling in generate_model --- src/model/generate_model.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/model/generate_model.jl b/src/model/generate_model.jl index b834773188..677fc7b03d 100644 --- a/src/model/generate_model.jl +++ b/src/model/generate_model.jl @@ -125,7 +125,7 @@ function generate_model(setup::Dict,inputs::Dict,OPTIMIZER::MOI.OptimizerWithAtt fuel!(EP, inputs, setup) - co2!(EP, inputs) + co2!(EP, inputs) if setup["OperationalReserves"] > 0 operational_reserves!(EP, inputs, setup) @@ -225,7 +225,7 @@ function generate_model(setup::Dict,inputs::Dict,OPTIMIZER::MOI.OptimizerWithAtt end ## Define the objective function - @objective(EP,Min,EP[:eObj]) + @objective(EP,Min, setup["ObjScale"] * EP[:eObj]) ## Power balance constraints # demand = generation + storage discharge - storage charge - demand deferral + deferred demand satisfaction - demand curtailment (NSE) From e6eab255e940795bdaf1bf369871443b77f9e187 Mon Sep 17 00:00:00 2001 From: Yuheng Zhang <55777837+Betristor@users.noreply.github.com> Date: Mon, 25 Mar 2024 12:55:36 +0800 Subject: [PATCH 05/59] Add objective scaler and update changelog --- CHANGELOG.md | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0fdf495d9..b891691011 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,15 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added +- Add objective scaler (#667) + ## [0.4.0] - 2024-03-18 ### Added - Feature CO2 and fuel module (#536) - Adds a fuel module which enables modeling of fuel usage via (1) a constant heat rate and (2) - piecewise-linear approximation of heat rate curves. - Adds a CO2 module that determines the CO2 emissions based on fuel consumption, CO2 capture + Adds a fuel module which enables modeling of fuel usage via (1) a constant heat rate and (2) + piecewise-linear approximation of heat rate curves. + Adds a CO2 module that determines the CO2 emissions based on fuel consumption, CO2 capture fraction, and whether the feedstock is biomass. -- Enable thermal power plants to burn multiple fuels (#586) +- Enable thermal power plants to burn multiple fuels (#586) - Feature electrolysis basic (#525) Adds hydrogen electrolyzer model which enables the addition of hydrogen electrolyzer demands along with optional clean supply constraints. @@ -25,14 +28,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add PR template (#516) - Validation ensures that resource flags (THERM, HYDRO, LDS etc) are self-consistent (#513). - Maintenance formulation for thermal-commit plants (#556). -- Add new tests for GenX: three-zone, multi-stage, electrolyzer, VRE+storage, +- Add new tests for GenX: three-zone, multi-stage, electrolyzer, VRE+storage, piecewise_fuel+CO2, and TDR (#563 and #578). - Added a DC OPF method (#543) to calculate power flows across all lines - Added write_operating_reserve_price_revenue.jl to compute annual operating reserve and regulation revenue. Added the operating reserve and regulation revenue to net revenue (PR # 611) - Add functions to compute conflicting constraints when model is infeasible if supported by the solver (#624). -- New settings parameter, VirtualChargeDischargeCost to test script and VREStor example case. The PR 608 attempts to - introduce this parameter as cost of virtual charging and discharging to avoid unusual results (#608). +- New settings parameter, VirtualChargeDischargeCost to test script and VREStor example case. The PR 608 attempts to + introduce this parameter as cost of virtual charging and discharging to avoid unusual results (#608). - New settings parameter, StorageVirtualDischarge, to turn storage virtual charging and discharging off if desired by the user (#638). - Add module to retrofit existing resources with new technologies (#600). @@ -73,14 +76,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 This mitigates but does not fully fix (#576). - Expressions of virtual charging and discharging costs in storage_all.jl and vre_stor.jl - The input file `Generators_data.csv` has been split into different files, one for each type of generator. - The new files are: `Thermal.csv`, `Hydro.csv`, `Vre.csv`, `Storage.csv`, `Flex_demand.csv`, `Must_run.csv`, - `Electrolyzer.csv`, and `Vre_stor.csv`. The examples have been updated, and new tests have been added to + The new files are: `Thermal.csv`, `Hydro.csv`, `Vre.csv`, `Storage.csv`, `Flex_demand.csv`, `Must_run.csv`, + `Electrolyzer.csv`, and `Vre_stor.csv`. The examples have been updated, and new tests have been added to check the new data format (#612). -- The settings parameter `Reserves` has been renamed to `OperationalReserves`, `Reserves.csv` to +- The settings parameter `Reserves` has been renamed to `OperationalReserves`, `Reserves.csv` to `Operational_reserves.csv`, and the `.jl` files contain the word `reserves` have been renamed to `operational_reserves` (#641). -- New folder structure for a GenX case. The input files are now organized in the following folders: `settings`, - `policies`, `resources` and `system`. The examples and tests have been updated to reflect this change. +- New folder structure for a GenX case. The input files are now organized in the following folders: `settings`, + `policies`, `resources` and `system`. The examples and tests have been updated to reflect this change. ### Deprecated - The above `load` keys, which generally refer to electrical demand, are being deprecated. @@ -135,7 +138,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed -- The settings key `OperationsWrapping`. Its functionality has now been folded into the +- The settings key `OperationsWrapping`. Its functionality has now been folded into the `TimeDomainReduction` setting. Using the key now will print a gentle warning (#426). ## [0.3.4] - 2023-04-28 From 3b2fd2d9c17eafea4ef1b6f13b89e18e4afd7e2f Mon Sep 17 00:00:00 2001 From: Yuheng Zhang <55777837+Betristor@users.noreply.github.com> Date: Mon, 25 Mar 2024 12:58:19 +0800 Subject: [PATCH 06/59] Fix formatting in objective function documentation --- docs/src/Model_Concept_Overview/objective_function.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/src/Model_Concept_Overview/objective_function.md b/docs/src/Model_Concept_Overview/objective_function.md index 94308431f8..53a8a29ed1 100644 --- a/docs/src/Model_Concept_Overview/objective_function.md +++ b/docs/src/Model_Concept_Overview/objective_function.md @@ -32,7 +32,7 @@ The objective function of GenX minimizes total annual electricity system costs o & \sum_{y \in \mathcal{VS}^{sym,dc} \cup \mathcal{VS}^{asym,dc,dis}} \sum_{z \in \mathcal{Z}} \sum_{t \in \mathcal{T}} \left( \omega_{t}\times\pi^{VOM,dc,dis}_{y,z} \times\eta^{inverter}_{y,z} \times \Theta^{dc}_{y,z,t}\right) + \\ & \sum_{y \in \mathcal{VS}^{sym,dc} \cup \mathcal{VS}^{asym,dc,cha}} \sum_{z \in \mathcal{Z}} \sum_{t \in \mathcal{T}} \left( \omega_{t}\times\pi^{VOM,dc,cha}_{y,z} \times \frac{\Pi^{dc}_{y,z,t}}{\eta^{inverter}_{y,z}}\right) + \\ & \sum_{y \in \mathcal{VS}^{sym,ac} \cup \mathcal{VS}^{asym,ac,dis}} \sum_{z \in \mathcal{Z}} \sum_{t \in \mathcal{T}} \left( \omega_{t}\times\pi^{VOM,ac,dis}_{y,z} \times \Theta^{ac}_{y,z,t}\right) + \\ - & \sum_{y \in \mathcal{VS}^{sym,ac} \cup \mathcal{VS}^{asym,ac,cha}} \sum_{z \in \mathcal{Z}} \sum_{t \in \mathcal{T}} \left( \omega_{t}\times\pi^{VOM,ac,cha}_{y,z} \times \Pi^{ac}_{y,z,t}\right) + & \sum_{y \in \mathcal{VS}^{sym,ac} \cup \mathcal{VS}^{asym,ac,cha}} \sum_{z \in \mathcal{Z}} \sum_{t \in \mathcal{T}} \left( \omega_{t}\times\pi^{VOM,ac,cha}_{y,z} \times \Pi^{ac}_{y,z,t}\right) \end{aligned} ``` @@ -80,7 +80,7 @@ The eighteenth summation represents the variable O&M cost, $\pi^{VOM,wind}_{y,z} The nineteenth summation represents the variable O&M cost, $\pi^{VOM,dc,dis}_{y,z}$, times the energy discharge by storage DC components ($y\in\mathcal{VS}^{sym,dc} \cup \mathcal{VS}^{asym,dc,dis}$) in time step $t$, $\Theta^{dc}_{y,z,t}$, the inverter efficiency, $\eta^{inverter}_{y,z}$, and the weight of each time step $t$, $\omega_t$. The twentieth summation represents the variable O&M cost, $\pi^{VOM,dc,cha}_{y,z}$, times the energy withdrawn by storage DC components ($y\in\mathcal{VS}^{sym,dc} \cup \mathcal{VS}^{asym,dc,cha}$) in time step $t$, $\Pi^{dc}_{y,z,t}$, and the weight of each time step $t$, $\omega_t$, and divided by the inverter efficiency, $\eta^{inverter}_{y,z}$. The twenty-first summation represents the variable O&M cost, $\pi^{VOM,ac,dis}_{y,z}$, times the energy discharge by storage AC components ($y\in\mathcal{VS}^{sym,ac} \cup \mathcal{VS}^{asym,ac,dis}$) in time step $t$, $\Theta^{ac}_{y,z,t}$, and the weight of each time step $t$, $\omega_t$. -The twenty-second summation represents the variable O&M cost, $\pi^{VOM,ac,cha}_{y,z}$, times the energy withdrawn by storage AC components ($y\in\mathcal{VS}^{sym,ac} \cup \mathcal{VS}^{asym,ac,cha}$) in time step $t$, $\Pi^{ac}_{y,z,t}$, and the weight of each time step $t$, $\omega_t$. +The twenty-second summation represents the variable O&M cost, $\pi^{VOM,ac,cha}_{y,z}$, times the energy withdrawn by storage AC components ($y\in\mathcal{VS}^{sym,ac} \cup \mathcal{VS}^{asym,ac,cha}$) in time step $t$, $\Pi^{ac}_{y,z,t}$, and the weight of each time step $t$, $\omega_t$. In summary, the objective function can be understood as the minimization of costs associated with five sets of different decisions: 1. where and how to invest on capacity, @@ -92,3 +92,6 @@ In summary, the objective function can be understood as the minimization of cost Note however that each of these components are considered jointly and the optimization is performed over the whole problem at once as a monolithic co-optimization problem. While the objective function is formulated as a cost minimization problem, it is also equivalent to a social welfare maximization problem, with the bulk of demand treated as inelastic and always served, and the utility of consumption for price-elastic consumers represented as a segment-wise approximation, as per the cost of unserved demand summation above. + +# Objective Scaling +Sometimes the model will be built into an ill form if some objective terms are quite large or small. To alleviate this problem, we could add a scaling factor to scale the objective function during solving while leaving all other expressions untouched. The default ```ObjScale``` is set to 1 which has no effect on objective. If you want to scale the objective, you can set the ```ObjScale``` to an appropriate value in the ```genx_settings.yml```. The objective function will be multiplied by the ```ObjScale``` value during the solving process. \ No newline at end of file From 44b053e0ed7d4e8c16d3b10420d07dd188440966 Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Mon, 25 Mar 2024 11:22:05 -0400 Subject: [PATCH 07/59] Update investment_transmission.jl doc string rendering fixed for investment_transmission by adding ```math tag --- src/model/core/transmission/investment_transmission.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/model/core/transmission/investment_transmission.jl b/src/model/core/transmission/investment_transmission.jl index 671d7e47cd..685d5d8046 100644 --- a/src/model/core/transmission/investment_transmission.jl +++ b/src/model/core/transmission/investment_transmission.jl @@ -9,15 +9,19 @@ Note that fixed O\&M and replacement capital costs (depreciation) for existing transmission capacity is treated as a sunk cost and not included explicitly in the GenX objective function. **Accounting for Transmission Between Zones** Available transmission capacity between zones is set equal to the existing line's maximum power transfer capacity, $\overline{\varphi^{cap}_{l}}$, plus any transmission capacity added on that line (for lines eligible for expansion in the set $\mathcal{E}$). + ```math \begin{aligned} &\varphi^{cap}_{l} = \overline{\varphi^{cap}_{l}} , &\quad \forall l \in (\mathcal{L} \setminus \mathcal{E} ),\forall t \in \mathcal{T}\\ % trasmission expansion &\varphi^{cap}_{l} = \overline{\varphi^{cap}_{l}} + \bigtriangleup\varphi^{cap}_{l} , &\quad \forall l \in \mathcal{E},\forall t \in \mathcal{T} \end{aligned} + ``` The additional transmission capacity, $\bigtriangleup\varphi^{cap}_{l} $, is constrained by a maximum allowed reinforcement, $\overline{\bigtriangleup\varphi^{cap}_{l}}$, for each line $l \in \mathcal{E}$. + ```math \begin{aligned} & \bigtriangleup\varphi^{cap}_{l} \leq \overline{\bigtriangleup\varphi^{cap}_{l}}, &\quad \forall l \in \mathcal{E} \end{aligned} + ``` """ function investment_transmission!(EP::Model, inputs::Dict, setup::Dict) From ef8374ae0c8fd5fc03391f1cf3b996fd1dfec64d Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Mon, 25 Mar 2024 11:36:54 -0400 Subject: [PATCH 08/59] Update .zenodo.json Updated author list with the name of Justin Zhou from MIT --- .zenodo.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.zenodo.json b/.zenodo.json index 0b4d814792..facc5c3cac 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -87,6 +87,10 @@ "name": "Xu, Qingyu", "affiliation": "Tsinghua University", "orcid": "0000-0003-2692-5135" + }, + { + "name": "Zhou, Justin", + "affiliation": "Massachusetts Institute of Technology" } ] } From 26d9dbdbbdd4a9ce22ae3876df4e3ea660d31813 Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Mon, 25 Mar 2024 11:40:44 -0400 Subject: [PATCH 09/59] Update Project.toml Updated Project.toml with the name of Justin Zhou from MIT --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 9694c26432..6a092d4996 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "GenX" uuid = "5d317b1e-30ec-4ed6-a8ce-8d2d88d7cfac" -authors = ["Bonaldo, Luca", "Chakrabarti, Sambuddha", "Cheng, Fangwei", "Ding, Yifu", "Jenkins, Jesse D.", "Luo, Qian", "Macdonald, Ruaridh", "Mallapragada, Dharik", "Manocha, Aneesha", "Mantegna, Gabe ", "Morris, Jack", "Patankar, Neha", "Pecci, Filippo", "Schwartz, Aaron", "Schwartz, Jacob", "Schivley, Greg", "Sepulveda, Nestor", "Xu, Qingyu"] +authors = ["Bonaldo, Luca", "Chakrabarti, Sambuddha", "Cheng, Fangwei", "Ding, Yifu", "Jenkins, Jesse D.", "Luo, Qian", "Macdonald, Ruaridh", "Mallapragada, Dharik", "Manocha, Aneesha", "Mantegna, Gabe ", "Morris, Jack", "Patankar, Neha", "Pecci, Filippo", "Schwartz, Aaron", "Schwartz, Jacob", "Schivley, Greg", "Sepulveda, Nestor", "Xu, Qingyu", "Zhou, Justin"] version = "0.4.0" [deps] From 0a6c2c698c7ae5a57a22d0a346d910fedd136396 Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Mon, 25 Mar 2024 14:38:07 -0400 Subject: [PATCH 10/59] Changed the doc URL in the README to the current correct one --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b52efc7960..b24402102e 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ There are two ways to run GenX with either type of solver options (open-source f ## Documentation -Detailed documentation for GenX can be found [here](https://genxproject.github.io/GenX/dev). +Detailed documentation for GenX can be found [here](https://genxproject.github.io/GenX.jl/dev). It includes details of each of GenX's methods, required and optional input files, and outputs. Interested users may also want to browse through [prior publications](https://energy.mit.edu/genx/#publications) that have used GenX to understand the various features of the tool. From ef1d0316226c08e3680c3d3a616dbdc0e78e3f4a Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Mon, 25 Mar 2024 15:07:45 -0400 Subject: [PATCH 11/59] HiGHS solver parameter list shortened to most frequently used/modified ones --- src/configure_solver/configure_highs.jl | 337 ------------------------ 1 file changed, 337 deletions(-) diff --git a/src/configure_solver/configure_highs.jl b/src/configure_solver/configure_highs.jl index 3f5e04d811..0c8b7cb712 100644 --- a/src/configure_solver/configure_highs.jl +++ b/src/configure_solver/configure_highs.jl @@ -60,276 +60,6 @@ The HiGHS optimizer instance is configured with the following default parameters # number of threads used by HiGHS (0: automatic) # [type: HighsInt, advanced: false, range: {0, 2147483647}, default: 0] threads: 0 - - # Debugging level in HiGHS - # [type: HighsInt, advanced: false, range: {0, 3}, default: 0] - highs_debug_level: 0 - - # Analysis level in HiGHS - # [type: HighsInt, advanced: false, range: {0, 63}, default: 0] - highs_analysis_level: 0 - - # Strategy for simplex solver 0 => Choose; 1 => Dual (serial); 2 => Dual (PAMI); 3 => Dual (SIP); 4 => Primal - # [type: HighsInt, advanced: false, range: {0, 4}, default: 1] - simplex_strategy: 1 - - # Simplex scaling strategy: off / choose / equilibration / forced equilibration / max value 0 / max value 1 (0/1/2/3/4/5) - # [type: HighsInt, advanced: false, range: {0, 5}, default: 1] - simplex_scale_strategy: 1 - - # Strategy for simplex crash: off / LTSSF / Bixby (0/1/2) - # [type: HighsInt, advanced: false, range: {0, 9}, default: 0] - simplex_crash_strategy: 0 - - # Strategy for simplex dual edge weights: Choose / Dantzig / Devex / Steepest Edge (-1/0/1/2) - # [type: HighsInt, advanced: false, range: {-1, 2}, default: -1] - simplex_dual_edge_weight_strategy: -1 - - # Strategy for simplex primal edge weights: Choose / Dantzig / Devex / Steepest Edge (-1/0/1/2) - # [type: HighsInt, advanced: false, range: {-1, 2}, default: -1] - simplex_primal_edge_weight_strategy: -1 - - # Iteration limit for simplex solver - # [type: HighsInt, advanced: false, range: {0, 2147483647}, default: 2147483647] - simplex_iteration_limit: 2147483647 - - # Limit on the number of simplex UPDATE operations - # [type: HighsInt, advanced: false, range: {0, 2147483647}, default: 5000] - simplex_update_limit: 5000 - - # Iteration limit for IPM solver - # [type: HighsInt, advanced: false, range: {0, 2147483647}, default: 2147483647] - ipm_iteration_limit: 2147483647 - - # Minimum level of concurrency in parallel simplex - # [type: HighsInt, advanced: false, range: {1, 8}, default: 1] - simplex_min_concurrency: 1 - - # Maximum level of concurrency in parallel simplex - # [type: HighsInt, advanced: false, range: {1, 8}, default: 8] - simplex_max_concurrency: 8 - - # Enables or disables solver output - # [type: bool, advanced: false, range: {false, true}, default: true] - output_flag: true - - # Enables or disables console logging - # [type: bool, advanced: false, range: {false, true}, default: true] - log_to_console: true - - # Solution file - # [type: string, advanced: false, default: ""] - solution_file: "" - - # Log file - # [type: string, advanced: false, default: ""] - log_file: "" - - # Write the primal and dual solution to a file - # [type: bool, advanced: false, range: {false, true}, default: false] - write_solution_to_file: false - - # Write the solution in style: 0=>Raw (computer-readable); 1=>Pretty (human-readable) - # [type: HighsInt, advanced: false, range: {0, 2}, default: 0] - write_solution_style: 0 - - # Write model file - # [type: string, advanced: false, default: ""] - write_model_file: "" - - # Write the model to a file - # [type: bool, advanced: false, range: {false, true}, default: false] - write_model_to_file: false - - # Whether symmetry should be detected - # [type: bool, advanced: false, range: {false, true}, default: true] - mip_detect_symmetry: true - - # MIP solver max number of nodes - # [type: HighsInt, advanced: false, range: {0, 2147483647}, default: 2147483647] - mip_max_nodes: 2147483647 - - # MIP solver max number of nodes where estimate is above cutoff bound - # [type: HighsInt, advanced: false, range: {0, 2147483647}, default: 2147483647] - mip_max_stall_nodes: 2147483647 - - # MIP solver max number of leave nodes - # [type: HighsInt, advanced: false, range: {0, 2147483647}, default: 2147483647] - mip_max_leaves: 2147483647 - - # limit on the number of improving solutions found to stop the MIP solver prematurely - # [type: HighsInt, advanced: false, range: {1, 2147483647}, default: 2147483647] - mip_max_improving_sols: 2147483647 - - # maximal age of dynamic LP rows before they are removed from the LP relaxation - # [type: HighsInt, advanced: false, range: {0, 32767}, default: 10] - mip_lp_age_limit: 10 - - # maximal age of rows in the cutpool before they are deleted - # [type: HighsInt, advanced: false, range: {0, 1000}, default: 30] - mip_pool_age_limit: 30 - - # soft limit on the number of rows in the cutpool for dynamic age adjustment - # [type: HighsInt, advanced: false, range: {1, 2147483647}, default: 10000] - mip_pool_soft_limit: 10000 - - # minimal number of observations before pseudo costs are considered reliable - # [type: HighsInt, advanced: false, range: {0, 2147483647}, default: 8] - mip_pscost_minreliable: 8 - - # minimal number of entries in the cliquetable before neighborhood queries of the conflict graph use parallel processing - # [type: HighsInt, advanced: false, range: {0, 2147483647}, default: 100000] - mip_min_cliquetable_entries_for_parallelism: 100000 - - # MIP solver reporting level - # [type: HighsInt, advanced: false, range: {0, 2}, default: 1] - mip_report_level: 1 - - # MIP feasibility tolerance - # [type: double, advanced: false, range: [1e-10, inf], default: 1e-06] - mip_feasibility_tolerance: 1e-06 - - # effort spent for MIP heuristics - # [type: double, advanced: false, range: [0, 1], default: 0.05] - mip_heuristic_effort: 0.05 - - # tolerance on relative gap, |ub-lb|/|ub|, to determine whether optimality has been reached for a MIP instance - # [type: double, advanced: false, range: [0, inf], default: 0.0001] - mip_rel_gap: 0.0001 - - # tolerance on absolute gap of MIP, |ub-lb|, to determine whether optimality has been reached for a MIP instance - # [type: double, advanced: false, range: [0, inf], default: 1e-06] - mip_abs_gap: 1e-06 - - # Output development messages: 0 => none; 1 => info; 2 => verbose - # [type: HighsInt, advanced: true, range: {0, 3}, default: 0] - log_dev_level: 0 - - # Run the crossover routine for IPX - # [type: string, advanced: "on", range: {"off", "on"}, default: "off"] - run_crossover: "off" - - # Allow ModelStatus::kUnboundedOrInfeasible - # [type: bool, advanced: true, range: {false, true}, default: false] - allow_unbounded_or_infeasible: false - - # Use relaxed implied bounds from presolve - # [type: bool, advanced: true, range: {false, true}, default: false] - use_implied_bounds_from_presolve: false - - # Prevents LP presolve steps for which postsolve cannot maintain a basis - # [type: bool, advanced: true, range: {false, true}, default: true] - lp_presolve_requires_basis_postsolve: true - - # Use the free format MPS file reader - # [type: bool, advanced: true, range: {false, true}, default: true] - mps_parser_type_free: true - - # For multiple N-rows in MPS files: delete rows / delete entries / keep rows (-1/0/1) - # [type: HighsInt, advanced: true, range: {-1, 1}, default: -1] - keep_n_rows: -1 - - # Scaling factor for costs - # [type: HighsInt, advanced: true, range: {-20, 20}, default: 0] - cost_scale_factor: 0 - - # Largest power-of-two factor permitted when scaling the constraint matrix - # [type: HighsInt, advanced: true, range: {0, 30}, default: 20] - allowed_matrix_scale_factor: 20 - - # Largest power-of-two factor permitted when scaling the costs - # [type: HighsInt, advanced: true, range: {0, 20}, default: 0] - allowed_cost_scale_factor: 0 - - # Strategy for permuting before simplex - # [type: HighsInt, advanced: true, range: {-1, 1}, default: -1] - simplex_permute_strategy: -1 - - # Max level of dual simplex cleanup - # [type: HighsInt, advanced: true, range: {0, 2147483647}, default: 1] - max_dual_simplex_cleanup_level: 1 - - # Max level of dual simplex phase 1 cleanup - # [type: HighsInt, advanced: true, range: {0, 2147483647}, default: 2] - max_dual_simplex_phase1_cleanup_level: 2 - - # Strategy for PRICE in simplex - # [type: HighsInt, advanced: true, range: {0, 3}, default: 3] - simplex_price_strategy: 3 - - Strategy for solving unscaled LP in simplex - [type: HighsInt, advanced: true, range: {0, 2}, default: 1] - simplex_unscaled_solution_strategy: 1 - - Perform initial basis condition check in simplex - [type: bool, advanced: true, range: {false, true}, default: true] - simplex_initial_condition_check: true - - No unnecessary refactorization on simplex rebuild - [type: bool, advanced: true, range: {false, true}, default: true] - no_unnecessary_rebuild_refactor: true - - Tolerance on initial basis condition in simplex - [type: double, advanced: true, range: [1, inf], default: 1e+14] - simplex_initial_condition_tolerance: 1e+14 - - Tolerance on solution error when considering refactorization on simplex rebuild - [type: double, advanced: true, range: [-inf, inf], default: 1e-08] - rebuild_refactor_solution_error_tolerance: 1e-08 - - Tolerance on dual steepest edge weight errors - [type: double, advanced: true, range: [0, inf], default: inf] - dual_steepest_edge_weight_error_tolerance: Inf - - Threshold on dual steepest edge weight errors for Devex switch - [type: double, advanced: true, range: [1, inf], default: 10] - dual_steepest_edge_weight_log_error_threshold: 10.0 - - Dual simplex cost perturbation multiplier: 0 => no perturbation - [type: double, advanced: true, range: [0, inf], default: 1] - dual_simplex_cost_perturbation_multiplier: 1.0 - - Primal simplex bound perturbation multiplier: 0 => no perturbation - [type: double, advanced: true, range: [0, inf], default: 1] - primal_simplex_bound_perturbation_multiplier: 1.0 - - Dual simplex pivot growth tolerance - [type: double, advanced: true, range: [1e-12, inf], default: 1e-09] - dual_simplex_pivot_growth_tolerance: 1e-09 - - Matrix factorization pivot threshold for substitutions in presolve - [type: double, advanced: true, range: [0.0008, 0.5], default: 0.01] - presolve_pivot_threshold: 0.01 - - Maximal fillin allowed for substitutions in presolve - [type: HighsInt, advanced: true, range: {0, 2147483647}, default: 10] - presolve_substitution_maxfillin: 10 - - Matrix factorization pivot threshold - [type: double, advanced: true, range: [0.0008, 0.5], default: 0.1] - factor_pivot_threshold: 0.1 - - Matrix factorization pivot tolerance - [type: double, advanced: true, range: [0, 1], default: 1e-10] - factor_pivot_tolerance: 1e-10 - - Tolerance to be satisfied before IPM crossover will start - [type: double, advanced: true, range: [1e-12, inf], default: 1e-08] - start_crossover_tolerance: 1e-08 - - Use original HFactor logic for sparse vs hyper-sparse TRANs - [type: bool, advanced: true, range: {false, true}, default: true] - use_original_HFactor_logic: true - - Check whether LP is candidate for LiDSE - [type: bool, advanced: true, range: {false, true}, default: true] - less_infeasible_DSE_check: true - - Use LiDSE if LP has right properties - [type: bool, advanced: true, range: {false, true}, default: true] - less_infeasible_DSE_choose_row: true - - """ function configure_highs(solver_settings_path::String, optimizer::Any) @@ -353,73 +83,6 @@ function configure_highs(solver_settings_path::String, optimizer::Any) "objective_target" => -Inf, "random_seed" => 0, "threads" => 0, - "highs_debug_level" => 0, - "highs_analysis_level" => 0, - "simplex_strategy" => 1, - "simplex_scale_strategy" => 1, - "simplex_crash_strategy" => 0, - "simplex_dual_edge_weight_strategy" => -1, - "simplex_primal_edge_weight_strategy" => -1, - "simplex_iteration_limit" => 2147483647, - "simplex_update_limit" => 5000, - "ipm_iteration_limit" => 2147483647, - "simplex_min_concurrency" => 1, - "simplex_max_concurrency" => 8, - "output_flag" => true, - "log_to_console" => true, - "solution_file" => "", - "log_file" => "", - "write_solution_to_file" => false, - "write_solution_style" => 0, - "write_model_file" => "", - "write_model_to_file" => false, - "mip_detect_symmetry" => true, - "mip_max_nodes" => 2147483647, - "mip_max_stall_nodes" => 2147483647, - "mip_max_leaves" => 2147483647, - "mip_max_improving_sols" => 2147483647, - "mip_lp_age_limit" => 10, - "mip_pool_age_limit" => 30, - "mip_pool_soft_limit" => 10000, - "mip_pscost_minreliable" => 8, - "mip_min_cliquetable_entries_for_parallelism" => 100000, - "mip_report_level" => 1, - "mip_feasibility_tolerance" => 1e-06, - "mip_heuristic_effort" => 0.05, - "mip_rel_gap" => 0.001, - "mip_abs_gap" => 1e-06, - "log_dev_level" => 0, - "run_crossover" => "off", - "allow_unbounded_or_infeasible" => false, - "use_implied_bounds_from_presolve" => false, - "lp_presolve_requires_basis_postsolve" => true, - "mps_parser_type_free" => true, - "keep_n_rows" => -1, - "cost_scale_factor" => 0, - "allowed_matrix_scale_factor" => 20, - "allowed_cost_scale_factor" => 0, - "simplex_permute_strategy" => -1, - "max_dual_simplex_cleanup_level" => 1, - "max_dual_simplex_phase1_cleanup_level" => 2, - "simplex_price_strategy" => 3, - "simplex_unscaled_solution_strategy" => 1, - "simplex_initial_condition_check" => true, - "no_unnecessary_rebuild_refactor" => true, - "simplex_initial_condition_tolerance" => 1e+14, - "rebuild_refactor_solution_error_tolerance" => 1e-08, - "dual_steepest_edge_weight_error_tolerance" => Inf, - "dual_steepest_edge_weight_log_error_threshold" => 10.0, - "dual_simplex_cost_perturbation_multiplier" => 1.0, - "primal_simplex_bound_perturbation_multiplier" => 1.0, - "dual_simplex_pivot_growth_tolerance" => 1e-09, - "presolve_pivot_threshold" => 0.01, - "presolve_substitution_maxfillin" => 10, - "factor_pivot_threshold" => 0.1, - "factor_pivot_tolerance" => 1e-10, - "start_crossover_tolerance" => 1e-08, - "use_original_HFactor_logic" => true, - "less_infeasible_DSE_check" => true, - "less_infeasible_DSE_choose_row" => true, ) attributes = merge(default_settings, solver_settings) From df2dc9aab4c0518e4b1227478ddd6265bbe88a35 Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Mon, 25 Mar 2024 15:45:45 -0400 Subject: [PATCH 12/59] Update CITATION.cff With revised names and ORCID IDs of authors and significant contributors (in alphabetic order of last names), the DOI of Zenodo GitHub page for GenX, and version number (v0.4.0) --- CITATION.cff | 56 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 648457d1ef..752cd4a74e 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,30 +1,60 @@ cff-version: 1.2.0 message: "If you use this software, please cite it as below." authors: +- family-names: "Bonaldo" + given-names: "Luca" + orcid: "https://orcid.org/0009-0000-0650-0266" +- family-names: "Chakrabarti" + given-names: "Sambuddha" + orcid: "https://orcid.org/0000-0002-8916-5076" +- family-names: "Cheng" + given-names: "Fangwei" + orcid: "https://orcid.org/0000-0001-6589-2749" +- family-names: "Ding" + given-names: "Yifu" + orcid: "https://orcid.org/0000-0001-7128-8847" - family-names: "Jenkins" given-names: "Jesse" -- family-names: "Sepulveda" - given-names: "Nestor" + orcid: "https://orcid.org/0000-0002-9670-7793" +- family-names: "Luo" + given-names: "Qian" + orcid: "https://orcid.org/0000-0003-3894-4093" +- family-names: "Macdonald" + given-names: "Ruaridh" + orcid: "https://orcid.org/0000-0001-9034-6635" - family-names: "Mallapragada" given-names: "Dharik" + orcid: "https://orcid.org/0000-0002-0330-0063" +- family-names: "Manocha" + given-names: "Aneesha" + orcid: "https://orcid.org/0000-0002-7190-4782" +- family-names: "Mantegna" + given-names: "Gabe" + orcid: "https://orcid.org/0000-0002-7707-0221" +- family-names: "Morris" + given-names: "Jack" - family-names: "Patankar" given-names: "Neha" + orcid: "https://orcid.org/0000-0001-7288-0391" +- family-names: "Pecci" + given-names: "Filippo" + orcid: "https://orcid.org/0000-0003-3200-0892" - family-names: "Schwartz" given-names: "Aaron" - family-names: "Schwartz" given-names: "Jacob" orcid: "https://orcid.org/0000-0001-9636-8181" -- family-names: "Chakrabarti" - given-names: "Sambuddha" - orcid: "https://orcid.org/0000-0002-8916-5076" -- family-names: "Xu" - given-names: "Qingyu" -- family-names: "Morris" - given-names: "Jack" +- family-names: "Schivley" + given-names: "Greg" + orcid: "https://orcid.org/0000-0002-8947-694X" - family-names: "Sepulveda" given-names: "Nestor" + orcid: "https://orcid.org/0000-0003-2735-8769" +- family-names: "Xu" + given-names: "Qingyu" + orcid: "https://orcid.org/0000-0003-2692-5135" title: "GenX" -version: 0.3.0 -doi: 10.5281/zenodo.6229425 -date-released: 2022-04-26 -url: "https://github.com/GenXProject/GenX" +version: 0.4.0 +doi: 10.5281/zenodo.10846070 +date-released: 2024-04-26 +url: "https://github.com/GenXProject/GenX.jl" From 5fb1f2c58c7e85b7afc02cdc5470c5cf04a7f135 Mon Sep 17 00:00:00 2001 From: lbonaldo Date: Mon, 1 Apr 2024 12:45:00 -0400 Subject: [PATCH 13/59] Fix initialize_cost_to_go docstring --- src/multi_stage/dual_dynamic_programming.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/multi_stage/dual_dynamic_programming.jl b/src/multi_stage/dual_dynamic_programming.jl index d2ff098952..bb9f14df68 100644 --- a/src/multi_stage/dual_dynamic_programming.jl +++ b/src/multi_stage/dual_dynamic_programming.jl @@ -577,10 +577,10 @@ The updated objective function $OBJ^{*}$ returned by this method takes the form: where $OBJ$ is the original objective function. $OBJ$ is scaled by two terms. The first is a discount factor (applied only in the non-myopic case), which discounts costs associated with the model stage $p$ to year-0 dollars: ```math \begin{aligned} - DF = \frac{1}{(1+WACC)^{L*(p-1)}} + DF = \frac{1}{(1+WACC)^{L_{p}*(p-1)}} \end{aligned} ``` -where $WACC$ is the weighted average cost of capital, and $L$ is the length of each stage in years (both set in multi\_stage\_settings.yml) +where $WACC$ is the weighted average cost of capital, and $L_{p}$ is the length of each stage in years (both set in multi\_stage\_settings.yml) The second term is a discounted sum of annual operational expenses incurred each year of a multi-year model stage: ```math From ed24f3a8a6437656740fab7895d24406362957ea Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Tue, 2 Apr 2024 15:11:23 -0400 Subject: [PATCH 14/59] Update configure_highs.jl Gotten rid of further non-essential parameters --- src/configure_solver/configure_highs.jl | 31 ------------------------- 1 file changed, 31 deletions(-) diff --git a/src/configure_solver/configure_highs.jl b/src/configure_solver/configure_highs.jl index 0c8b7cb712..8796643302 100644 --- a/src/configure_solver/configure_highs.jl +++ b/src/configure_solver/configure_highs.jl @@ -16,15 +16,6 @@ The HiGHS optimizer instance is configured with the following default parameters Pre_Solve: choose # Presolve option: "off", "choose" or "on" # [type: string, advanced: false, default: "choose"] Method: ipm #choose #HiGHS-specific solver settings # Solver option: "simplex", "choose" or "ipm" # [type: string, advanced: false, default: "choose"] In order to run a case when the UCommit is set to 1, i.e. MILP instance, set the Method to choose - #HiGHS-specific solver settings - # Parallel option: "off", "choose" or "on" - # [type: string, advanced: false, default: "choose"] - parallel: choose - - # Compute cost, bound, RHS and basic solution ranging: "off" or "on" - # [type: string, advanced: false, default: "off"] - ranging: off - # Limit on cost coefficient: values larger than this will be treated as infinite # [type: double, advanced: false, range: [1e+15, inf], default: 1e+20] infinite_cost: 1e+20 @@ -33,14 +24,6 @@ The HiGHS optimizer instance is configured with the following default parameters # [type: double, advanced: false, range: [1e+15, inf], default: 1e+20] infinite_bound: 1e+20 - # Lower limit on |matrix entries|: values smaller than this will be treated as zero - # [type: double, advanced: false, range: [1e-12, inf], default: 1e-09] - small_matrix_value: 1e-09 - - # Upper limit on |matrix entries|: values larger than this will be treated as infinite - # [type: double, advanced: false, range: [1, inf], default: 1e+15] - large_matrix_value: 1e+15 - # IPM optimality tolerance # [type: double, advanced: false, range: [1e-12, inf], default: 1e-08] ipm_optimality_tolerance: 1e-08 @@ -52,14 +35,6 @@ The HiGHS optimizer instance is configured with the following default parameters # Objective target for termination # [type: double, advanced: false, range: [-inf, inf], default: -inf] objective_target: -Inf - - # random seed used in HiGHS - # [type: HighsInt, advanced: false, range: {0, 2147483647}, default: 0] - random_seed: 0 - - # number of threads used by HiGHS (0: automatic) - # [type: HighsInt, advanced: false, range: {0, 2147483647}, default: 0] - threads: 0 """ function configure_highs(solver_settings_path::String, optimizer::Any) @@ -72,17 +47,11 @@ function configure_highs(solver_settings_path::String, optimizer::Any) "Pre_Solve" => "choose", "TimeLimit" => Inf, "Method" => "ipm", - "parallel" => "choose", - "ranging" => "off", "infinite_cost" => 1e+20, "infinite_bound" => 1e+20, - "small_matrix_value" => 1e-09, - "large_matrix_value" => 1e+15, "ipm_optimality_tolerance" => 1e-08, "objective_bound" => Inf, "objective_target" => -Inf, - "random_seed" => 0, - "threads" => 0, ) attributes = merge(default_settings, solver_settings) From e5933dc3c2cb070a26fb0d08f7d2aa8a03d7a557 Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Tue, 2 Apr 2024 15:17:49 -0400 Subject: [PATCH 15/59] Update configure_highs.jl --- src/configure_solver/configure_highs.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/configure_solver/configure_highs.jl b/src/configure_solver/configure_highs.jl index 8796643302..e998e564f4 100644 --- a/src/configure_solver/configure_highs.jl +++ b/src/configure_solver/configure_highs.jl @@ -52,6 +52,7 @@ function configure_highs(solver_settings_path::String, optimizer::Any) "ipm_optimality_tolerance" => 1e-08, "objective_bound" => Inf, "objective_target" => -Inf, + "run_crossover" => "off", ) attributes = merge(default_settings, solver_settings) From b1007457650e09fb891f3ce3de341551fc5dd0fb Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Tue, 2 Apr 2024 15:20:13 -0400 Subject: [PATCH 16/59] Update configure_highs.jl Re-introduced crossover --- src/configure_solver/configure_highs.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/configure_solver/configure_highs.jl b/src/configure_solver/configure_highs.jl index e998e564f4..7e87e25470 100644 --- a/src/configure_solver/configure_highs.jl +++ b/src/configure_solver/configure_highs.jl @@ -35,6 +35,10 @@ The HiGHS optimizer instance is configured with the following default parameters # Objective target for termination # [type: double, advanced: false, range: [-inf, inf], default: -inf] objective_target: -Inf + + # Run the crossover routine for IPX + # [type: string, advanced: "on", range: {"off", "on"}, default: "off"] + run_crossover: "off" """ function configure_highs(solver_settings_path::String, optimizer::Any) From 825107055f3aff1b1d925adb9dbd908de9d4d922 Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Tue, 2 Apr 2024 16:14:33 -0400 Subject: [PATCH 17/59] Update configure_highs.jl Removed infinite bound, infinite cost, objective bound, objective target, small matrix and large matrix values. --- src/configure_solver/configure_highs.jl | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/configure_solver/configure_highs.jl b/src/configure_solver/configure_highs.jl index 7e87e25470..8a811fba20 100644 --- a/src/configure_solver/configure_highs.jl +++ b/src/configure_solver/configure_highs.jl @@ -16,25 +16,9 @@ The HiGHS optimizer instance is configured with the following default parameters Pre_Solve: choose # Presolve option: "off", "choose" or "on" # [type: string, advanced: false, default: "choose"] Method: ipm #choose #HiGHS-specific solver settings # Solver option: "simplex", "choose" or "ipm" # [type: string, advanced: false, default: "choose"] In order to run a case when the UCommit is set to 1, i.e. MILP instance, set the Method to choose - # Limit on cost coefficient: values larger than this will be treated as infinite - # [type: double, advanced: false, range: [1e+15, inf], default: 1e+20] - infinite_cost: 1e+20 - - # Limit on |constraint bound|: values larger than this will be treated as infinite - # [type: double, advanced: false, range: [1e+15, inf], default: 1e+20] - infinite_bound: 1e+20 - # IPM optimality tolerance # [type: double, advanced: false, range: [1e-12, inf], default: 1e-08] ipm_optimality_tolerance: 1e-08 - - # Objective bound for termination - # [type: double, advanced: false, range: [-inf, inf], default: inf] - objective_bound: Inf - - # Objective target for termination - # [type: double, advanced: false, range: [-inf, inf], default: -inf] - objective_target: -Inf # Run the crossover routine for IPX # [type: string, advanced: "on", range: {"off", "on"}, default: "off"] @@ -51,11 +35,7 @@ function configure_highs(solver_settings_path::String, optimizer::Any) "Pre_Solve" => "choose", "TimeLimit" => Inf, "Method" => "ipm", - "infinite_cost" => 1e+20, - "infinite_bound" => 1e+20, "ipm_optimality_tolerance" => 1e-08, - "objective_bound" => Inf, - "objective_target" => -Inf, "run_crossover" => "off", ) From f97e9ab2ae8c4148e623dc98db269dc55161e800 Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Tue, 2 Apr 2024 16:15:43 -0400 Subject: [PATCH 18/59] Update configure_highs.jl --- src/configure_solver/configure_highs.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/configure_solver/configure_highs.jl b/src/configure_solver/configure_highs.jl index 8a811fba20..232efc83ef 100644 --- a/src/configure_solver/configure_highs.jl +++ b/src/configure_solver/configure_highs.jl @@ -37,6 +37,8 @@ function configure_highs(solver_settings_path::String, optimizer::Any) "Method" => "ipm", "ipm_optimality_tolerance" => 1e-08, "run_crossover" => "off", + "mip_rel_gap" => 0.001, + "mip_abs_gap" => 1e-06, ) attributes = merge(default_settings, solver_settings) From ac1490fb767ce35ca5214d88153009a0c2299e09 Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Tue, 2 Apr 2024 16:17:02 -0400 Subject: [PATCH 19/59] Update configure_highs.jl Re-introduced MIP rel gap and MIP abs gap --- src/configure_solver/configure_highs.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/configure_solver/configure_highs.jl b/src/configure_solver/configure_highs.jl index 232efc83ef..d0bab8e835 100644 --- a/src/configure_solver/configure_highs.jl +++ b/src/configure_solver/configure_highs.jl @@ -23,6 +23,14 @@ The HiGHS optimizer instance is configured with the following default parameters # Run the crossover routine for IPX # [type: string, advanced: "on", range: {"off", "on"}, default: "off"] run_crossover: "off" + + # tolerance on relative gap, |ub-lb|/|ub|, to determine whether optimality has been reached for a MIP instance + # [type: double, advanced: false, range: [0, inf], default: 0.0001] + mip_rel_gap: 0.0001 + + # tolerance on absolute gap of MIP, |ub-lb|, to determine whether optimality has been reached for a MIP instance + # [type: double, advanced: false, range: [0, inf], default: 1e-06] + mip_abs_gap: 1e-06 """ function configure_highs(solver_settings_path::String, optimizer::Any) From bfa485c48d40a3aa0cf192f2685231987b41f050 Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Wed, 3 Apr 2024 11:41:09 -0400 Subject: [PATCH 20/59] Update CHANGELOG.md With modified scaler definition, mentioning that it addresses problem ill-conditioning --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b891691011..9c90d83dfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added -- Add objective scaler (#667) +- Add objective scaler for addressing problem ill-conditioning (#667) ## [0.4.0] - 2024-03-18 From 9d4b1451c8825c0f5e798e48244e3ff6b8579263 Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Wed, 3 Apr 2024 13:05:10 -0400 Subject: [PATCH 21/59] Update model_input.md Changed Generators_data.csv to Resources folder and associated description in GenX Inputs --- docs/src/User_Guide/model_input.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/User_Guide/model_input.md b/docs/src/User_Guide/model_input.md index af198ad780..c30a595dcf 100644 --- a/docs/src/User_Guide/model_input.md +++ b/docs/src/User_Guide/model_input.md @@ -1,12 +1,12 @@ # GenX Inputs -All input files are in CSV format. Running the GenX model requires a minimum of five **mandatory input files**: +All input files are in CSV format. Running the GenX model requires a minimum of four **mandatory input files** and one folder, which consists of CSV files for generating resources: 1. Fuels\_data.csv: specify fuel type, CO2 emissions intensity, and time-series of fuel prices. 2. Network.csv: specify network topology, transmission fixed costs, capacity and loss parameters. 3. Demand\_data.csv: specify time-series of demand profiles for each model zone, weights for each time step, demand shedding costs, and optional time domain reduction parameters. 4. Generators\_variability.csv: specify time-series of capacity factor/availability for each resource. -5. Generators\_data.csv: specify cost and performance data for generation, storage and demand flexibility resources. +5. Resources folder: specify cost and performance data for generation, storage and demand flexibility resources. Additionally, the user may need to specify eight more **settings-specific** input files based on model configuration and type of scenarios of interest: 1. Operational\_reserves.csv: specify operational reserve requirements as a function of demand and renewables generation and penalty for not meeting these requirements. From ee3f08756584ba16a57bb701492270a7bf129b4d Mon Sep 17 00:00:00 2001 From: Luca Bonaldo <39280783+lbonaldo@users.noreply.github.com> Date: Thu, 4 Apr 2024 10:33:40 -0400 Subject: [PATCH 22/59] Standardize code formatting across project (#673) * Format codebase and add format check to CI * Add style guide instructions to developer_guide --- .JuliaFormatter.toml | 1 + .github/workflows/format_suggestions.yml | 9 + CHANGELOG.md | 1 + README.md | 9 +- docs/make.jl | 51 +- docs/src/developer_guide.md | 13 + example_systems/1_three_zones/Run.jl | 2 +- .../2_three_zones_w_electrolyzer/Run.jl | 2 +- .../3_three_zones_w_co2_capture/Run.jl | 2 +- .../4_three_zones_w_policies_slack/Run.jl | 2 +- .../6_three_zones_w_multistage/Run.jl | 2 +- .../Run.jl | 2 +- example_systems/9_IEEE_9_bus_DC_OPF/Run.jl | 2 +- src/additional_tools/method_of_morris.jl | 172 +- .../modeling_to_generate_alternatives.jl | 130 +- src/case_runners/case_runner.jl | 27 +- src/configure_settings/configure_settings.jl | 24 +- src/configure_solver/configure_cbc.jl | 23 +- src/configure_solver/configure_clp.jl | 20 +- src/configure_solver/configure_cplex.jl | 49 +- src/configure_solver/configure_gurobi.jl | 35 +- src/configure_solver/configure_highs.jl | 24 +- src/configure_solver/configure_scip.jl | 13 +- src/configure_solver/configure_solver.jl | 12 +- src/load_inputs/load_cap_reserve_margin.jl | 4 +- src/load_inputs/load_co2_cap.jl | 9 +- src/load_inputs/load_dataframe.jl | 37 +- src/load_inputs/load_demand_data.jl | 61 +- .../load_energy_share_requirement.jl | 6 +- src/load_inputs/load_fuels_data.jl | 12 +- .../load_generators_variability.jl | 15 +- src/load_inputs/load_inputs.jl | 153 +- .../load_minimum_capacity_requirement.jl | 6 +- src/load_inputs/load_multistage_data.jl | 25 +- src/load_inputs/load_network_data.jl | 56 +- src/load_inputs/load_operational_reserves.jl | 71 +- src/load_inputs/load_period_map.jl | 16 +- src/load_inputs/load_resources_data.jl | 815 +++++---- src/load_inputs/load_vre_stor_variability.jl | 52 +- src/model/core/co2.jl | 72 +- src/model/core/discharge/discharge.jl | 54 +- .../core/discharge/investment_discharge.jl | 264 +-- src/model/core/fuel.jl | 197 ++- src/model/core/non_served_energy.jl | 79 +- src/model/core/operational_reserves.jl | 265 +-- .../core/transmission/dcopf_transmission.jl | 61 +- .../transmission/investment_transmission.jl | 167 +- src/model/core/transmission/transmission.jl | 384 ++-- src/model/core/ucommit.jl | 97 +- src/model/expression_manipulation.jl | 39 +- src/model/generate_model.jl | 339 ++-- src/model/policies/cap_reserve_margin.jl | 39 +- src/model/policies/co2_cap.jl | 105 +- .../policies/energy_share_requirement.jl | 29 +- .../policies/maximum_capacity_requirement.jl | 30 +- .../policies/minimum_capacity_requirement.jl | 31 +- .../curtailable_variable_renewable.jl | 107 +- .../flexible_demand/flexible_demand.jl | 156 +- .../hydro/hydro_inter_period_linkage.jl | 114 +- src/model/resources/hydro/hydro_res.jl | 208 ++- src/model/resources/hydrogen/electrolyzer.jl | 171 +- src/model/resources/maintenance.jl | 54 +- src/model/resources/must_run/must_run.jl | 55 +- src/model/resources/resources.jl | 547 +++--- src/model/resources/retrofits/retrofits.jl | 57 +- .../resources/storage/investment_charge.jl | 194 +- .../resources/storage/investment_energy.jl | 195 +- .../storage/long_duration_storage.jl | 203 ++- src/model/resources/storage/storage.jl | 94 +- src/model/resources/storage/storage_all.jl | 360 ++-- .../resources/storage/storage_asymmetric.jl | 61 +- .../resources/storage/storage_symmetric.jl | 75 +- src/model/resources/thermal/thermal.jl | 52 +- src/model/resources/thermal/thermal_commit.jl | 246 +-- .../resources/thermal/thermal_no_commit.jl | 119 +- src/model/resources/vre_stor/vre_stor.jl | 1575 ++++++++++------- src/model/solve_model.jl | 148 +- src/model/utility.jl | 13 +- .../configure_multi_stage_inputs.jl | 249 +-- src/multi_stage/dual_dynamic_programming.jl | 104 +- src/multi_stage/endogenous_retirement.jl | 1480 +++++++++------- .../write_multi_stage_capacities_charge.jl | 5 +- .../write_multi_stage_capacities_discharge.jl | 5 +- .../write_multi_stage_capacities_energy.jl | 5 +- src/multi_stage/write_multi_stage_costs.jl | 10 +- .../write_multi_stage_network_expansion.jl | 5 +- src/multi_stage/write_multi_stage_stats.jl | 12 +- src/time_domain_reduction/precluster.jl | 2 +- .../time_domain_reduction.jl | 1043 +++++++---- .../effective_capacity.jl | 37 +- .../write_capacity_value.jl | 164 +- .../write_reserve_margin.jl | 14 +- .../write_reserve_margin_revenue.jl | 140 +- .../write_reserve_margin_slack.jl | 21 +- .../write_reserve_margin_w.jl | 25 +- .../write_virtual_discharge.jl | 36 +- src/write_outputs/choose_output_dir.jl | 14 +- src/write_outputs/co2_cap/write_co2_cap.jl | 16 +- src/write_outputs/dftranspose.jl | 13 +- .../write_esr_prices.jl | 28 +- .../write_esr_revenue.jl | 135 +- .../hydrogen/write_hourly_matching_prices.jl | 32 +- .../hydrogen/write_hydrogen_prices.jl | 9 +- .../write_opwrap_lds_dstor.jl | 54 +- .../write_opwrap_lds_stor_init.jl | 57 +- .../write_maximum_capacity_requirement.jl | 14 +- .../write_minimum_capacity_requirement.jl | 14 +- .../write_operating_reserve_price_revenue.jl | 49 +- src/write_outputs/reserves/write_reg.jl | 30 +- src/write_outputs/reserves/write_rsv.jl | 57 +- .../transmission/write_nw_expansion.jl | 34 +- .../transmission/write_transmission_flows.jl | 49 +- .../transmission/write_transmission_losses.jl | 60 +- src/write_outputs/ucommit/write_commit.jl | 23 +- src/write_outputs/ucommit/write_shutdown.jl | 30 +- src/write_outputs/ucommit/write_start.jl | 29 +- src/write_outputs/write_angles.jl | 22 +- src/write_outputs/write_capacity.jl | 236 +-- src/write_outputs/write_capacityfactor.jl | 52 +- src/write_outputs/write_charge.jl | 70 +- src/write_outputs/write_charging_cost.jl | 70 +- src/write_outputs/write_co2.jl | 22 +- src/write_outputs/write_costs.jl | 548 +++--- src/write_outputs/write_curtailment.jl | 89 +- src/write_outputs/write_emissions.jl | 195 +- src/write_outputs/write_energy_revenue.jl | 46 +- src/write_outputs/write_fuel_consumption.jl | 147 +- src/write_outputs/write_maintenance.jl | 2 +- src/write_outputs/write_net_revenue.jl | 503 +++--- src/write_outputs/write_nse.jl | 58 +- src/write_outputs/write_outputs.jl | 861 +++++---- src/write_outputs/write_power.jl | 38 +- src/write_outputs/write_power_balance.jl | 159 +- src/write_outputs/write_price.jl | 26 +- src/write_outputs/write_reliability.jl | 24 +- src/write_outputs/write_status.jl | 23 +- src/write_outputs/write_storage.jl | 66 +- src/write_outputs/write_storagedual.jl | 123 +- src/write_outputs/write_subsidy_revenue.jl | 204 ++- src/write_outputs/write_time_weights.jl | 8 +- src/write_outputs/write_vre_stor.jl | 792 +++++---- test/expression_manipulation_test.jl | 40 +- test/resource_test.jl | 172 +- test/runtests.jl | 3 +- test/test_DCOPF.jl | 10 +- test/test_VRE_storage.jl | 8 +- test/test_compute_conflicts.jl | 16 +- test/test_electrolyzer.jl | 8 +- test/test_examples.jl | 5 +- test/test_load_resource_data.jl | 451 +++-- test/test_multifuels.jl | 10 +- test/test_multistage.jl | 157 +- test/test_piecewisefuel.jl | 8 +- test/test_retrofit.jl | 8 +- test/test_threezones.jl | 8 +- test/test_time_domain_reduction.jl | 11 +- test/utilities.jl | 69 +- 157 files changed, 10375 insertions(+), 8118 deletions(-) create mode 100644 .JuliaFormatter.toml create mode 100644 .github/workflows/format_suggestions.yml diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml new file mode 100644 index 0000000000..453925c3f9 --- /dev/null +++ b/.JuliaFormatter.toml @@ -0,0 +1 @@ +style = "sciml" \ No newline at end of file diff --git a/.github/workflows/format_suggestions.yml b/.github/workflows/format_suggestions.yml new file mode 100644 index 0000000000..05e574dd68 --- /dev/null +++ b/.github/workflows/format_suggestions.yml @@ -0,0 +1,9 @@ +name: Format suggestions +on: + pull_request: + +jobs: + code-style: + runs-on: ubuntu-latest + steps: + - uses: julia-actions/julia-format@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3792d38e89..c372739aaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 introduce this parameter as cost of virtual charging and discharging to avoid unusual results (#608). - New settings parameter, StorageVirtualDischarge, to turn storage virtual charging and discharging off if desired by the user (#638). - Add module to retrofit existing resources with new technologies (#600). +- Formatted the code and added a format check to the CI pipeline (#673). ### Fixed - Set MUST_RUN=1 for RealSystemExample/small_hydro plants (#517). diff --git a/README.md b/README.md index f4eb78ce22..7a4e642e3f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@
GenX.jl
-[![CI](https://github.com/GenXProject/GenX/actions/workflows/ci.yml/badge.svg)](https://github.com/GenXProject/GenX/actions/workflows/ci.yml) -[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://genxproject.github.io/GenX.jl/dev) -[![DOI](https://zenodo.org/badge/368957308.svg)](https://zenodo.org/doi/10.5281/zenodo.10846069) -[![ColPrac: Contributor's Guide on Collaborative Practices for Community Packages](https://img.shields.io/badge/ColPrac-Contributor's%20Guide-blueviolet)](https://github.com/SciML/ColPrac) +| **Documentation** | **DOI** | +|:-------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------:| +[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://genxproject.github.io/GenX.jl/stable/) [![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://genxproject.github.io/GenX.jl/dev) | [![DOI](https://zenodo.org/badge/368957308.svg)](https://zenodo.org/doi/10.5281/zenodo.10846069) + +[![CI](https://github.com/GenXProject/GenX/actions/workflows/ci.yml/badge.svg)](https://github.com/GenXProject/GenX/actions/workflows/ci.yml) [![SciML Code Style](https://img.shields.io/static/v1?label=code%20style&message=SciML&color=9558b2&labelColor=389826)](https://github.com/SciML/SciMLStyle) [![ColPrac: Contributor's Guide on Collaborative Practices for Community Packages](https://img.shields.io/badge/ColPrac-Contributor's%20Guide-blueviolet)](https://github.com/SciML/ColPrac) ## Overview GenX is a highly-configurable, [open source](https://github.com/GenXProject/GenX/blob/main/LICENSE) electricity resource capacity expansion model diff --git a/docs/make.jl b/docs/make.jl index eeaa501c5b..f31c297eeb 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -2,14 +2,13 @@ using Documenter using GenX import DataStructures: OrderedDict -DocMeta.setdocmeta!(GenX, :DocTestSetup, :(using GenX); recursive=true) +DocMeta.setdocmeta!(GenX, :DocTestSetup, :(using GenX); recursive = true) -pages = OrderedDict( - "Welcome Page" => [ +pages = OrderedDict("Welcome Page" => [ "GenX: Introduction" => "index.md", "Installation Guide" => "installation.md", "Limitation of GenX" => "limitations_genx.md", - "Third Party Extensions" => "third_party_genx.md" + "Third Party Extensions" => "third_party_genx.md", ], "Getting Started" => [ "Running GenX" => "Getting_Started/examples_casestudies.md", @@ -51,7 +50,7 @@ pages = OrderedDict( "Flexible Demand" => "Model_Reference/Resources/flexible_demand.md", "Hydro" => [ "Hydro Reservoir" => "Model_Reference/Resources/hydro_res.md", - "Long Duration Hydro" => "Model_Reference/Resources/hydro_inter_period_linkage.md" + "Long Duration Hydro" => "Model_Reference/Resources/hydro_inter_period_linkage.md", ], "Must Run" => "Model_Reference/Resources/must_run.md", "Retrofit" => "Model_Reference/Resources/retrofit.md", @@ -62,18 +61,18 @@ pages = OrderedDict( "Long Duration Storage" => "Model_Reference/Resources/long_duration_storage.md", "Storage All" => "Model_Reference/Resources/storage_all.md", "Storage Asymmetric" => "Model_Reference/Resources/storage_asymmetric.md", - "Storage Symmetric" => "Model_Reference/Resources/storage_symmetric.md" + "Storage Symmetric" => "Model_Reference/Resources/storage_symmetric.md", ], "Co-located VRE and Storage" => "Model_Reference/Resources/vre_stor.md", "Thermal" => [ "Thermal" => "Model_Reference/Resources/thermal.md", "Thermal Commit" => "Model_Reference/Resources/thermal_commit.md", - "Thermal No Commit" => "Model_Reference/Resources/thermal_no_commit.md" + "Thermal No Commit" => "Model_Reference/Resources/thermal_no_commit.md", ], "Hydrogen Electrolyzers" => "Model_Reference/Resources/electrolyzers.md", "Retrofit" => "Model_Reference/Resources/retrofit.md", "Scheduled maintenance for various resources" => "Model_Reference/Resources/maintenance.md", - "Resource types" => "Model_Reference/Resources/resource.md" + "Resource types" => "Model_Reference/Resources/resource.md", ], "Maintenance" => "Model_Reference/maintenance_overview.md", "Policies" => "Model_Reference/policies.md", @@ -88,46 +87,40 @@ pages = OrderedDict( "Multi-Stage Modeling Introduction" => "Model_Reference/Multi_Stage/multi_stage_overview.md", "Configure multi-stage inputs" => "Model_Reference/Multi_Stage/configure_multi_stage_inputs.md", "Model multi stage: Dual Dynamic Programming Algorithm" => "Model_Reference/Multi_Stage/dual_dynamic_programming.md", - "Endogenous Retirement" => "Model_Reference/Multi_Stage/endogenous_retirement.md" + "Endogenous Retirement" => "Model_Reference/Multi_Stage/endogenous_retirement.md", ], "Method of Morris" => "Model_Reference/methodofmorris.md", "Utility Functions" => "Model_Reference/utility_functions.md", ], "Public API Reference" => [ - "Public API" => "Public_API/public_api.md", - - ], + "Public API" => "Public_API/public_api.md"], "Third Party Extensions" => "additional_third_party_extensions.md", - "Developer Docs" => "developer_guide.md", -) + "Developer Docs" => "developer_guide.md") # Build documentation. # ==================== makedocs(; - modules=[GenX], - authors="Jesse Jenkins, Nestor Sepulveda, Dharik Mallapragada, Aaron Schwartz, Neha Patankar, Qingyu Xu, Jack Morris, Sambuddha Chakrabarti", - sitename="GenX.jl", - format=Documenter.HTML(; - prettyurls=get(ENV, "CI", "false") == "true", - canonical="https://genxproject.github.io/GenX.jl/stable", + modules = [GenX], + authors = "Jesse Jenkins, Nestor Sepulveda, Dharik Mallapragada, Aaron Schwartz, Neha Patankar, Qingyu Xu, Jack Morris, Sambuddha Chakrabarti", + sitename = "GenX.jl", + format = Documenter.HTML(; + prettyurls = get(ENV, "CI", "false") == "true", + canonical = "https://genxproject.github.io/GenX.jl/stable", assets = ["assets/genx_style.css"], - sidebar_sitename=false, - collapselevel=1 - ), - pages=[p for p in pages] -) + sidebar_sitename = false, + collapselevel = 1), + pages = [p for p in pages]) # Deploy built documentation. # =========================== deploydocs(; - repo="github.com/GenXProject/GenX.jl.git", + repo = "github.com/GenXProject/GenX.jl.git", target = "build", branch = "gh-pages", devbranch = "main", devurl = "dev", - push_preview=true, + push_preview = true, versions = ["stable" => "v^", "v#.#.#", "dev" => "dev"], - forcepush = false, -) + forcepush = false) diff --git a/docs/src/developer_guide.md b/docs/src/developer_guide.md index 584a60458b..bca5d37293 100644 --- a/docs/src/developer_guide.md +++ b/docs/src/developer_guide.md @@ -12,6 +12,19 @@ GenX is an open-source project, and we welcome contributions from the community. The following sections describe in more detail how to work with GenX resources and how to add a new resource to GenX. +## Style guide +GenX project follows the [SciML Style Guide](https://github.com/SciML/SciMLStyle). We encourage contributors to follow this style guide when submitting code changes to GenX. Before submitting a new PR, please run the following command to format a file or a directory: +```julia +julia> using JuliaFormatter +julia> format("path_to_directory", SciMLStyle(), verbose=true) +``` +or +```julia +julia> using JuliaFormatter +julia> format("path_to_file.jl", SciMLStyle(), verbose=true) +``` +The GitHub repository of GenX is configured to verify the code style of each PR and will automatically provide comments to assist you in formatting the code according to the style guide. + ## GenX resources In GenX, a resource is defined as an instance of a `GenX resource type`, a subtype of an `AbstractResource`. This allows the code to use multiple dispatch and define a common interface (behavior) for all resources in the code. diff --git a/example_systems/1_three_zones/Run.jl b/example_systems/1_three_zones/Run.jl index 7ce1891834..b44ca23ec1 100644 --- a/example_systems/1_three_zones/Run.jl +++ b/example_systems/1_three_zones/Run.jl @@ -1,3 +1,3 @@ using GenX -run_genx_case!(dirname(@__FILE__)) \ No newline at end of file +run_genx_case!(dirname(@__FILE__)) diff --git a/example_systems/2_three_zones_w_electrolyzer/Run.jl b/example_systems/2_three_zones_w_electrolyzer/Run.jl index 7ce1891834..b44ca23ec1 100644 --- a/example_systems/2_three_zones_w_electrolyzer/Run.jl +++ b/example_systems/2_three_zones_w_electrolyzer/Run.jl @@ -1,3 +1,3 @@ using GenX -run_genx_case!(dirname(@__FILE__)) \ No newline at end of file +run_genx_case!(dirname(@__FILE__)) diff --git a/example_systems/3_three_zones_w_co2_capture/Run.jl b/example_systems/3_three_zones_w_co2_capture/Run.jl index 7ce1891834..b44ca23ec1 100644 --- a/example_systems/3_three_zones_w_co2_capture/Run.jl +++ b/example_systems/3_three_zones_w_co2_capture/Run.jl @@ -1,3 +1,3 @@ using GenX -run_genx_case!(dirname(@__FILE__)) \ No newline at end of file +run_genx_case!(dirname(@__FILE__)) diff --git a/example_systems/4_three_zones_w_policies_slack/Run.jl b/example_systems/4_three_zones_w_policies_slack/Run.jl index 7ce1891834..b44ca23ec1 100644 --- a/example_systems/4_three_zones_w_policies_slack/Run.jl +++ b/example_systems/4_three_zones_w_policies_slack/Run.jl @@ -1,3 +1,3 @@ using GenX -run_genx_case!(dirname(@__FILE__)) \ No newline at end of file +run_genx_case!(dirname(@__FILE__)) diff --git a/example_systems/6_three_zones_w_multistage/Run.jl b/example_systems/6_three_zones_w_multistage/Run.jl index 7ce1891834..b44ca23ec1 100644 --- a/example_systems/6_three_zones_w_multistage/Run.jl +++ b/example_systems/6_three_zones_w_multistage/Run.jl @@ -1,3 +1,3 @@ using GenX -run_genx_case!(dirname(@__FILE__)) \ No newline at end of file +run_genx_case!(dirname(@__FILE__)) diff --git a/example_systems/7_three_zones_w_colocated_VRE_storage/Run.jl b/example_systems/7_three_zones_w_colocated_VRE_storage/Run.jl index 7ce1891834..b44ca23ec1 100644 --- a/example_systems/7_three_zones_w_colocated_VRE_storage/Run.jl +++ b/example_systems/7_three_zones_w_colocated_VRE_storage/Run.jl @@ -1,3 +1,3 @@ using GenX -run_genx_case!(dirname(@__FILE__)) \ No newline at end of file +run_genx_case!(dirname(@__FILE__)) diff --git a/example_systems/9_IEEE_9_bus_DC_OPF/Run.jl b/example_systems/9_IEEE_9_bus_DC_OPF/Run.jl index 7ce1891834..b44ca23ec1 100644 --- a/example_systems/9_IEEE_9_bus_DC_OPF/Run.jl +++ b/example_systems/9_IEEE_9_bus_DC_OPF/Run.jl @@ -1,3 +1,3 @@ using GenX -run_genx_case!(dirname(@__FILE__)) \ No newline at end of file +run_genx_case!(dirname(@__FILE__)) diff --git a/src/additional_tools/method_of_morris.jl b/src/additional_tools/method_of_morris.jl index 0938c8022e..d4628b55fa 100644 --- a/src/additional_tools/method_of_morris.jl +++ b/src/additional_tools/method_of_morris.jl @@ -1,28 +1,28 @@ const SEED = 1234 - @doc raw""" morris(EP::Model, path::AbstractString, setup::Dict, inputs::Dict, outpath::AbstractString, OPTIMIZER) We apply the Method of Morris developed by [Morris, M., 1991](https://www.jstor.org/stable/1269043) in order to identify the input parameters that produce the largest change on total system cost. Method of Morris falls under the simplest class of one-factor-at-a-time (OAT) screening techniques. It assumes l levels per input factor and generates a set of trajectories through the input space. As such, the Method of Morris generates a grid of uncertain model input parameters, $x_i, i=1, ..., k$,, where the range $[x_i^{-}, x_i^{+}$ of each uncertain input parameter i is split into l intervals of equal length. Each trajectory starts at different realizations of input parameters chosen at random and are built by successively selecting one of the inputs randomly and moving it to an adjacent level. These trajectories are used to estimate the mean and the standard deviation of each input parameter on total system cost. A high estimated mean indicates that the input parameter is important; a high estimated standard deviation indicates important interactions between that input parameter and other inputs. """ -struct MatSpread{T1,T2} +struct MatSpread{T1, T2} mat::T1 spread::T2 end -struct MorrisResult{T1,T2} +struct MorrisResult{T1, T2} means::T1 means_star::T1 variances::T1 elementary_effects::T2 end -function generate_design_matrix(p_range, p_steps, rng;len_design_mat,groups) - ps = [range(p_range[i][1], stop=p_range[i][2], length=p_steps[i]) for i in 1:length(p_range)] +function generate_design_matrix(p_range, p_steps, rng; len_design_mat, groups) + ps = [range(p_range[i][1], stop = p_range[i][2], length = p_steps[i]) + for i in 1:length(p_range)] indices = [rand(rng, 1:i) for i in p_steps] - all_idxs_original = Vector{typeof(indices)}(undef,len_design_mat) - + all_idxs_original = Vector{typeof(indices)}(undef, len_design_mat) + for i in 1:len_design_mat j = rand(rng, 1:length(p_range)) indices[j] += (rand(rng) < 0.5 ? -1 : 1) @@ -34,20 +34,20 @@ function generate_design_matrix(p_range, p_steps, rng;len_design_mat,groups) all_idxs_original[i] = copy(indices) end - df_all_idx_original = DataFrame(all_idxs_original,:auto) + df_all_idx_original = DataFrame(all_idxs_original, :auto) println(df_all_idx_original) all_idxs = similar(df_all_idx_original) for g in unique(groups) - temp = findall(x->x==g, groups) + temp = findall(x -> x == g, groups) for k in temp - all_idxs[k,:] = df_all_idx_original[temp[1],:] + all_idxs[k, :] = df_all_idx_original[temp[1], :] end end println(all_idxs) - B = Array{Array{Float64}}(undef,len_design_mat) + B = Array{Array{Float64}}(undef, len_design_mat) for j in 1:len_design_mat - cur_p = [ps[u][(all_idxs[:,j][u])] for u in 1:length(p_range)] + cur_p = [ps[u][(all_idxs[:, j][u])] for u in 1:length(p_range)] B[j] = cur_p end reduce(hcat, B) @@ -55,45 +55,66 @@ end function calculate_spread(matrix) spread = 0.0 - for i in 2:size(matrix,2) - spread += sqrt(sum(abs2.(matrix[:,i] - matrix[:,i-1]))) + for i in 2:size(matrix, 2) + spread += sqrt(sum(abs2.(matrix[:, i] - matrix[:, i - 1]))) end spread end -function sample_matrices(p_range,p_steps, rng;num_trajectory,total_num_trajectory,len_design_mat,groups) +function sample_matrices(p_range, + p_steps, + rng; + num_trajectory, + total_num_trajectory, + len_design_mat, + groups) matrix_array = [] println(num_trajectory) println(total_num_trajectory) - if total_num_trajectory x.spread,rev=true) + sort!(matrix_array, by = x -> x.spread, rev = true) matrices = [i.mat for i in matrix_array[1:num_trajectory]] - reduce(hcat,matrices) + reduce(hcat, matrices) end -function my_gsa(f, p_steps, num_trajectory, total_num_trajectory, p_range::AbstractVector, len_design_mat, groups, random) +function my_gsa(f, + p_steps, + num_trajectory, + total_num_trajectory, + p_range::AbstractVector, + len_design_mat, + groups, + random) rng = Random.default_rng() - if !random; Random.seed!(SEED); end - design_matrices_original = sample_matrices(p_range, p_steps, rng;num_trajectory, - total_num_trajectory,len_design_mat,groups) + if !random + Random.seed!(SEED) + end + design_matrices_original = sample_matrices(p_range, p_steps, rng; num_trajectory, + total_num_trajectory, len_design_mat, groups) println(design_matrices_original) - L = DataFrame(design_matrices_original,:auto) + L = DataFrame(design_matrices_original, :auto) println(L) - distinct_trajectories = Array{Int64}(undef,num_trajectory) - design_matrices = Matrix(DataFrame(unique(last, pairs(eachcol(L[!,1:len_design_mat]))))) - distinct_trajectories[1] = length(design_matrices[1,:]) + distinct_trajectories = Array{Int64}(undef, num_trajectory) + design_matrices = Matrix(DataFrame(unique(last, + pairs(eachcol(L[!, 1:len_design_mat]))))) + distinct_trajectories[1] = length(design_matrices[1, :]) if num_trajectory > 1 for i in 2:num_trajectory - design_matrices = hcat(design_matrices, Matrix(DataFrame(unique(last, pairs(eachcol(L[!,(i-1)*len_design_mat+1:i*len_design_mat])))))) - distinct_trajectories[i] = length(Matrix(DataFrame(unique(last, pairs(eachcol(L[!,(i-1)*len_design_mat+1:i*len_design_mat])))))[1,:]) + design_matrices = hcat(design_matrices, + Matrix(DataFrame(unique(last, + pairs(eachcol(L[!, + ((i - 1) * len_design_mat + 1):(i * len_design_mat)])))))) + distinct_trajectories[i] = length(Matrix(DataFrame(unique(last, + pairs(eachcol(L[!, ((i - 1) * len_design_mat + 1):(i * len_design_mat)])))))[1, + :]) end end println(distinct_trajectories) @@ -102,26 +123,27 @@ function my_gsa(f, p_steps, num_trajectory, total_num_trajectory, p_range::Abstr multioutput = false desol = false local y_size - - _y = [f(design_matrices[:,i]) for i in 1:size(design_matrices,2)] + + _y = [f(design_matrices[:, i]) for i in 1:size(design_matrices, 2)] multioutput = !(eltype(_y) <: Number) if eltype(_y) <: RecursiveArrayTools.AbstractVectorOfArray y_size = size(_y[1]) _y = vec.(_y) desol = true end - all_y = multioutput ? reduce(hcat,_y) : _y + all_y = multioutput ? reduce(hcat, _y) : _y println(all_y) effects = [] - while(length(effects) < length(groups)) - push!(effects,Vector{Float64}[]) + while (length(effects) < length(groups)) + push!(effects, Vector{Float64}[]) end for i in 1:num_trajectory len_design_mat = distinct_trajectories[i] - y1 = multioutput ? all_y[:,(i-1)*len_design_mat+1] : all_y[(i-1)*len_design_mat+1] - for j in (i-1)*len_design_mat+1:(i*len_design_mat)-1 + y1 = multioutput ? all_y[:, (i - 1) * len_design_mat + 1] : + all_y[(i - 1) * len_design_mat + 1] + for j in ((i - 1) * len_design_mat + 1):((i * len_design_mat) - 1) y2 = y1 - del = design_matrices[:,j+1] - design_matrices[:,j] + del = design_matrices[:, j + 1] - design_matrices[:, j] change_index = 0 for k in 1:length(del) if abs(del[k]) > 0 @@ -130,14 +152,14 @@ function my_gsa(f, p_steps, num_trajectory, total_num_trajectory, p_range::Abstr end end del = sum(del) - y1 = multioutput ? all_y[:,j+1] : all_y[j+1] - effect = @. (y1-y2)/(del) + y1 = multioutput ? all_y[:, j + 1] : all_y[j + 1] + effect = @. (y1 - y2) / (del) elem_effect = typeof(y1) <: Number ? effect : mean(effect, dims = 2) - temp_g_index = findall(x->x==groups[change_index], groups) + temp_g_index = findall(x -> x == groups[change_index], groups) for g in temp_g_index println(effects) println(elem_effect) - push!(effects[g],elem_effect) + push!(effects[g], elem_effect) end end end @@ -156,23 +178,32 @@ function my_gsa(f, p_steps, num_trajectory, total_num_trajectory, p_range::Abstr end end if desol - f_shape = x -> [reshape(x[:,i],y_size) for i in 1:size(x,2)] - means = map(f_shape,means) - means_star = map(f_shape,means_star) - variances = map(f_shape,variances) + f_shape = x -> [reshape(x[:, i], y_size) for i in 1:size(x, 2)] + means = map(f_shape, means) + means_star = map(f_shape, means_star) + variances = map(f_shape, variances) end - MorrisResult(reduce(hcat, means),reduce(hcat, means_star),reduce(hcat, variances),effects) + MorrisResult(reduce(hcat, means), + reduce(hcat, means_star), + reduce(hcat, variances), + effects) end -function morris(EP::Model, path::AbstractString, setup::Dict, inputs::Dict, outpath::AbstractString, OPTIMIZER; random=true) +function morris(EP::Model, + path::AbstractString, + setup::Dict, + inputs::Dict, + outpath::AbstractString, + OPTIMIZER; + random = true) # Reading the input parameters Morris_range = load_dataframe(joinpath(path, "Method_of_morris_range.csv")) - groups = Morris_range[!,:Group] - p_steps = Morris_range[!,:p_steps] - total_num_trajectory = Morris_range[!,:total_num_trajectory][1] - num_trajectory = Morris_range[!,:num_trajectory][1] - len_design_mat = Morris_range[!,:len_design_mat][1] - uncertain_columns = unique(Morris_range[!,:Parameter]) + groups = Morris_range[!, :Group] + p_steps = Morris_range[!, :p_steps] + total_num_trajectory = Morris_range[!, :total_num_trajectory][1] + num_trajectory = Morris_range[!, :num_trajectory][1] + len_design_mat = Morris_range[!, :len_design_mat][1] + uncertain_columns = unique(Morris_range[!, :Parameter]) #save_parameters = zeros(length(Morris_range[!,:Parameter])) gen = inputs["RESOURCES"] @@ -181,40 +212,53 @@ function morris(EP::Model, path::AbstractString, setup::Dict, inputs::Dict, outp for column in uncertain_columns col_sym = Symbol(lowercase(column)) # column_f is the function to get the value "column" for each generator - column_f = isdefined(GenX, col_sym) ? getfield(GenX, col_sym) : r -> getproperty(r, col_sym) - sigma = [sigma; [column_f.(gen) .* (1 .+ Morris_range[Morris_range[!,:Parameter] .== column, :Lower_bound] ./100) column_f.(gen) .* (1 .+ Morris_range[Morris_range[!,:Parameter] .== column, :Upper_bound] ./100)]] + column_f = isdefined(GenX, col_sym) ? getfield(GenX, col_sym) : + r -> getproperty(r, col_sym) + sigma = [sigma; + [column_f.(gen) .* (1 .+ + Morris_range[Morris_range[!, :Parameter] .== column, :Lower_bound] ./ 100) column_f.(gen) .* + (1 .+ + Morris_range[Morris_range[!, :Parameter] .== column, + :Upper_bound] ./ 100)]] end - sigma = sigma[2:end,:] + sigma = sigma[2:end, :] - p_range = mapslices(x->[x], sigma, dims=2)[:] + p_range = mapslices(x -> [x], sigma, dims = 2)[:] # Creating a function for iteratively solving the model with different sets of input parameters - f1 = function(sigma) + f1 = function (sigma) #print(sigma) print("\n") #save_parameters = hcat(save_parameters, sigma) for column in uncertain_columns - index = findall(s -> s == column, Morris_range[!,:Parameter]) + index = findall(s -> s == column, Morris_range[!, :Parameter]) attr_to_set = Symbol(lowercase(column)) gen[attr_to_set] = sigma[first(index):last(index)] end EP = generate_model(setup, inputs, OPTIMIZER) #EP, solve_time = solve_model(EP, setup) - redirect_stdout((()->optimize!(EP)),open("/dev/null", "w")) + redirect_stdout((() -> optimize!(EP)), open("/dev/null", "w")) [objective_value(EP)] end # Perform the method of morris analysis - m = my_gsa(f1,p_steps,num_trajectory,total_num_trajectory,p_range,len_design_mat,groups,random) + m = my_gsa(f1, + p_steps, + num_trajectory, + total_num_trajectory, + p_range, + len_design_mat, + groups, + random) println(m.means) println(DataFrame(m.means', :auto)) #save the mean effect of each uncertain variable on the objective fucntion - Morris_range[!,:mean] = DataFrame(m.means', :auto)[!,:x1] + Morris_range[!, :mean] = DataFrame(m.means', :auto)[!, :x1] println(DataFrame(m.variances', :auto)) #save the variance of effect of each uncertain variable on the objective function - Morris_range[!,:variance] = DataFrame(m.variances', :auto)[!,:x1] + Morris_range[!, :variance] = DataFrame(m.variances', :auto)[!, :x1] CSV.write(joinpath(outpath, "morris.csv"), Morris_range) return Morris_range diff --git a/src/additional_tools/modeling_to_generate_alternatives.jl b/src/additional_tools/modeling_to_generate_alternatives.jl index 97400b54b1..ba0fec33e3 100644 --- a/src/additional_tools/modeling_to_generate_alternatives.jl +++ b/src/additional_tools/modeling_to_generate_alternatives.jl @@ -19,95 +19,101 @@ To create the MGA formulation, we replace the cost-minimizing objective function where, $\beta_{zr}$ is a random objective fucntion coefficient betwen $[0,100]$ for MGA iteration $k$. $\Theta_{y,t,z,r}$ is a generation of technology $y$ in zone $z$ in time period $t$ that belongs to a resource type $r$. We aggregate $\Theta_{y,t,z,r}$ into a new variable $P_{z,r}$ that represents total generation from technology type $r$ in a zone $z$. In the second constraint above, $\delta$ denote the increase in budget from the least-cost solution and $f$ represents the expression for the total system cost. The constraint $Ax = b$ represents all other constraints in the power system model. We then solve the formulation with minimization and maximization objective function to explore near optimal solution space. """ function mga(EP::Model, path::AbstractString, setup::Dict, inputs::Dict) - - if setup["ModelingToGenerateAlternatives"]==1 + if setup["ModelingToGenerateAlternatives"] == 1 # Start MGA Algorithm - println("MGA Module") + println("MGA Module") - # Objective function value of the least cost problem - Least_System_Cost = objective_value(EP) + # Objective function value of the least cost problem + Least_System_Cost = objective_value(EP) - # Read sets - gen = inputs["RESOURCES"] - T = inputs["T"] # Number of time steps (hours) - Z = inputs["Z"] # Number of zonests - zones = unique(inputs["R_ZONES"]) + # Read sets + gen = inputs["RESOURCES"] + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zonests + zones = unique(inputs["R_ZONES"]) - # Create a set of unique technology types - resources_with_mga = gen[ids_with_mga(gen)] - TechTypes = unique(resource_type_mga.(resources_with_mga)) + # Create a set of unique technology types + resources_with_mga = gen[ids_with_mga(gen)] + TechTypes = unique(resource_type_mga.(resources_with_mga)) - # Read slack parameter representing desired increase in budget from the least cost solution - slack = setup["ModelingtoGenerateAlternativeSlack"] + # Read slack parameter representing desired increase in budget from the least cost solution + slack = setup["ModelingtoGenerateAlternativeSlack"] - ### Variables ### + ### Variables ### - @variable(EP, vSumvP[TechTypes = 1:length(TechTypes), z = 1:Z] >= 0) # Variable denoting total generation from eligible technology of a given type + @variable(EP, vSumvP[TechTypes = 1:length(TechTypes), z = 1:Z]>=0) # Variable denoting total generation from eligible technology of a given type - ### End Variables ### + ### End Variables ### - ### Constraints ### + ### Constraints ### - # Constraint to set budget for MGA iterations - @constraint(EP, budget, EP[:eObj] <= Least_System_Cost * (1 + slack) ) + # Constraint to set budget for MGA iterations + @constraint(EP, budget, EP[:eObj]<=Least_System_Cost * (1 + slack)) # Constraint to compute total generation in each zone from a given Technology Type - function resource_in_zone_with_TechType(tt::Int64, z::Int64) - condition::BitVector = (resource_type_mga.(gen) .== TechTypes[tt]) .& (zone_id.(gen) .== z) - return resource_id.(gen[condition]) - end - @constraint(EP,cGeneration[tt = 1:length(TechTypes), z = 1:Z], vSumvP[tt,z] == sum(EP[:vP][y,t] * inputs["omega"][t] for y in resource_in_zone_with_TechType(tt,z), t in 1:T)) - - ### End Constraints ### - - ### Create Results Directory for MGA iterations + function resource_in_zone_with_TechType(tt::Int64, z::Int64) + condition::BitVector = (resource_type_mga.(gen) .== TechTypes[tt]) .& + (zone_id.(gen) .== z) + return resource_id.(gen[condition]) + end + @constraint(EP, + cGeneration[tt = 1:length(TechTypes), z = 1:Z], + vSumvP[tt, + z]==sum(EP[:vP][y, t] * inputs["omega"][t] + for y in resource_in_zone_with_TechType(tt, z), t in 1:T)) + + ### End Constraints ### + + ### Create Results Directory for MGA iterations outpath_max = joinpath(path, "MGAResults_max") - if !(isdir(outpath_max)) - mkdir(outpath_max) - end + if !(isdir(outpath_max)) + mkdir(outpath_max) + end outpath_min = joinpath(path, "MGAResults_min") - if !(isdir(outpath_min)) - mkdir(outpath_min) - end + if !(isdir(outpath_min)) + mkdir(outpath_min) + end - ### Begin MGA iterations for maximization and minimization objective ### - mga_start_time = time() + ### Begin MGA iterations for maximization and minimization objective ### + mga_start_time = time() - print("Starting the first MGA iteration") + print("Starting the first MGA iteration") - for i in 1:setup["ModelingToGenerateAlternativeIterations"] + for i in 1:setup["ModelingToGenerateAlternativeIterations"] - # Create random coefficients for the generators that we want to include in the MGA run for the given budget - pRand = rand(length(TechTypes),length(zones)) + # Create random coefficients for the generators that we want to include in the MGA run for the given budget + pRand = rand(length(TechTypes), length(zones)) - ### Maximization objective - @objective(EP, Max, sum(pRand[tt,z] * vSumvP[tt,z] for tt in 1:length(TechTypes), z in 1:Z )) + ### Maximization objective + @objective(EP, + Max, + sum(pRand[tt, z] * vSumvP[tt, z] for tt in 1:length(TechTypes), z in 1:Z)) - # Solve Model Iteration - status = optimize!(EP) + # Solve Model Iteration + status = optimize!(EP) # Create path for saving MGA iterations - mgaoutpath_max = joinpath(outpath_max, string("MGA", "_", slack,"_", i)) + mgaoutpath_max = joinpath(outpath_max, string("MGA", "_", slack, "_", i)) - # Write results - write_outputs(EP, mgaoutpath_max, setup, inputs) + # Write results + write_outputs(EP, mgaoutpath_max, setup, inputs) - ### Minimization objective - @objective(EP, Min, sum(pRand[tt,z] * vSumvP[tt,z] for tt in 1:length(TechTypes), z in 1:Z )) + ### Minimization objective + @objective(EP, + Min, + sum(pRand[tt, z] * vSumvP[tt, z] for tt in 1:length(TechTypes), z in 1:Z)) - # Solve Model Iteration - status = optimize!(EP) + # Solve Model Iteration + status = optimize!(EP) # Create path for saving MGA iterations - mgaoutpath_min = joinpath(outpath_min, string("MGA", "_", slack,"_", i)) - - # Write results - write_outputs(EP, mgaoutpath_min, setup, inputs) - - end + mgaoutpath_min = joinpath(outpath_min, string("MGA", "_", slack, "_", i)) - total_time = time() - mga_start_time - ### End MGA Iterations ### - end + # Write results + write_outputs(EP, mgaoutpath_min, setup, inputs) + end + total_time = time() - mga_start_time + ### End MGA Iterations ### + end end diff --git a/src/case_runners/case_runner.jl b/src/case_runners/case_runner.jl index bbdaae4f53..f5725e7bfc 100644 --- a/src/case_runners/case_runner.jl +++ b/src/case_runners/case_runner.jl @@ -28,7 +28,7 @@ run_genx_case!("path/to/case", HiGHS.Optimizer) run_genx_case!("path/to/case", Gurobi.Optimizer) ``` """ -function run_genx_case!(case::AbstractString, optimizer::Any=HiGHS.Optimizer) +function run_genx_case!(case::AbstractString, optimizer::Any = HiGHS.Optimizer) genx_settings = get_settings_path(case, "genx_settings.yml") # Settings YAML file path writeoutput_settings = get_settings_path(case, "output_settings.yml") # Write-output settings YAML file path mysetup = configure_settings(genx_settings, writeoutput_settings) # mysetup dictionary stores settings and GenX-specific parameters @@ -86,7 +86,10 @@ function run_genx_case_simple!(case::AbstractString, mysetup::Dict, optimizer::A if has_values(EP) println("Writing Output") outputs_path = get_default_output_folder(case) - elapsed_time = @elapsed outputs_path = write_outputs(EP, outputs_path, mysetup, myinputs) + elapsed_time = @elapsed outputs_path = write_outputs(EP, + outputs_path, + mysetup, + myinputs) println("Time elapsed for writing is") println(elapsed_time) if mysetup["ModelingToGenerateAlternatives"] == 1 @@ -101,7 +104,6 @@ function run_genx_case_simple!(case::AbstractString, mysetup::Dict, optimizer::A end end - function run_genx_case_multistage!(case::AbstractString, mysetup::Dict, optimizer::Any) settings_path = get_settings_path(case) multistage_settings = get_settings_path(case, "multi_stage_settings.yml") # Multi stage settings YAML file path @@ -111,13 +113,14 @@ function run_genx_case_multistage!(case::AbstractString, mysetup::Dict, optimize if mysetup["TimeDomainReduction"] == 1 tdr_settings = get_settings_path(case, "time_domain_reduction_settings.yml") # Multi stage settings YAML file path TDRSettingsDict = YAML.load(open(tdr_settings)) - + first_stage_path = joinpath(case, "inputs", "inputs_p1") TDRpath = joinpath(first_stage_path, mysetup["TimeDomainReductionFolder"]) system_path = joinpath(first_stage_path, mysetup["SystemFolder"]) prevent_doubled_timedomainreduction(system_path) if !time_domain_reduced_files_exist(TDRpath) - if (mysetup["MultiStage"] == 1) && (TDRSettingsDict["MultiStageConcatenate"] == 0) + if (mysetup["MultiStage"] == 1) && + (TDRSettingsDict["MultiStageConcatenate"] == 0) println("Clustering Time Series Data (Individually)...") for stage_id in 1:mysetup["MultiStageSettingsDict"]["NumStages"] cluster_inputs(case, settings_path, mysetup, stage_id) @@ -135,8 +138,8 @@ function run_genx_case_multistage!(case::AbstractString, mysetup::Dict, optimize println("Configuring Solver") OPTIMIZER = configure_solver(settings_path, optimizer) - model_dict=Dict() - inputs_dict=Dict() + model_dict = Dict() + inputs_dict = Dict() for t in 1:mysetup["MultiStageSettingsDict"]["NumStages"] @@ -144,17 +147,18 @@ function run_genx_case_multistage!(case::AbstractString, mysetup::Dict, optimize mysetup["MultiStageSettingsDict"]["CurStage"] = t # Step 1) Load Inputs - inpath_sub = joinpath(case, "inputs", string("inputs_p",t)) + inpath_sub = joinpath(case, "inputs", string("inputs_p", t)) inputs_dict[t] = load_inputs(mysetup, inpath_sub) - inputs_dict[t] = configure_multi_stage_inputs(inputs_dict[t],mysetup["MultiStageSettingsDict"],mysetup["NetworkExpansion"]) + inputs_dict[t] = configure_multi_stage_inputs(inputs_dict[t], + mysetup["MultiStageSettingsDict"], + mysetup["NetworkExpansion"]) - compute_cumulative_min_retirements!(inputs_dict,t) + compute_cumulative_min_retirements!(inputs_dict, t) # Step 2) Generate model model_dict[t] = generate_model(mysetup, inputs_dict[t], OPTIMIZER) end - ### Solve model println("Solving Model") @@ -187,4 +191,3 @@ function run_genx_case_multistage!(case::AbstractString, mysetup::Dict, optimize write_multi_stage_outputs(mystats_d, outpath, mysetup, inputs_dict) end - diff --git a/src/configure_settings/configure_settings.jl b/src/configure_settings/configure_settings.jl index 8706c20b96..d37107f256 100644 --- a/src/configure_settings/configure_settings.jl +++ b/src/configure_settings/configure_settings.jl @@ -1,6 +1,5 @@ function default_settings() - Dict{Any,Any}( - "PrintModel" => 0, + Dict{Any, Any}("PrintModel" => 0, "OverwriteResults" => 0, "NetworkExpansion" => 0, "Trans_Loss_Segments" => 1, @@ -32,8 +31,7 @@ function default_settings() "ResourcePoliciesFolder" => "policy_assignments", "SystemFolder" => "system", "PoliciesFolder" => "policies", - "ObjScale" => 1, - ) + "ObjScale" => 1) end @doc raw""" @@ -64,7 +62,7 @@ function configure_settings(settings_path::String, output_settings_path::String) return settings end -function validate_settings!(settings::Dict{Any,Any}) +function validate_settings!(settings::Dict{Any, Any}) # Check for any settings combinations that are not allowed. # If we find any then make a response and issue a note to the user. @@ -81,20 +79,18 @@ function validate_settings!(settings::Dict{Any,Any}) if haskey(settings, "Reserves") Base.depwarn("""The Reserves setting has been deprecated. Please use the - OperationalReserves setting instead.""", :validate_settings!, force=true) + OperationalReserves setting instead.""", :validate_settings!, force = true) settings["OperationalReserves"] = settings["Reserves"] delete!(settings, "Reserves") end - if settings["EnableJuMPStringNames"]==0 && settings["ComputeConflicts"]==1 - settings["EnableJuMPStringNames"]=1; + if settings["EnableJuMPStringNames"] == 0 && settings["ComputeConflicts"] == 1 + settings["EnableJuMPStringNames"] = 1 end - end function default_writeoutput() - Dict{String,Bool}( - "WriteCosts" => true, + Dict{String, Bool}("WriteCosts" => true, "WriteCapacity" => true, "WriteCapacityValue" => true, "WriteCapacityFactor" => true, @@ -140,12 +136,10 @@ function default_writeoutput() "WriteTransmissionLosses" => true, "WriteVirtualDischarge" => true, "WriteVREStor" => true, - "WriteAngles" => true - ) + "WriteAngles" => true) end function configure_writeoutput(output_settings_path::String, settings::Dict) - writeoutput = default_writeoutput() # don't write files with hourly data if settings["WriteOutputs"] == "annual" @@ -169,4 +163,4 @@ function configure_writeoutput(output_settings_path::String, settings::Dict) merge!(writeoutput, model_writeoutput) end return writeoutput -end \ No newline at end of file +end diff --git a/src/configure_solver/configure_cbc.jl b/src/configure_solver/configure_cbc.jl index afa3f41727..0379fbd43c 100644 --- a/src/configure_solver/configure_cbc.jl +++ b/src/configure_solver/configure_cbc.jl @@ -17,26 +17,23 @@ The Cbc optimizer instance is configured with the following default parameters i """ function configure_cbc(solver_settings_path::String, optimizer::Any) - - solver_settings = YAML.load(open(solver_settings_path)) - solver_settings = convert(Dict{String, Any}, solver_settings) + solver_settings = YAML.load(open(solver_settings_path)) + solver_settings = convert(Dict{String, Any}, solver_settings) default_settings = Dict("TimeLimit" => 1e-6, - "logLevel" => 1e-6, - "maxSolutions" => -1, - "maxNodes" => -1, - "allowableGap" => -1, - "ratioGap" => Inf, - "threads" => 1, - ) + "logLevel" => 1e-6, + "maxSolutions" => -1, + "maxNodes" => -1, + "allowableGap" => -1, + "ratioGap" => Inf, + "threads" => 1) attributes = merge(default_settings, solver_settings) - key_replacement = Dict("TimeLimit" => "seconds", - ) + key_replacement = Dict("TimeLimit" => "seconds") attributes = rename_keys(attributes, key_replacement) attributes::Dict{String, Any} - return optimizer_with_attributes(optimizer, attributes...) + return optimizer_with_attributes(optimizer, attributes...) end diff --git a/src/configure_solver/configure_clp.jl b/src/configure_solver/configure_clp.jl index 2da048e989..cd5af6e42d 100644 --- a/src/configure_solver/configure_clp.jl +++ b/src/configure_solver/configure_clp.jl @@ -21,12 +21,10 @@ The Clp optimizer instance is configured with the following default parameters i """ function configure_clp(solver_settings_path::String, optimizer::Any) + solver_settings = YAML.load(open(solver_settings_path)) + solver_settings = convert(Dict{String, Any}, solver_settings) - solver_settings = YAML.load(open(solver_settings_path)) - solver_settings = convert(Dict{String, Any}, solver_settings) - - default_settings = Dict{String,Any}( - "Feasib_Tol" => 1e-7, + default_settings = Dict{String, Any}("Feasib_Tol" => 1e-7, "DualObjectiveLimit" => 1e308, "MaximumIterations" => 2147483647, "TimeLimit" => -1.0, @@ -35,16 +33,14 @@ function configure_clp(solver_settings_path::String, optimizer::Any) "Method" => 5, "InfeasibleReturn" => 0, "Scaling" => 3, - "Perturbation" => 100, - ) + "Perturbation" => 100) attributes = merge(default_settings, solver_settings) key_replacement = Dict("Feasib_Tol" => "PrimalTolerance", - "TimeLimit" => "MaximumSeconds", - "Pre_Solve" => "PresolveType", - "Method" => "SolveType", - ) + "TimeLimit" => "MaximumSeconds", + "Pre_Solve" => "PresolveType", + "Method" => "SolveType") attributes = rename_keys(attributes, key_replacement) @@ -53,5 +49,5 @@ function configure_clp(solver_settings_path::String, optimizer::Any) attributes["DualTolerance"] = attributes["PrimalTolerance"] attributes::Dict{String, Any} - return optimizer_with_attributes(optimizer, attributes...) + return optimizer_with_attributes(optimizer, attributes...) end diff --git a/src/configure_solver/configure_cplex.jl b/src/configure_solver/configure_cplex.jl index fe860fb67d..b320857217 100644 --- a/src/configure_solver/configure_cplex.jl +++ b/src/configure_solver/configure_cplex.jl @@ -78,40 +78,35 @@ The optimizer instance is configured with the following default parameters if a Any other attributes in the settings file (which typically start with `CPX_PARAM_`) will also be passed to the solver. """ function configure_cplex(solver_settings_path::String, optimizer::Any) - solver_settings = YAML.load(open(solver_settings_path)) solver_settings = convert(Dict{String, Any}, solver_settings) default_settings = Dict("Feasib_Tol" => 1e-6, - "Optimal_Tol" => 1e-4, - "AggFill" => 10, - "PreDual" => 0, - "TimeLimit" => 1e+75, - "MIPGap" => 1e-3, - "Method" => 0, - "BarConvTol" => 1e-8, - "NumericFocus" => 0, - "BarObjRng" => 1e+75, - "SolutionType" => 2, - ) - + "Optimal_Tol" => 1e-4, + "AggFill" => 10, + "PreDual" => 0, + "TimeLimit" => 1e+75, + "MIPGap" => 1e-3, + "Method" => 0, + "BarConvTol" => 1e-8, + "NumericFocus" => 0, + "BarObjRng" => 1e+75, + "SolutionType" => 2) attributes = merge(default_settings, solver_settings) - key_replacement = Dict( - "Feasib_Tol" => "CPX_PARAM_EPRHS", - "Optimal_Tol" => "CPX_PARAM_EPOPT", - "AggFill" => "CPX_PARAM_AGGFILL", - "PreDual" => "CPX_PARAM_PREDUAL", - "TimeLimit" => "CPX_PARAM_TILIM", - "MIPGap" => "CPX_PARAM_EPGAP", - "Method" => "CPX_PARAM_LPMETHOD", - "Pre_Solve" => "CPX_PARAM_PREIND", # https://www.ibm.com/docs/en/icos/12.8.0.0?topic=parameters-presolve-switch - "BarConvTol" => "CPX_PARAM_BAREPCOMP", - "NumericFocus" => "CPX_PARAM_NUMERICALEMPHASIS", - "BarObjRng" => "CPX_PARAM_BAROBJRNG", - "SolutionType" => "CPX_PARAM_SOLUTIONTYPE", - ) + key_replacement = Dict("Feasib_Tol" => "CPX_PARAM_EPRHS", + "Optimal_Tol" => "CPX_PARAM_EPOPT", + "AggFill" => "CPX_PARAM_AGGFILL", + "PreDual" => "CPX_PARAM_PREDUAL", + "TimeLimit" => "CPX_PARAM_TILIM", + "MIPGap" => "CPX_PARAM_EPGAP", + "Method" => "CPX_PARAM_LPMETHOD", + "Pre_Solve" => "CPX_PARAM_PREIND", # https://www.ibm.com/docs/en/icos/12.8.0.0?topic=parameters-presolve-switch + "BarConvTol" => "CPX_PARAM_BAREPCOMP", + "NumericFocus" => "CPX_PARAM_NUMERICALEMPHASIS", + "BarObjRng" => "CPX_PARAM_BAROBJRNG", + "SolutionType" => "CPX_PARAM_SOLUTIONTYPE") attributes = rename_keys(attributes, key_replacement) attributes::Dict{String, Any} diff --git a/src/configure_solver/configure_gurobi.jl b/src/configure_solver/configure_gurobi.jl index 2e5c8b7d39..00f132f34f 100644 --- a/src/configure_solver/configure_gurobi.jl +++ b/src/configure_solver/configure_gurobi.jl @@ -21,33 +21,30 @@ The Gurobi optimizer instance is configured with the following default parameter """ function configure_gurobi(solver_settings_path::String, optimizer::Any) - - solver_settings = YAML.load(open(solver_settings_path)) - solver_settings = convert(Dict{String, Any}, solver_settings) + solver_settings = YAML.load(open(solver_settings_path)) + solver_settings = convert(Dict{String, Any}, solver_settings) default_settings = Dict("Feasib_Tol" => 1e-6, - "Optimal_Tol" => 1e-4, - "Pre_Solve" => -1, - "AggFill" => -1, - "PreDual" => -1, - "TimeLimit" => Inf, - "MIPGap" => 1e-3, - "Crossover" => -1, - "Method" => -1, - "BarConvTol" => 1e-8, - "NumericFocus" => 0, - "OutputFlag" => 1 - ) + "Optimal_Tol" => 1e-4, + "Pre_Solve" => -1, + "AggFill" => -1, + "PreDual" => -1, + "TimeLimit" => Inf, + "MIPGap" => 1e-3, + "Crossover" => -1, + "Method" => -1, + "BarConvTol" => 1e-8, + "NumericFocus" => 0, + "OutputFlag" => 1) attributes = merge(default_settings, solver_settings) key_replacement = Dict("Feasib_Tol" => "FeasibilityTol", - "Optimal_Tol" => "OptimalityTol", - "Pre_Solve" => "Presolve", - ) + "Optimal_Tol" => "OptimalityTol", + "Pre_Solve" => "Presolve") attributes = rename_keys(attributes, key_replacement) attributes::Dict{String, Any} - return optimizer_with_attributes(optimizer, attributes...) + return optimizer_with_attributes(optimizer, attributes...) end diff --git a/src/configure_solver/configure_highs.jl b/src/configure_solver/configure_highs.jl index d0bab8e835..549395d8dc 100644 --- a/src/configure_solver/configure_highs.jl +++ b/src/configure_solver/configure_highs.jl @@ -33,30 +33,26 @@ The HiGHS optimizer instance is configured with the following default parameters mip_abs_gap: 1e-06 """ function configure_highs(solver_settings_path::String, optimizer::Any) + solver_settings = YAML.load(open(solver_settings_path)) + solver_settings = convert(Dict{String, Any}, solver_settings) - solver_settings = YAML.load(open(solver_settings_path)) - solver_settings = convert(Dict{String, Any}, solver_settings) - - default_settings = Dict{String,Any}( - "Feasib_Tol" => 1e-6, + default_settings = Dict{String, Any}("Feasib_Tol" => 1e-6, "Optimal_Tol" => 1e-4, "Pre_Solve" => "choose", "TimeLimit" => Inf, "Method" => "ipm", "ipm_optimality_tolerance" => 1e-08, - "run_crossover" => "off", - "mip_rel_gap" => 0.001, - "mip_abs_gap" => 1e-06, - ) + "run_crossover" => "off", + "mip_rel_gap" => 0.001, + "mip_abs_gap" => 1e-06) attributes = merge(default_settings, solver_settings) key_replacement = Dict("Feasib_Tol" => "primal_feasibility_tolerance", - "Optimal_Tol" => "dual_feasibility_tolerance", - "TimeLimit" => "time_limit", - "Pre_Solve" => "presolve", - "Method" => "solver", - ) + "Optimal_Tol" => "dual_feasibility_tolerance", + "TimeLimit" => "time_limit", + "Pre_Solve" => "presolve", + "Method" => "solver") attributes = rename_keys(attributes, key_replacement) diff --git a/src/configure_solver/configure_scip.jl b/src/configure_solver/configure_scip.jl index 3609657d66..591d36eeb7 100644 --- a/src/configure_solver/configure_scip.jl +++ b/src/configure_solver/configure_scip.jl @@ -12,21 +12,18 @@ The SCIP optimizer instance is configured with the following default parameters """ function configure_scip(solver_settings_path::String, optimizer::Any) - - solver_settings = YAML.load(open(solver_settings_path)) - solver_settings = convert(Dict{String, Any}, solver_settings) + solver_settings = YAML.load(open(solver_settings_path)) + solver_settings = convert(Dict{String, Any}, solver_settings) default_settings = Dict("Dispverblevel" => 0, - "limitsgap" => 0.05, - ) + "limitsgap" => 0.05) attributes = merge(default_settings, solver_settings) key_replacement = Dict("Dispverblevel" => "display_verblevel", - "limitsgap" => "limits_gap", - ) + "limitsgap" => "limits_gap") attributes = rename_keys(attributes, key_replacement) attributes::Dict{String, Any} - return optimizer_with_attributes(optimizer, attributes...) + return optimizer_with_attributes(optimizer, attributes...) end diff --git a/src/configure_solver/configure_solver.jl b/src/configure_solver/configure_solver.jl index 96a8bb2e02..76cf01cca7 100644 --- a/src/configure_solver/configure_solver.jl +++ b/src/configure_solver/configure_solver.jl @@ -6,7 +6,6 @@ function infer_solver(optimizer::Any) return lowercase(string(parentmodule(optimizer))) end - @doc raw""" configure_solver(solver_settings_path::String, optimizer::Any) @@ -24,15 +23,13 @@ function configure_solver(solver_settings_path::String, optimizer::Any) solver_name = infer_solver(optimizer) path = joinpath(solver_settings_path, solver_name * "_settings.yml") - configure_functions = Dict( - "highs" => configure_highs, + configure_functions = Dict("highs" => configure_highs, "gurobi" => configure_gurobi, "cplex" => configure_cplex, "clp" => configure_clp, "cbc" => configure_cbc, - "scip" => configure_scip, - ) - + "scip" => configure_scip) + return configure_functions[solver_name](path, optimizer) end @@ -50,7 +47,8 @@ function rename_keys(attributes::Dict, new_key_names::Dict) else new_key = new_key_names[old_key] if haskey(attributes, new_key) - @error "Colliding keys: '$old_key' needs to be renamed to '$new_key' but '$new_key' already exists in", attributes + @error "Colliding keys: '$old_key' needs to be renamed to '$new_key' but '$new_key' already exists in", + attributes end end updated_attributes[new_key] = value diff --git a/src/load_inputs/load_cap_reserve_margin.jl b/src/load_inputs/load_cap_reserve_margin.jl index 646385d078..0a652bc78f 100644 --- a/src/load_inputs/load_cap_reserve_margin.jl +++ b/src/load_inputs/load_cap_reserve_margin.jl @@ -5,12 +5,12 @@ Read input parameters related to planning reserve margin constraints """ function load_cap_reserve_margin!(setup::Dict, path::AbstractString, inputs::Dict) scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 - + filename = "Capacity_reserve_margin_slack.csv" if isfile(joinpath(path, filename)) df = load_dataframe(joinpath(path, filename)) inputs["dfCapRes_slack"] = df - inputs["dfCapRes_slack"][!,:PriceCap] ./= scale_factor # Million $/GW if scaled, $/MW if not scaled + inputs["dfCapRes_slack"][!, :PriceCap] ./= scale_factor # Million $/GW if scaled, $/MW if not scaled end filename = "Capacity_reserve_margin.csv" diff --git a/src/load_inputs/load_co2_cap.jl b/src/load_inputs/load_co2_cap.jl index 0c93f3c199..08e6802a0a 100644 --- a/src/load_inputs/load_co2_cap.jl +++ b/src/load_inputs/load_co2_cap.jl @@ -5,14 +5,14 @@ Read input parameters related to CO$_2$ emissions cap constraints """ function load_co2_cap!(setup::Dict, path::AbstractString, inputs::Dict) scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 - + filename = "CO2_cap_slack.csv" if isfile(joinpath(path, filename)) df = load_dataframe(joinpath(path, filename)) inputs["dfCO2Cap_slack"] = df - inputs["dfCO2Cap_slack"][!,:PriceCap] ./= scale_factor # Million $/kton if scaled, $/ton if not scaled - end - + inputs["dfCO2Cap_slack"][!, :PriceCap] ./= scale_factor # Million $/kton if scaled, $/ton if not scaled + end + filename = "CO2_cap.csv" df = load_dataframe(joinpath(path, filename)) @@ -21,7 +21,6 @@ function load_co2_cap!(setup::Dict, path::AbstractString, inputs::Dict) inputs["dfCO2CapZones"] = mat inputs["NCO2Cap"] = size(mat, 2) - # Emission limits if setup["CO2Cap"] == 1 # CO2 emissions cap in mass diff --git a/src/load_inputs/load_dataframe.jl b/src/load_inputs/load_dataframe.jl index b6ca5ef552..64aa112968 100644 --- a/src/load_inputs/load_dataframe.jl +++ b/src/load_inputs/load_dataframe.jl @@ -64,7 +64,9 @@ function load_dataframe(dir::AbstractString, basenames::Vector{String})::DataFra target = look_for_file_with_alternate_case(dir, base) # admonish if target != FILENOTFOUND - Base.depwarn("""The filename '$target' is deprecated. '$best_basename' is preferred.""", :load_dataframe, force=true) + Base.depwarn("""The filename '$target' is deprecated. '$best_basename' is preferred.""", + :load_dataframe, + force = true) return load_dataframe_from_file(joinpath(dir, target)) end end @@ -107,7 +109,7 @@ end function keep_duplicated_entries!(s, uniques) for u in uniques - deleteat!(s, first(findall(x->x==u, s))) + deleteat!(s, first(findall(x -> x == u, s))) end return s end @@ -126,23 +128,23 @@ end function load_dataframe_from_file(path)::DataFrame check_for_duplicate_keys(path) - CSV.read(path, DataFrame, header=1) + CSV.read(path, DataFrame, header = 1) end function find_matrix_columns_in_dataframe(df::DataFrame, - columnprefix::AbstractString; - prefixseparator='_')::Vector{Int} + columnprefix::AbstractString; + prefixseparator = '_')::Vector{Int} all_columns = names(df) # 2 is the length of the '_' connector plus one for indexing - get_integer_part(c) = tryparse(Int, c[length(columnprefix)+2:end]) + get_integer_part(c) = tryparse(Int, c[(length(columnprefix) + 2):end]) # if prefix is "ESR", the column name should be like "ESR_1" function is_of_this_column_type(c) startswith(c, columnprefix) && - length(c) >= length(columnprefix) + 2 && - c[length(columnprefix) + 1] == prefixseparator && - !isnothing(get_integer_part(c)) + length(c) >= length(columnprefix) + 2 && + c[length(columnprefix) + 1] == prefixseparator && + !isnothing(get_integer_part(c)) end columns = filter(is_of_this_column_type, all_columns) @@ -164,11 +166,13 @@ ESR_1, other_thing, ESR_3, ESR_2, 0.4, 2, 0.6, 0.5, ``` """ -function extract_matrix_from_dataframe(df::DataFrame, columnprefix::AbstractString; prefixseparator='_') +function extract_matrix_from_dataframe(df::DataFrame, + columnprefix::AbstractString; + prefixseparator = '_') all_columns = names(df) columnnumbers = find_matrix_columns_in_dataframe(df, - columnprefix, - prefixseparator=prefixseparator) + columnprefix, + prefixseparator = prefixseparator) if length(columnnumbers) == 0 msg = """an input dataframe with columns $all_columns was searched for @@ -188,10 +192,13 @@ function extract_matrix_from_dataframe(df::DataFrame, columnprefix::AbstractStri Matrix(dropmissing(df[:, sorted_columns])) end -function extract_matrix_from_resources(rs::Vector{T}, columnprefix::AbstractString, default=0.0) where T<:AbstractResource +function extract_matrix_from_resources(rs::Vector{T}, + columnprefix::AbstractString, + default = 0.0) where {T <: AbstractResource} # attributes starting with columnprefix with a numeric suffix - attributes_n = [attr for attr in string.(attributes(rs[1])) if startswith(attr, columnprefix)] + attributes_n = [attr + for attr in string.(attributes(rs[1])) if startswith(attr, columnprefix)] # sort the attributes by the numeric suffix sort!(attributes_n, by = x -> parse(Int, split(x, "_")[end])) @@ -216,7 +223,7 @@ Check that the dataframe has all the required columns. - `df_name::AbstractString`: the name of the dataframe, for error messages - `required_cols::Vector{AbstractString}`: the names of the required columns """ -function validate_df_cols(df::DataFrame, df_name::AbstractString, required_cols) +function validate_df_cols(df::DataFrame, df_name::AbstractString, required_cols) for col in required_cols if col ∉ names(df) error("$df_name data file is missing column $col") diff --git a/src/load_inputs/load_demand_data.jl b/src/load_inputs/load_demand_data.jl index 509d0216bb..4c0e8a0319 100644 --- a/src/load_inputs/load_demand_data.jl +++ b/src/load_inputs/load_demand_data.jl @@ -3,14 +3,16 @@ function get_demand_dataframe(path) deprecated_synonym = "Load_data.csv" df = load_dataframe(path, [filename, deprecated_synonym]) # update column names - old_columns = find_matrix_columns_in_dataframe(df, DEMAND_COLUMN_PREFIX_DEPRECATED()[1:end-1], - prefixseparator='z') - old_column_symbols = Symbol.(DEMAND_COLUMN_PREFIX_DEPRECATED()*string(i) for i in old_columns) + old_columns = find_matrix_columns_in_dataframe(df, + DEMAND_COLUMN_PREFIX_DEPRECATED()[1:(end - 1)], + prefixseparator = 'z') + old_column_symbols = Symbol.(DEMAND_COLUMN_PREFIX_DEPRECATED() * string(i) + for i in old_columns) if length(old_column_symbols) > 0 pref_prefix = DEMAND_COLUMN_PREFIX() dep_prefix = DEMAND_COLUMN_PREFIX_DEPRECATED() @info "$dep_prefix is deprecated. Use $pref_prefix." - new_column_symbols = Symbol.(DEMAND_COLUMN_PREFIX()*string(i) for i in old_columns) + new_column_symbols = Symbol.(DEMAND_COLUMN_PREFIX() * string(i) for i in old_columns) rename!(df, Dict(old_column_symbols .=> new_column_symbols)) end return df @@ -26,7 +28,7 @@ Read input parameters related to electricity demand (load) """ function load_demand_data!(setup::Dict, path::AbstractString, inputs::Dict) - # Load related inputs + # Load related inputs TDR_directory = joinpath(path, setup["TimeDomainReductionFolder"]) # if TDR is used, my_dir = TDR_directory, else my_dir = "system" my_dir = get_systemfiles_path(setup, TDR_directory, path) @@ -35,17 +37,17 @@ function load_demand_data!(setup::Dict, path::AbstractString, inputs::Dict) as_vector(col::Symbol) = collect(skipmissing(demand_in[!, col])) - # Number of time steps (periods) + # Number of time steps (periods) T = length(as_vector(:Time_Index)) - # Number of demand curtailment/lost load segments + # Number of demand curtailment/lost load segments SEG = length(as_vector(:Demand_Segment)) - ## Set indices for internal use + ## Set indices for internal use inputs["T"] = T inputs["SEG"] = SEG - Z = inputs["Z"] # Number of zones + Z = inputs["Z"] # Number of zones - inputs["omega"] = zeros(Float64, T) # weights associated with operational sub-period in the model - sum of weight = 8760 + inputs["omega"] = zeros(Float64, T) # weights associated with operational sub-period in the model - sum of weight = 8760 # Weights for each period - assumed same weights for each sub-period within a period inputs["Weights"] = as_vector(:Sub_Weights) # Weights each period @@ -56,30 +58,31 @@ function load_demand_data!(setup::Dict, path::AbstractString, inputs::Dict) # Creating sub-period weights from weekly weights for w in 1:inputs["REP_PERIOD"] for h in 1:inputs["H"] - t = inputs["H"]*(w-1)+h - inputs["omega"][t] = inputs["Weights"][w]/inputs["H"] + t = inputs["H"] * (w - 1) + h + inputs["omega"][t] = inputs["Weights"][w] / inputs["H"] end end - # Create time set steps indicies - inputs["hours_per_subperiod"] = div.(T,inputs["REP_PERIOD"]) # total number of hours per subperiod - hours_per_subperiod = inputs["hours_per_subperiod"] # set value for internal use + # Create time set steps indicies + inputs["hours_per_subperiod"] = div.(T, inputs["REP_PERIOD"]) # total number of hours per subperiod + hours_per_subperiod = inputs["hours_per_subperiod"] # set value for internal use - inputs["START_SUBPERIODS"] = 1:hours_per_subperiod:T # set of indexes for all time periods that start a subperiod (e.g. sample day/week) - inputs["INTERIOR_SUBPERIODS"] = setdiff(1:T, inputs["START_SUBPERIODS"]) # set of indexes for all time periods that do not start a subperiod + inputs["START_SUBPERIODS"] = 1:hours_per_subperiod:T # set of indexes for all time periods that start a subperiod (e.g. sample day/week) + inputs["INTERIOR_SUBPERIODS"] = setdiff(1:T, inputs["START_SUBPERIODS"]) # set of indexes for all time periods that do not start a subperiod - # Demand in MW for each zone + # Demand in MW for each zone scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 # Max value of non-served energy inputs["Voll"] = as_vector(:Voll) / scale_factor # convert from $/MWh $ million/GWh (assuming objective is divided by 1000) # Demand in MW inputs["pD"] = extract_matrix_from_dataframe(demand_in, - DEMAND_COLUMN_PREFIX()[1:end-1], - prefixseparator='z') / scale_factor + DEMAND_COLUMN_PREFIX()[1:(end - 1)], + prefixseparator = 'z') / scale_factor - # Cost of non-served energy/demand curtailment + # Cost of non-served energy/demand curtailment # Cost of each segment reported as a fraction of value of non-served energy - scaled implicitly - inputs["pC_D_Curtail"] = as_vector(:Cost_of_Demand_Curtailment_per_MW) * inputs["Voll"][1] + inputs["pC_D_Curtail"] = as_vector(:Cost_of_Demand_Curtailment_per_MW) * + inputs["Voll"][1] # Maximum hourly demand curtailable as % of the max demand (for each segment) inputs["pMax_D_Curtail"] = as_vector(:Max_Demand_Curtailment) @@ -106,13 +109,13 @@ function validatetimebasis(inputs::Dict) expected_length_2 = H * number_of_representative_periods check_equal = [T, - demand_length, - generators_variability_length, - fuel_costs_length, - expected_length_1, - expected_length_2] + demand_length, + generators_variability_length, + fuel_costs_length, + expected_length_1, + expected_length_2] - allequal(x) = all(y->y==x[1], x) + allequal(x) = all(y -> y == x[1], x) ok = allequal(check_equal) if ~ok @@ -160,7 +163,6 @@ This function prevents TimeDomainReduction from running on a case which already has more than one Representative Period or has more than one Sub_Weight specified. """ function prevent_doubled_timedomainreduction(path::AbstractString) - demand_in = get_demand_dataframe(path) as_vector(col::Symbol) = collect(skipmissing(demand_in[!, col])) representative_periods = convert(Int16, as_vector(:Rep_Periods)[1]) @@ -174,5 +176,4 @@ function prevent_doubled_timedomainreduction(path::AbstractString) and the number of subperiod weight entries (:Sub_Weights) is ($num_sub_weights). Each of these must be 1: only a single period can have TimeDomainReduction applied.""") end - end diff --git a/src/load_inputs/load_energy_share_requirement.jl b/src/load_inputs/load_energy_share_requirement.jl index af6ef9b786..02b96fe7e7 100644 --- a/src/load_inputs/load_energy_share_requirement.jl +++ b/src/load_inputs/load_energy_share_requirement.jl @@ -11,9 +11,9 @@ function load_energy_share_requirement!(setup::Dict, path::AbstractString, input if isfile(joinpath(path, filename)) df = load_dataframe(joinpath(path, filename)) inputs["dfESR_slack"] = df - inputs["dfESR_slack"][!,:PriceCap] ./= scale_factor # million $/GWh if scaled, $/MWh if not scaled - end - + inputs["dfESR_slack"][!, :PriceCap] ./= scale_factor # million $/GWh if scaled, $/MWh if not scaled + end + filename = "Energy_share_requirement.csv" df = load_dataframe(joinpath(path, filename)) mat = extract_matrix_from_dataframe(df, "ESR") diff --git a/src/load_inputs/load_fuels_data.jl b/src/load_inputs/load_fuels_data.jl index aa64ff43fa..61b0ff2f0f 100644 --- a/src/load_inputs/load_fuels_data.jl +++ b/src/load_inputs/load_fuels_data.jl @@ -9,7 +9,7 @@ function load_fuels_data!(setup::Dict, path::AbstractString, inputs::Dict) TDR_directory = joinpath(path, setup["TimeDomainReductionFolder"]) # if TDR is used, my_dir = TDR_directory, else my_dir = "system" my_dir = get_systemfiles_path(setup, TDR_directory, path) - + filename = "Fuels_data.csv" fuels_in = load_dataframe(joinpath(my_dir, filename)) @@ -26,11 +26,11 @@ function load_fuels_data!(setup::Dict, path::AbstractString, inputs::Dict) scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 - for i = 1:length(fuels) - # fuel cost is in $/MMBTU w/o scaling, $/Billon BTU w/ scaling - fuel_costs[fuels[i]] = costs[:,i] / scale_factor - # No need to scale fuel_CO2, fuel_CO2 is ton/MMBTU or kton/Billion BTU - fuel_CO2[fuels[i]] = CO2_content[i] + for i in 1:length(fuels) + # fuel cost is in $/MMBTU w/o scaling, $/Billon BTU w/ scaling + fuel_costs[fuels[i]] = costs[:, i] / scale_factor + # No need to scale fuel_CO2, fuel_CO2 is ton/MMBTU or kton/Billion BTU + fuel_CO2[fuels[i]] = CO2_content[i] end inputs["fuels"] = fuels diff --git a/src/load_inputs/load_generators_variability.jl b/src/load_inputs/load_generators_variability.jl index 1ca02162ec..99294bffed 100644 --- a/src/load_inputs/load_generators_variability.jl +++ b/src/load_inputs/load_generators_variability.jl @@ -5,11 +5,11 @@ Read input parameters related to hourly maximum capacity factors for generators, """ function load_generators_variability!(setup::Dict, path::AbstractString, inputs::Dict) - # Hourly capacity factors + # Hourly capacity factors TDR_directory = joinpath(path, setup["TimeDomainReductionFolder"]) # if TDR is used, my_dir = TDR_directory, else my_dir = "system" my_dir = get_systemfiles_path(setup, TDR_directory, path) - + filename = "Generators_variability.csv" gen_var = load_dataframe(joinpath(my_dir, filename)) @@ -23,11 +23,12 @@ function load_generators_variability!(setup::Dict, path::AbstractString, inputs: end end - # Reorder DataFrame to R_ID order - select!(gen_var, [:Time_Index; Symbol.(all_resources) ]) + # Reorder DataFrame to R_ID order + select!(gen_var, [:Time_Index; Symbol.(all_resources)]) - # Maximum power output and variability of each energy resource - inputs["pP_Max"] = transpose(Matrix{Float64}(gen_var[1:inputs["T"],2:(inputs["G"]+1)])) + # Maximum power output and variability of each energy resource + inputs["pP_Max"] = transpose(Matrix{Float64}(gen_var[1:inputs["T"], + 2:(inputs["G"] + 1)])) - println(filename * " Successfully Read!") + println(filename * " Successfully Read!") end diff --git a/src/load_inputs/load_inputs.jl b/src/load_inputs/load_inputs.jl index 9ef747a0ed..1b8705ec4e 100644 --- a/src/load_inputs/load_inputs.jl +++ b/src/load_inputs/load_inputs.jl @@ -9,94 +9,95 @@ path - string path to working directory returns: Dict (dictionary) object containing all data inputs """ -function load_inputs(setup::Dict,path::AbstractString) - - ## Read input files - println("Reading Input CSV Files") - ## input paths - system_path = joinpath(path, setup["SystemFolder"]) - resources_path = joinpath(path, setup["ResourcesFolder"]) - policies_path = joinpath(path, setup["PoliciesFolder"]) - ## Declare Dict (dictionary) object used to store parameters - inputs = Dict() - # Read input data about power network topology, operating and expansion attributes - if isfile(joinpath(system_path,"Network.csv")) - network_var = load_network_data!(setup, system_path, inputs) - else - inputs["Z"] = 1 - inputs["L"] = 0 - end - - # Read temporal-resolved load data, and clustering information if relevant - load_demand_data!(setup, path, inputs) - # Read fuel cost data, including time-varying fuel costs - load_fuels_data!(setup, path, inputs) - # Read in generator/resource related inputs - load_resources_data!(inputs, setup, path, resources_path) - # Read in generator/resource availability profiles - load_generators_variability!(setup, path, inputs) +function load_inputs(setup::Dict, path::AbstractString) + + ## Read input files + println("Reading Input CSV Files") + ## input paths + system_path = joinpath(path, setup["SystemFolder"]) + resources_path = joinpath(path, setup["ResourcesFolder"]) + policies_path = joinpath(path, setup["PoliciesFolder"]) + ## Declare Dict (dictionary) object used to store parameters + inputs = Dict() + # Read input data about power network topology, operating and expansion attributes + if isfile(joinpath(system_path, "Network.csv")) + network_var = load_network_data!(setup, system_path, inputs) + else + inputs["Z"] = 1 + inputs["L"] = 0 + end + + # Read temporal-resolved load data, and clustering information if relevant + load_demand_data!(setup, path, inputs) + # Read fuel cost data, including time-varying fuel costs + load_fuels_data!(setup, path, inputs) + # Read in generator/resource related inputs + load_resources_data!(inputs, setup, path, resources_path) + # Read in generator/resource availability profiles + load_generators_variability!(setup, path, inputs) validatetimebasis(inputs) - if setup["CapacityReserveMargin"]==1 - load_cap_reserve_margin!(setup, policies_path, inputs) - if inputs["Z"] >1 - load_cap_reserve_margin_trans!(setup, inputs, network_var) - end - end + if setup["CapacityReserveMargin"] == 1 + load_cap_reserve_margin!(setup, policies_path, inputs) + if inputs["Z"] > 1 + load_cap_reserve_margin_trans!(setup, inputs, network_var) + end + end - # Read in general configuration parameters for operational reserves (resource-specific reserve parameters are read in load_resources_data) - if setup["OperationalReserves"]==1 - load_operational_reserves!(setup, system_path, inputs) - end + # Read in general configuration parameters for operational reserves (resource-specific reserve parameters are read in load_resources_data) + if setup["OperationalReserves"] == 1 + load_operational_reserves!(setup, system_path, inputs) + end - if setup["MinCapReq"] == 1 - load_minimum_capacity_requirement!(policies_path, inputs, setup) - end + if setup["MinCapReq"] == 1 + load_minimum_capacity_requirement!(policies_path, inputs, setup) + end - if setup["MaxCapReq"] == 1 - load_maximum_capacity_requirement!(policies_path, inputs, setup) - end + if setup["MaxCapReq"] == 1 + load_maximum_capacity_requirement!(policies_path, inputs, setup) + end - if setup["EnergyShareRequirement"]==1 - load_energy_share_requirement!(setup, policies_path, inputs) - end + if setup["EnergyShareRequirement"] == 1 + load_energy_share_requirement!(setup, policies_path, inputs) + end - if setup["CO2Cap"] >= 1 - load_co2_cap!(setup, policies_path, inputs) - end + if setup["CO2Cap"] >= 1 + load_co2_cap!(setup, policies_path, inputs) + end - if !isempty(inputs["VRE_STOR"]) - load_vre_stor_variability!(setup, path, inputs) - end + if !isempty(inputs["VRE_STOR"]) + load_vre_stor_variability!(setup, path, inputs) + end - # Read in mapping of modeled periods to representative periods - if is_period_map_necessary(inputs) && is_period_map_exist(setup, path) - load_period_map!(setup, path, inputs) - end + # Read in mapping of modeled periods to representative periods + if is_period_map_necessary(inputs) && is_period_map_exist(setup, path) + load_period_map!(setup, path, inputs) + end - # Virtual charge discharge cost - scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 - inputs["VirtualChargeDischargeCost"] = setup["VirtualChargeDischargeCost"] / scale_factor + # Virtual charge discharge cost + scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 + inputs["VirtualChargeDischargeCost"] = setup["VirtualChargeDischargeCost"] / + scale_factor - println("CSV Files Successfully Read In From $path") + println("CSV Files Successfully Read In From $path") - return inputs + return inputs end function is_period_map_necessary(inputs::Dict) - multiple_rep_periods = inputs["REP_PERIOD"] > 1 - has_stor_lds = !isempty(inputs["STOR_LONG_DURATION"]) - has_hydro_lds = !isempty(inputs["STOR_HYDRO_LONG_DURATION"]) - has_vre_stor_lds = !isempty(inputs["VRE_STOR"]) && !isempty(inputs["VS_LDS"]) + multiple_rep_periods = inputs["REP_PERIOD"] > 1 + has_stor_lds = !isempty(inputs["STOR_LONG_DURATION"]) + has_hydro_lds = !isempty(inputs["STOR_HYDRO_LONG_DURATION"]) + has_vre_stor_lds = !isempty(inputs["VRE_STOR"]) && !isempty(inputs["VS_LDS"]) multiple_rep_periods && (has_stor_lds || has_hydro_lds || has_vre_stor_lds) end function is_period_map_exist(setup::Dict, path::AbstractString) - filename = "Period_map.csv" - is_in_system_dir = isfile(joinpath(path, setup["SystemFolder"], filename)) - is_in_TDR_dir = isfile(joinpath(path, setup["TimeDomainReductionFolder"], filename)) - is_in_system_dir || is_in_TDR_dir + filename = "Period_map.csv" + is_in_system_dir = isfile(joinpath(path, setup["SystemFolder"], filename)) + is_in_TDR_dir = isfile(joinpath(path, setup["TimeDomainReductionFolder"], filename)) + is_in_system_dir || is_in_TDR_dir end """ @@ -115,17 +116,21 @@ Parameters: Returns: - String: The directory path based on the setup parameters. """ -function get_systemfiles_path(setup::Dict, TDR_directory::AbstractString, path::AbstractString) +function get_systemfiles_path(setup::Dict, + TDR_directory::AbstractString, + path::AbstractString) if setup["TimeDomainReduction"] == 1 && time_domain_reduced_files_exist(TDR_directory) return TDR_directory else - # If TDR is not used, then use the "system" directory specified in the setup + # If TDR is not used, then use the "system" directory specified in the setup return joinpath(path, setup["SystemFolder"]) end end abstract type AbstractLogMsg end -struct ErrorMsg <: AbstractLogMsg msg::String end -struct WarnMsg <: AbstractLogMsg msg::String end - - +struct ErrorMsg <: AbstractLogMsg + msg::String +end +struct WarnMsg <: AbstractLogMsg + msg::String +end diff --git a/src/load_inputs/load_minimum_capacity_requirement.jl b/src/load_inputs/load_minimum_capacity_requirement.jl index fad1fbd165..d30f2d6425 100644 --- a/src/load_inputs/load_minimum_capacity_requirement.jl +++ b/src/load_inputs/load_minimum_capacity_requirement.jl @@ -6,14 +6,14 @@ Read input parameters related to mimimum capacity requirement constraints (e.g. function load_minimum_capacity_requirement!(path::AbstractString, inputs::Dict, setup::Dict) filename = "Minimum_capacity_requirement.csv" df = load_dataframe(joinpath(path, filename)) - NumberOfMinCapReqs = length(df[!,:MinCapReqConstraint]) + NumberOfMinCapReqs = length(df[!, :MinCapReqConstraint]) inputs["NumberOfMinCapReqs"] = NumberOfMinCapReqs - inputs["MinCapReq"] = df[!,:Min_MW] + inputs["MinCapReq"] = df[!, :Min_MW] if setup["ParameterScale"] == 1 inputs["MinCapReq"] /= ModelScalingFactor # Convert to GW end if "PriceCap" in names(df) - inputs["MinCapPriceCap"] = df[!,:PriceCap] + inputs["MinCapPriceCap"] = df[!, :PriceCap] if setup["ParameterScale"] == 1 inputs["MinCapPriceCap"] /= ModelScalingFactor # Convert to million $/GW end diff --git a/src/load_inputs/load_multistage_data.jl b/src/load_inputs/load_multistage_data.jl index edd5021839..95395c726f 100644 --- a/src/load_inputs/load_multistage_data.jl +++ b/src/load_inputs/load_multistage_data.jl @@ -15,7 +15,7 @@ end function validate_multistage_data!(multistage_df::DataFrame) # cols that the user must provide - required_cols = ("lifetime","capital_recovery_period") + required_cols = ("lifetime", "capital_recovery_period") # check that all required columns are present for col in required_cols if col ∉ names(multistage_df) @@ -26,17 +26,16 @@ end function scale_multistage_data!(multistage_in::DataFrame, scale_factor::Float64) columns_to_scale = [:min_retired_cap_mw, # to GW - :min_retired_charge_cap_mw, # to GW - :min_retired_energy_cap_mw, # to GW - - :min_retired_cap_inverter_mw, - :min_retired_cap_solar_mw, - :min_retired_cap_wind_mw, - :min_retired_cap_charge_dc_mw, - :min_retired_cap_charge_ac_mw, - :min_retired_cap_discharge_dc_mw, - :min_retired_cap_discharge_ac_mw, - ] + :min_retired_charge_cap_mw, # to GW + :min_retired_energy_cap_mw, # to GW + :min_retired_cap_inverter_mw, + :min_retired_cap_solar_mw, + :min_retired_cap_wind_mw, + :min_retired_cap_charge_dc_mw, + :min_retired_cap_charge_ac_mw, + :min_retired_cap_discharge_dc_mw, + :min_retired_cap_discharge_ac_mw, + ] scale_columns!(multistage_in, columns_to_scale, scale_factor) return nothing -end \ No newline at end of file +end diff --git a/src/load_inputs/load_network_data.jl b/src/load_inputs/load_network_data.jl index 8116eaf02f..ac7f2b1c8c 100644 --- a/src/load_inputs/load_network_data.jl +++ b/src/load_inputs/load_network_data.jl @@ -4,7 +4,6 @@ Function for reading input parameters related to the electricity transmission network """ function load_network_data!(setup::Dict, path::AbstractString, inputs_nw::Dict) - scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 filename = "Network.csv" @@ -40,42 +39,46 @@ function load_network_data!(setup::Dict, path::AbstractString, inputs_nw::Dict) if setup["DC_OPF"] == 1 if setup["NetworkExpansion"] == 1 @warn("Because the DC_OPF flag is active, GenX will not allow any transmission capacity expansion. Set the DC_OPF flag to 0 if you want to optimize tranmission capacity expansion.") - setup["NetworkExpansion"] = 0; + setup["NetworkExpansion"] = 0 end println("Reading DC-OPF values...") # Transmission line voltage (in kV) line_voltage_kV = to_floats(:Line_Voltage_kV) # Transmission line reactance (in Ohms) - line_reactance_Ohms = to_floats(:Line_Reactance_Ohms) + line_reactance_Ohms = to_floats(:Line_Reactance_Ohms) # Line angle limit (in radians) inputs_nw["Line_Angle_Limit"] = to_floats(:Angle_Limit_Rad) # DC-OPF coefficient for each line (in MW when not scaled, in GW when scaled) # MW = (kV)^2/Ohms - inputs_nw["pDC_OPF_coeff"] = ((line_voltage_kV.^2)./line_reactance_Ohms)/scale_factor + inputs_nw["pDC_OPF_coeff"] = ((line_voltage_kV .^ 2) ./ line_reactance_Ohms) / + scale_factor end # Maximum possible flow after reinforcement for use in linear segments of piecewise approximation inputs_nw["pTrans_Max_Possible"] = inputs_nw["pTrans_Max"] - if setup["NetworkExpansion"]==1 + if setup["NetworkExpansion"] == 1 # Read between zone network reinforcement costs per peak MW of capacity added - inputs_nw["pC_Line_Reinforcement"] = to_floats(:Line_Reinforcement_Cost_per_MWyr) / scale_factor # convert to million $/GW/yr with objective function in millions + inputs_nw["pC_Line_Reinforcement"] = to_floats(:Line_Reinforcement_Cost_per_MWyr) / + scale_factor # convert to million $/GW/yr with objective function in millions # Maximum reinforcement allowed in MW #NOTE: values <0 indicate no expansion possible - inputs_nw["pMax_Line_Reinforcement"] = map(x->max(0, x), to_floats(:Line_Max_Reinforcement_MW)) / scale_factor # convert to GW + inputs_nw["pMax_Line_Reinforcement"] = map(x -> max(0, x), + to_floats(:Line_Max_Reinforcement_MW)) / scale_factor # convert to GW inputs_nw["pTrans_Max_Possible"] += inputs_nw["pMax_Line_Reinforcement"] end # Multi-Stage if setup["MultiStage"] == 1 # Weighted Average Cost of Capital for Transmission Expansion - if setup["NetworkExpansion"]>=1 - inputs_nw["transmission_WACC"]= to_floats(:WACC) - inputs_nw["Capital_Recovery_Period_Trans"]= to_floats(:Capital_Recovery_Period) + if setup["NetworkExpansion"] >= 1 + inputs_nw["transmission_WACC"] = to_floats(:WACC) + inputs_nw["Capital_Recovery_Period_Trans"] = to_floats(:Capital_Recovery_Period) end # Max Flow Possible on Each Line - inputs_nw["pLine_Max_Flow_Possible_MW"] = to_floats(:Line_Max_Flow_Possible_MW) / scale_factor # Convert to GW + inputs_nw["pLine_Max_Flow_Possible_MW"] = to_floats(:Line_Max_Flow_Possible_MW) / + scale_factor # Convert to GW end # Transmission line (between zone) loss coefficient (resistance/voltage^2) @@ -84,17 +87,18 @@ function load_network_data!(setup::Dict, path::AbstractString, inputs_nw::Dict) inputs_nw["pTrans_Loss_Coef"] = inputs_nw["pPercent_Loss"] elseif setup["Trans_Loss_Segments"] >= 2 # If zones are connected, loss coefficient is R/V^2 where R is resistance in Ohms and V is voltage in Volts - inputs_nw["pTrans_Loss_Coef"] = (inputs_nw["Ohms"]/10^6)./(inputs_nw["kV"]/10^3)^2 * scale_factor # 1/GW *** + inputs_nw["pTrans_Loss_Coef"] = (inputs_nw["Ohms"] / 10^6) ./ + (inputs_nw["kV"] / 10^3)^2 * scale_factor # 1/GW *** end ## Sets and indices for transmission losses and expansion inputs_nw["TRANS_LOSS_SEGS"] = setup["Trans_Loss_Segments"] # Number of segments used in piecewise linear approximations quadratic loss functions - inputs_nw["LOSS_LINES"] = findall(inputs_nw["pTrans_Loss_Coef"].!=0) # Lines for which loss coefficients apply (are non-zero); + inputs_nw["LOSS_LINES"] = findall(inputs_nw["pTrans_Loss_Coef"] .!= 0) # Lines for which loss coefficients apply (are non-zero); if setup["NetworkExpansion"] == 1 # Network lines and zones that are expandable have non-negative maximum reinforcement inputs - inputs_nw["EXPANSION_LINES"] = findall(inputs_nw["pMax_Line_Reinforcement"].>=0) - inputs_nw["NO_EXPANSION_LINES"] = findall(inputs_nw["pMax_Line_Reinforcement"].<0) + inputs_nw["EXPANSION_LINES"] = findall(inputs_nw["pMax_Line_Reinforcement"] .>= 0) + inputs_nw["NO_EXPANSION_LINES"] = findall(inputs_nw["pMax_Line_Reinforcement"] .< 0) end println(filename * " Successfully Read!") @@ -138,9 +142,9 @@ starting zone of the line and the zone with entry -1 is the ending zone of the l """ function load_network_map_from_matrix(network_var::DataFrame, Z, L) # Topology of the network source-sink matrix - network_map_matrix_format_deprecation_warning() + network_map_matrix_format_deprecation_warning() col = findall(s -> s == "z1", names(network_var))[1] - mat = Matrix{Float64}(network_var[1:L, col:col+Z-1]) + mat = Matrix{Float64}(network_var[1:L, col:(col + Z - 1)]) end function load_network_map(network_var::DataFrame, Z, L) @@ -150,7 +154,7 @@ function load_network_map(network_var::DataFrame, Z, L) has_network_list = all([c in columns for c in list_columns]) zones_as_strings = ["z" * string(i) for i in 1:Z] - has_network_matrix = all([c in columns for c in zones_as_strings]) + has_network_matrix = all([c in columns for c in zones_as_strings]) instructions = """The transmission network should be specified in the form of a matrix (with columns z1, z2, ... zN) or in the form of lists (with Start_Zone, End_Zone), @@ -168,12 +172,12 @@ function load_network_map(network_var::DataFrame, Z, L) end function network_map_matrix_format_deprecation_warning() - @warn """Specifying the network map as a matrix is deprecated as of v0.4 -and will be removed in v0.5. Instead, use the more compact list-style format. - -..., Network_Lines, Start_Zone, End_Zone, ... - 1, 1, 2, - 2, 1, 3, - 3, 2, 3, -""" maxlog=1 + @warn """Specifying the network map as a matrix is deprecated as of v0.4 + and will be removed in v0.5. Instead, use the more compact list-style format. + + ..., Network_Lines, Start_Zone, End_Zone, ... + 1, 1, 2, + 2, 1, 3, + 3, 2, 3, + """ maxlog=1 end diff --git a/src/load_inputs/load_operational_reserves.jl b/src/load_inputs/load_operational_reserves.jl index 35508e9f5f..7aad7a74de 100644 --- a/src/load_inputs/load_operational_reserves.jl +++ b/src/load_inputs/load_operational_reserves.jl @@ -5,10 +5,10 @@ Read input parameters related to frequency regulation and operating reserve requ """ function load_operational_reserves!(setup::Dict, path::AbstractString, inputs::Dict) filename = "Operational_reserves.csv" - deprecated_synonym = "Reserves.csv" + deprecated_synonym = "Reserves.csv" res_in = load_dataframe(path, [filename, deprecated_synonym]) - gen = inputs["RESOURCES"] + gen = inputs["RESOURCES"] function load_field_with_deprecated_symbol(df::DataFrame, columns::Vector{Symbol}) best = popfirst!(columns) @@ -19,49 +19,52 @@ function load_operational_reserves!(setup::Dict, path::AbstractString, inputs::D end for col in columns if col in all_columns - Base.depwarn("The column name $col in file $filename is deprecated; prefer $best", :load_operational_reserves, force=true) + Base.depwarn("The column name $col in file $filename is deprecated; prefer $best", + :load_operational_reserves, + force = true) return float(df[firstrow, col]) end end error("None of the columns $columns were found in the file $filename") end - # Regulation requirement as a percent of hourly demand; here demand is the total across all model zones - inputs["pReg_Req_Demand"] = load_field_with_deprecated_symbol(res_in, - [:Reg_Req_Percent_Demand, - :Reg_Req_Percent_Load]) + # Regulation requirement as a percent of hourly demand; here demand is the total across all model zones + inputs["pReg_Req_Demand"] = load_field_with_deprecated_symbol(res_in, + [:Reg_Req_Percent_Demand, + :Reg_Req_Percent_Load]) - # Regulation requirement as a percent of hourly wind and solar generation (summed across all model zones) - inputs["pReg_Req_VRE"] = float(res_in[1,:Reg_Req_Percent_VRE]) - # Spinning up reserve requirement as a percent of hourly demand (which is summed across all zones) - inputs["pRsv_Req_Demand"] = load_field_with_deprecated_symbol(res_in, - [:Rsv_Req_Percent_Demand, - :Rsv_Req_Percent_Load]) - # Spinning up reserve requirement as a percent of hourly wind and solar generation (which is summed across all zones) - inputs["pRsv_Req_VRE"] = float(res_in[1,:Rsv_Req_Percent_VRE]) + # Regulation requirement as a percent of hourly wind and solar generation (summed across all model zones) + inputs["pReg_Req_VRE"] = float(res_in[1, :Reg_Req_Percent_VRE]) + # Spinning up reserve requirement as a percent of hourly demand (which is summed across all zones) + inputs["pRsv_Req_Demand"] = load_field_with_deprecated_symbol(res_in, + [:Rsv_Req_Percent_Demand, + :Rsv_Req_Percent_Load]) + # Spinning up reserve requirement as a percent of hourly wind and solar generation (which is summed across all zones) + inputs["pRsv_Req_VRE"] = float(res_in[1, :Rsv_Req_Percent_VRE]) scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 # Penalty for not meeting hourly spinning reserve requirement - inputs["pC_Rsv_Penalty"] = float(res_in[1,:Unmet_Rsv_Penalty_Dollar_per_MW]) / scale_factor # convert to million $/GW with objective function in millions - inputs["pStatic_Contingency"] = float(res_in[1,:Static_Contingency_MW]) / scale_factor # convert to GW + inputs["pC_Rsv_Penalty"] = float(res_in[1, :Unmet_Rsv_Penalty_Dollar_per_MW]) / + scale_factor # convert to million $/GW with objective function in millions + inputs["pStatic_Contingency"] = float(res_in[1, :Static_Contingency_MW]) / scale_factor # convert to GW - if setup["UCommit"] >= 1 - inputs["pDynamic_Contingency"] = convert(Int8, res_in[1,:Dynamic_Contingency] ) - # Set BigM value used for dynamic contingencies cases to be largest possible cluster size - # Note: this BigM value is only relevant for units in the COMMIT set. See operational_reserves.jl for details on implementation of dynamic contingencies - if inputs["pDynamic_Contingency"] > 0 - inputs["pContingency_BigM"] = zeros(Float64, inputs["G"]) - for y in inputs["COMMIT"] - inputs["pContingency_BigM"][y] = max_cap_mw(gen[y]) - # When Max_Cap_MW == -1, there is no limit on capacity size - if inputs["pContingency_BigM"][y] < 0 - # NOTE: this effectively acts as a maximum cluster size when not otherwise specified, adjust accordingly - inputs["pContingency_BigM"][y] = 5000 * cap_size(gen[y]) - end - end - end - end + if setup["UCommit"] >= 1 + inputs["pDynamic_Contingency"] = convert(Int8, res_in[1, :Dynamic_Contingency]) + # Set BigM value used for dynamic contingencies cases to be largest possible cluster size + # Note: this BigM value is only relevant for units in the COMMIT set. See operational_reserves.jl for details on implementation of dynamic contingencies + if inputs["pDynamic_Contingency"] > 0 + inputs["pContingency_BigM"] = zeros(Float64, inputs["G"]) + for y in inputs["COMMIT"] + inputs["pContingency_BigM"][y] = max_cap_mw(gen[y]) + # When Max_Cap_MW == -1, there is no limit on capacity size + if inputs["pContingency_BigM"][y] < 0 + # NOTE: this effectively acts as a maximum cluster size when not otherwise specified, adjust accordingly + inputs["pContingency_BigM"][y] = 5000 * cap_size(gen[y]) + end + end + end + end - println(filename * " Successfully Read!") + println(filename * " Successfully Read!") end diff --git a/src/load_inputs/load_period_map.jl b/src/load_inputs/load_period_map.jl index dee0b1ccd2..3966ea03b3 100644 --- a/src/load_inputs/load_period_map.jl +++ b/src/load_inputs/load_period_map.jl @@ -4,16 +4,16 @@ Read input parameters related to mapping of representative time periods to full chronological time series """ function load_period_map!(setup::Dict, path::AbstractString, inputs::Dict) - period_map = "Period_map.csv" - data_directory = joinpath(path, setup["TimeDomainReductionFolder"]) - if setup["TimeDomainReduction"] == 1 && isfile(joinpath(data_directory, period_map)) # Use Time Domain Reduced data for GenX - my_dir = data_directory - else + period_map = "Period_map.csv" + data_directory = joinpath(path, setup["TimeDomainReductionFolder"]) + if setup["TimeDomainReduction"] == 1 && isfile(joinpath(data_directory, period_map)) # Use Time Domain Reduced data for GenX + my_dir = data_directory + else # If TDR is not used, then use the "system" directory specified in the setup my_dir = joinpath(path, setup["SystemFolder"]) - end - file_path = joinpath(my_dir, period_map) + end + file_path = joinpath(my_dir, period_map) inputs["Period_Map"] = load_dataframe(file_path) - println(period_map * " Successfully Read!") + println(period_map * " Successfully Read!") end diff --git a/src/load_inputs/load_resources_data.jl b/src/load_inputs/load_resources_data.jl index c5a37486a1..979d9e7c21 100644 --- a/src/load_inputs/load_resources_data.jl +++ b/src/load_inputs/load_resources_data.jl @@ -8,16 +8,14 @@ Internal function to get resource information (filename and GenX type) for each """ function _get_resource_info() - resource_info = ( - hydro = (filename="Hydro.csv", type=Hydro), - thermal = (filename="Thermal.csv", type=Thermal), - vre = (filename="Vre.csv", type=Vre), - storage = (filename="Storage.csv", type=Storage), - flex_demand = (filename="Flex_demand.csv", type=FlexDemand), - must_run = (filename="Must_run.csv", type=MustRun), - electrolyzer = (filename="Electrolyzer.csv", type=Electrolyzer), - vre_stor = (filename="Vre_stor.csv", type=VreStorage) - ) + resource_info = (hydro = (filename = "Hydro.csv", type = Hydro), + thermal = (filename = "Thermal.csv", type = Thermal), + vre = (filename = "Vre.csv", type = Vre), + storage = (filename = "Storage.csv", type = Storage), + flex_demand = (filename = "Flex_demand.csv", type = FlexDemand), + must_run = (filename = "Must_run.csv", type = MustRun), + electrolyzer = (filename = "Electrolyzer.csv", type = Electrolyzer), + vre_stor = (filename = "Vre_stor.csv", type = VreStorage)) return resource_info end @@ -37,12 +35,11 @@ function _get_policyfile_info() min_cap_filenames = ["Resource_minimum_capacity_requirement.csv"] max_cap_filenames = ["Resource_maximum_capacity_requirement.csv"] - policyfile_info = ( - esr = (filenames=esr_filenames, setup_param="EnergyShareRequirement"), - cap_res = (filenames=cap_res_filenames, setup_param="CapacityReserveMargin"), - min_cap = (filenames=min_cap_filenames, setup_param="MinCapReq"), - max_cap = (filenames=max_cap_filenames, setup_param="MaxCapReq"), - ) + policyfile_info = (esr = (filenames = esr_filenames, + setup_param = "EnergyShareRequirement"), + cap_res = (filenames = cap_res_filenames, setup_param = "CapacityReserveMargin"), + min_cap = (filenames = min_cap_filenames, setup_param = "MinCapReq"), + max_cap = (filenames = max_cap_filenames, setup_param = "MaxCapReq")) return policyfile_info end @@ -52,18 +49,16 @@ end Internal function to get a map of GenX resource type their corresponding names in the summary table. """ function _get_summary_map() - names_map = Dict{Symbol,String}( - :Electrolyzer => "Electrolyzer", + names_map = Dict{Symbol, String}(:Electrolyzer => "Electrolyzer", :FlexDemand => "Flexible_demand", :Hydro => "Hydro", :Storage => "Storage", :Thermal => "Thermal", :Vre => "VRE", :MustRun => "Must_run", - :VreStorage => "VRE_and_storage", - ) + :VreStorage => "VRE_and_storage") max_length = maximum(length.(values(names_map))) - for (k,v) in names_map + for (k, v) in names_map names_map[k] = v * repeat(" ", max_length - length(v)) end return names_map @@ -82,43 +77,31 @@ See documentation for descriptions of each column being scaled. """ function scale_resources_data!(resource_in::DataFrame, scale_factor::Float64) columns_to_scale = [:existing_charge_cap_mw, # to GW - :existing_cap_mwh, # to GWh - :existing_cap_mw, # to GW - - :cap_size, # to GW - - :min_cap_mw, # to GW - :min_cap_mwh, # to GWh - :min_charge_cap_mw, # to GWh - - :max_cap_mw, # to GW - :max_cap_mwh, # to GWh - :max_charge_cap_mw, # to GW - - :inv_cost_per_mwyr, # to $M/GW/yr - :inv_cost_per_mwhyr, # to $M/GWh/yr - :inv_cost_charge_per_mwyr, # to $M/GW/yr - - :fixed_om_cost_per_mwyr, # to $M/GW/yr - :fixed_om_cost_per_mwhyr, # to $M/GWh/yr - :fixed_om_cost_charge_per_mwyr, # to $M/GW/yr - - :var_om_cost_per_mwh, # to $M/GWh - :var_om_cost_per_mwh_in, # to $M/GWh - - :reg_cost, # to $M/GW - :rsv_cost, # to $M/GW - - :min_retired_cap_mw, # to GW - :min_retired_charge_cap_mw, # to GW - :min_retired_energy_cap_mw, # to GW - - :start_cost_per_mw, # to $M/GW - - :ccs_disposal_cost_per_metric_ton, - - :hydrogen_mwh_per_tonne # to GWh/t - ] + :existing_cap_mwh, # to GWh + :existing_cap_mw, # to GW + :cap_size, # to GW + :min_cap_mw, # to GW + :min_cap_mwh, # to GWh + :min_charge_cap_mw, # to GWh + :max_cap_mw, # to GW + :max_cap_mwh, # to GWh + :max_charge_cap_mw, # to GW + :inv_cost_per_mwyr, # to $M/GW/yr + :inv_cost_per_mwhyr, # to $M/GWh/yr + :inv_cost_charge_per_mwyr, # to $M/GW/yr + :fixed_om_cost_per_mwyr, # to $M/GW/yr + :fixed_om_cost_per_mwhyr, # to $M/GWh/yr + :fixed_om_cost_charge_per_mwyr, # to $M/GW/yr + :var_om_cost_per_mwh, # to $M/GWh + :var_om_cost_per_mwh_in, # to $M/GWh + :reg_cost, # to $M/GW + :rsv_cost, # to $M/GW + :min_retired_cap_mw, # to GW + :min_retired_charge_cap_mw, # to GW + :min_retired_energy_cap_mw, # to GW + :start_cost_per_mw, # to $M/GW + :ccs_disposal_cost_per_metric_ton, :hydrogen_mwh_per_tonne, # to GWh/t + ] scale_columns!(resource_in, columns_to_scale, scale_factor) return nothing @@ -137,53 +120,53 @@ See documentation for descriptions of each column being scaled. """ function scale_vre_stor_data!(vre_stor_in::DataFrame, scale_factor::Float64) columns_to_scale = [:existing_cap_inverter_mw, - :existing_cap_solar_mw, - :existing_cap_wind_mw, - :existing_cap_charge_dc_mw, - :existing_cap_charge_ac_mw, - :existing_cap_discharge_dc_mw, - :existing_cap_discharge_ac_mw, - :min_cap_inverter_mw, - :max_cap_inverter_mw, - :min_cap_solar_mw, - :max_cap_solar_mw, - :min_cap_wind_mw, - :max_cap_wind_mw, - :min_cap_charge_ac_mw, - :max_cap_charge_ac_mw, - :min_cap_charge_dc_mw, - :max_cap_charge_dc_mw, - :min_cap_discharge_ac_mw, - :max_cap_discharge_ac_mw, - :min_cap_discharge_dc_mw, - :max_cap_discharge_dc_mw, - :inv_cost_inverter_per_mwyr, - :fixed_om_inverter_cost_per_mwyr, - :inv_cost_solar_per_mwyr, - :fixed_om_solar_cost_per_mwyr, - :inv_cost_wind_per_mwyr, - :fixed_om_wind_cost_per_mwyr, - :inv_cost_discharge_dc_per_mwyr, - :fixed_om_cost_discharge_dc_per_mwyr, - :inv_cost_charge_dc_per_mwyr, - :fixed_om_cost_charge_dc_per_mwyr, - :inv_cost_discharge_ac_per_mwyr, - :fixed_om_cost_discharge_ac_per_mwyr, - :inv_cost_charge_ac_per_mwyr, - :fixed_om_cost_charge_ac_per_mwyr, - :var_om_cost_per_mwh_solar, - :var_om_cost_per_mwh_wind, - :var_om_cost_per_mwh_charge_dc, - :var_om_cost_per_mwh_discharge_dc, - :var_om_cost_per_mwh_charge_ac, - :var_om_cost_per_mwh_discharge_ac, - :min_retired_cap_inverter_mw, - :min_retired_cap_solar_mw, - :min_retired_cap_wind_mw, - :min_retired_cap_charge_dc_mw, - :min_retired_cap_charge_ac_mw, - :min_retired_cap_discharge_dc_mw, - :min_retired_cap_discharge_ac_mw] + :existing_cap_solar_mw, + :existing_cap_wind_mw, + :existing_cap_charge_dc_mw, + :existing_cap_charge_ac_mw, + :existing_cap_discharge_dc_mw, + :existing_cap_discharge_ac_mw, + :min_cap_inverter_mw, + :max_cap_inverter_mw, + :min_cap_solar_mw, + :max_cap_solar_mw, + :min_cap_wind_mw, + :max_cap_wind_mw, + :min_cap_charge_ac_mw, + :max_cap_charge_ac_mw, + :min_cap_charge_dc_mw, + :max_cap_charge_dc_mw, + :min_cap_discharge_ac_mw, + :max_cap_discharge_ac_mw, + :min_cap_discharge_dc_mw, + :max_cap_discharge_dc_mw, + :inv_cost_inverter_per_mwyr, + :fixed_om_inverter_cost_per_mwyr, + :inv_cost_solar_per_mwyr, + :fixed_om_solar_cost_per_mwyr, + :inv_cost_wind_per_mwyr, + :fixed_om_wind_cost_per_mwyr, + :inv_cost_discharge_dc_per_mwyr, + :fixed_om_cost_discharge_dc_per_mwyr, + :inv_cost_charge_dc_per_mwyr, + :fixed_om_cost_charge_dc_per_mwyr, + :inv_cost_discharge_ac_per_mwyr, + :fixed_om_cost_discharge_ac_per_mwyr, + :inv_cost_charge_ac_per_mwyr, + :fixed_om_cost_charge_ac_per_mwyr, + :var_om_cost_per_mwh_solar, + :var_om_cost_per_mwh_wind, + :var_om_cost_per_mwh_charge_dc, + :var_om_cost_per_mwh_discharge_dc, + :var_om_cost_per_mwh_charge_ac, + :var_om_cost_per_mwh_discharge_ac, + :min_retired_cap_inverter_mw, + :min_retired_cap_solar_mw, + :min_retired_cap_wind_mw, + :min_retired_cap_charge_dc_mw, + :min_retired_cap_charge_ac_mw, + :min_retired_cap_discharge_dc_mw, + :min_retired_cap_discharge_ac_mw] scale_columns!(vre_stor_in, columns_to_scale, scale_factor) return nothing end @@ -199,7 +182,9 @@ Scales in-place the columns in `columns_to_scale` of a dataframe `df` by a `scal - `scale_factor` (Float64): A scaling factor for energy and currency units. """ -function scale_columns!(df::DataFrame, columns_to_scale::Vector{Symbol}, scale_factor::Float64) +function scale_columns!(df::DataFrame, + columns_to_scale::Vector{Symbol}, + scale_factor::Float64) for column in columns_to_scale if string(column) in names(df) df[!, column] /= scale_factor @@ -246,7 +231,7 @@ Computes the indices for the resources loaded from a single dataframe by shiftin """ function compute_resource_indices(resources_in::DataFrame, offset::Int64) - range = (1,nrow(resources_in)) .+ offset + range = (1, nrow(resources_in)) .+ offset return UnitRange{Int64}(range...) end @@ -314,7 +299,9 @@ Construct the array of resources from multiple files of different types located - `Error`: If no resources data is found. Check the data path or the configuration file "genx_settings.yml" inside Settings. """ -function create_resource_array(resource_folder::AbstractString, resources_info::NamedTuple, scale_factor::Float64=1.0) +function create_resource_array(resource_folder::AbstractString, + resources_info::NamedTuple, + scale_factor::Float64 = 1.0) resource_id_offset = 0 resources = [] # loop over available types and load all resources in resource_folder @@ -333,7 +320,8 @@ function create_resource_array(resource_folder::AbstractString, resources_info:: @info filename * " Successfully Read." end end - isempty(resources) && error("No resources data found. Check data path or configuration file \"genx_settings.yml\" inside Settings.") + isempty(resources) && + error("No resources data found. Check data path or configuration file \"genx_settings.yml\" inside Settings.") return reduce(vcat, resources) end @@ -353,15 +341,17 @@ function check_mustrun_reserve_contribution(r::AbstractResource) reg_max_r = reg_max(r) if reg_max_r != 0 - e = string("Resource ", resource_name(r), " is of MUST_RUN type but :Reg_Max = ", reg_max_r, ".\n", - "MUST_RUN units must have Reg_Max = 0 since they cannot contribute to reserves.") + e = string("Resource ", resource_name(r), " is of MUST_RUN type but :Reg_Max = ", + reg_max_r, ".\n", + "MUST_RUN units must have Reg_Max = 0 since they cannot contribute to reserves.") push!(error_strings, e) end - + rsv_max_r = rsv_max(r) if rsv_max_r != 0 - e = string("Resource ", resource_name(r), " is of MUST_RUN type but :Rsv_Max = ", rsv_max_r, ".\n", - "MUST_RUN units must have Rsv_Max = 0 since they cannot contribute to reserves.") + e = string("Resource ", resource_name(r), " is of MUST_RUN type but :Rsv_Max = ", + rsv_max_r, ".\n", + "MUST_RUN units must have Rsv_Max = 0 since they cannot contribute to reserves.") push!(error_strings, e) end return ErrorMsg.(error_strings) @@ -377,7 +367,7 @@ function check_LDS_applicability(r::AbstractResource) # LDS is available only for Hydro and Storage if !isa(r, applicable_resources) && lds_value > 0 e = string("Resource ", resource_name(r), " has :lds = ", lds_value, ".\n", - "This setting is valid only for resources where the type is one of $applicable_resources.") + "This setting is valid only for resources where the type is one of $applicable_resources.") push!(error_strings, e) end return ErrorMsg.(error_strings) @@ -388,9 +378,9 @@ function check_maintenance_applicability(r::AbstractResource) not_set = default_zero maint_value = get(r, :maint, not_set) - + error_strings = String[] - + if maint_value == not_set # not MAINT so the rest is not applicable return error_strings @@ -399,13 +389,13 @@ function check_maintenance_applicability(r::AbstractResource) # MAINT is available only for Thermal if !isa(r, applicable_resources) && maint_value > 0 e = string("Resource ", resource_name(r), " has :maint = ", maint_value, ".\n", - "This setting is valid only for resources where the type is one of $applicable_resources.") + "This setting is valid only for resources where the type is one of $applicable_resources.") push!(error_strings, e) end if get(r, :model, not_set) == 2 e = string("Resource ", resource_name(r), " has :maint = ", maint_value, ".\n", - "This is valid only for resources with unit commitment (:model = 1);\n", - "this has :model = 2.") + "This is valid only for resources with unit commitment (:model = 1);\n", + "this has :model = 2.") push!(error_strings, e) end return ErrorMsg.(error_strings) @@ -416,27 +406,29 @@ function check_retrofit_resource(r::AbstractResource) # check that retrofit_id is set only for retrofitting units and not for new builds or units that can retire if can_retrofit(r) == true && can_retire(r) == false - e = string("Resource ", resource_name(r), " has :can_retrofit = ", can_retrofit(r), " but :can_retire = ", can_retire(r), ".\n", - "A unit that can be retrofitted must also be eligible for retirement (:can_retire = 1)") + e = string("Resource ", resource_name(r), " has :can_retrofit = ", can_retrofit(r), + " but :can_retire = ", can_retire(r), ".\n", + "A unit that can be retrofitted must also be eligible for retirement (:can_retire = 1)") push!(error_strings, e) elseif is_retrofit_option(r) == true && new_build(r) == false - e = string("Resource ", resource_name(r), " has :retrofit = ", is_retrofit_option(r), " but :new_build = ", new_build(r), ".\n", - "This setting is valid only for resources that have :new_build = 1") + e = string("Resource ", resource_name(r), " has :retrofit = ", + is_retrofit_option(r), " but :new_build = ", new_build(r), ".\n", + "This setting is valid only for resources that have :new_build = 1") push!(error_strings, e) end return ErrorMsg.(error_strings) -end +end function check_resource(r::AbstractResource) e = [] e = [e; check_LDS_applicability(r)] - e = [e; check_maintenance_applicability(r)] + e = [e; check_maintenance_applicability(r)] e = [e; check_mustrun_reserve_contribution(r)] e = [e; check_retrofit_resource(r)] return e end -function check_retrofit_id(rs::Vector{T}) where T <: AbstractResource +function check_retrofit_id(rs::Vector{T}) where {T <: AbstractResource} warning_strings = String[] units_can_retrofit = ids_can_retrofit(rs) @@ -445,7 +437,7 @@ function check_retrofit_id(rs::Vector{T}) where T <: AbstractResource # check that all retrofit_ids for resources that can retrofit and retrofit options match if Set(rs[units_can_retrofit].retrofit_id) != Set(rs[retrofit_options].retrofit_id) msg = string("Retrofit IDs for resources that \"can retrofit\" and \"retrofit options\" do not match.\n" * - "All retrofitting units must be associated with a retrofit option.") + "All retrofitting units must be associated with a retrofit option.") push!(warning_strings, msg) end @@ -458,7 +450,7 @@ end Validate the consistency of a vector of GenX resources Reports any errors/warnings as a vector of messages. """ -function check_resource(resources::Vector{T}) where T <: AbstractResource +function check_resource(resources::Vector{T}) where {T <: AbstractResource} e = [] for r in resources e = [e; check_resource(r)] @@ -488,7 +480,7 @@ function announce_errors_and_halt(e::Vector) return nothing end -function validate_resources(resources::Vector{T}) where T <: AbstractResource +function validate_resources(resources::Vector{T}) where {T <: AbstractResource} e = check_resource(resources) if length(e) > 0 announce_errors_and_halt(e) @@ -510,7 +502,7 @@ Function that loads and scales resources data from folder specified in resources """ function create_resource_array(setup::Dict, resources_path::AbstractString) scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1.0 - + # get filename and GenX type for each type of resources available in GenX resources_info = _get_resource_info() @@ -521,7 +513,6 @@ function create_resource_array(setup::Dict, resources_path::AbstractString) return resources end - """ validate_policy_files(resource_policies_path::AbstractString, setup::Dict) @@ -538,8 +529,13 @@ Validate the policy files by checking if they exist in the specified folder and function validate_policy_files(resource_policies_path::AbstractString, setup::Dict) policyfile_info = _get_policyfile_info() for (filenames, setup_param) in values(policyfile_info) - if setup[setup_param] == 1 && any(!isfile(joinpath(resource_policies_path, filename)) for filename in filenames) - msg = string(setup_param, " is set to 1 in settings but the required file(s) ", filenames, " was (were) not found in ", resource_policies_path) + if setup[setup_param] == 1 && + any(!isfile(joinpath(resource_policies_path, filename)) for filename in filenames) + msg = string(setup_param, + " is set to 1 in settings but the required file(s) ", + filenames, + " was (were) not found in ", + resource_policies_path) @warn(msg) end end @@ -564,15 +560,16 @@ function validate_policy_dataframe!(filename::AbstractString, policy_in::DataFra error(msg) end # if the single column attribute does not have a tag number, add a tag number of 1 - if n_cols == 2 && cols[2][end-1:end] != "_1" + if n_cols == 2 && cols[2][(end - 1):end] != "_1" rename!(policy_in, Symbol.(cols[2]) => Symbol.(cols[2], "_1")) end # get policy column names cols = lowercase.(names(policy_in)) - filter!(col -> col ≠ "resource",cols) - + filter!(col -> col ≠ "resource", cols) + accepted_cols = ["derating_factor", "esr", "esr_vrestor", - [string(cap, type) for cap in ["min_cap", "max_cap"] for type in ("", "_stor", "_solar", "_wind")]...] + [string(cap, type) for cap in ["min_cap", "max_cap"] + for type in ("", "_stor", "_solar", "_wind")]...] # Check that all policy columns have names in accepted_cols if !all(x -> replace(x, r"(_*|_*\d*)$" => "") in accepted_cols, cols) @@ -581,7 +578,8 @@ function validate_policy_dataframe!(filename::AbstractString, policy_in::DataFra error(msg) end # Check that all policy columns have names with format "[policy_name]_[tagnum]" - if !all(any([occursin(Regex("$(y)")*r"_\d", col) for y in accepted_cols]) for col in cols) + if !all(any([occursin(Regex("$(y)") * r"_\d", col) for y in accepted_cols]) + for col in cols) msg = "Columns in policy file $filename must have names with format \"[policy_name]_[tagnum]\", case insensitive. (e.g., ESR_1, Min_Cap_1, Max_Cap_2, etc.)." error(msg) end @@ -599,14 +597,16 @@ Adds a set of new attributes (names and corresponding values) to a resource. The - `new_values::DataFrameRow`: DataFrameRow containing the values of the new attributes. """ -function add_attributes_to_resource!(resource::AbstractResource, new_symbols::Vector{Symbol}, new_values::T) where T <: DataFrameRow +function add_attributes_to_resource!(resource::AbstractResource, + new_symbols::Vector{Symbol}, + new_values::T) where {T <: DataFrameRow} # loop over new attributes for (sym, value) in zip(new_symbols, new_values) # add attribute to resource setproperty!(resource, sym, value) end return nothing -end +end """ add_df_to_resources!(resources::Vector{<:AbstractResource}, module_in::DataFrame) @@ -642,7 +642,9 @@ Loads a single policy file and adds the columns as new attributes to resources i - `path::AbstractString`: The path to the policy file. - `filename::AbstractString`: The name of the policy file. """ -function add_policy_to_resources!(resources::Vector{<:AbstractResource}, path::AbstractString, filename::AbstractString) +function add_policy_to_resources!(resources::Vector{<:AbstractResource}, + path::AbstractString, + filename::AbstractString) policy_in = load_dataframe(path) # check if policy file has any attributes, validate column names validate_policy_dataframe!(filename, policy_in) @@ -660,15 +662,16 @@ Reads policy files and adds policies-related attributes to resources in the mode - `resources::Vector{<:AbstractResource}`: Vector of resources in the model. - `resources_path::AbstractString`: The path to the resources folder. """ -function add_policies_to_resources!(resources::Vector{<:AbstractResource}, resource_policy_path::AbstractString) +function add_policies_to_resources!(resources::Vector{<:AbstractResource}, + resource_policy_path::AbstractString) # get filename for each type of policy available in GenX policies_info = _get_policyfile_info() # loop over policy files - for (filenames,_) in values(policies_info) + for (filenames, _) in values(policies_info) for filename in filenames path = joinpath(resource_policy_path, filename) # if file exists, add policy to resources - if isfile(path) + if isfile(path) add_policy_to_resources!(resources, path, filename) @info filename * " Successfully Read." end @@ -686,7 +689,8 @@ Reads module dataframe and adds columns as new attributes to the resources in th - `resources::Vector{<:AbstractResource}`: A vector of resources. - `module_in::DataFrame`: The dataframe with the columns to add to the resources. """ -function add_module_to_resources!(resources::Vector{<:AbstractResource}, module_in::DataFrame) +function add_module_to_resources!(resources::Vector{<:AbstractResource}, + module_in::DataFrame) # add module columns to resources as new attributes add_df_to_resources!(resources, module_in) return nothing @@ -702,7 +706,9 @@ Reads module dataframes, loops over files and adds columns as new attributes to - `setup (Dict)`: A dictionary containing GenX settings. - `resources_path::AbstractString`: The path to the resources folder. """ -function add_modules_to_resources!(resources::Vector{<:AbstractResource}, setup::Dict, resources_path::AbstractString) +function add_modules_to_resources!(resources::Vector{<:AbstractResource}, + setup::Dict, + resources_path::AbstractString) scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1.0 modules = Vector{DataFrame}() @@ -715,7 +721,7 @@ function add_modules_to_resources!(resources::Vector{<:AbstractResource}, setup: push!(modules, multistage_in) @info "Multistage data successfully read." end - + ## Loop over modules and add attributes to resources add_module_to_resources!.(Ref(resources), modules) @@ -723,32 +729,32 @@ function add_modules_to_resources!(resources::Vector{<:AbstractResource}, setup: end function validate_piecewisefuelusage(heat_rate_mat, load_point_mat) - # it's possible to construct piecewise fuel consumption with n of heat rate and n-1 of load point. - # if a user feed n of heat rate and more than n of load point, throw a error message, and then use - # n of heat rate and n-1 load point to construct the piecewise fuel usage fuction - if size(heat_rate_mat)[2] < size(load_point_mat)[2] - @error """ The numbers of heatrate data are less than load points, we found $(size(heat_rate_mat)[2]) of heat rate, - and $(size(load_point_mat)[2]) of load points. We will just use $(size(heat_rate_mat)[2]) of heat rate, and $(size(heat_rate_mat)[2]-1) - load point to create piecewise fuel usage - """ - end - - # check if values for piecewise fuel consumption make sense. Negative heat rate or load point are not allowed - if any(heat_rate_mat .< 0) | any(load_point_mat .< 0) - @error """ Neither heat rate nor load point can be negative - """ - error("Invalid inputs detected for piecewise fuel usage") - end - # for non-zero values, heat rates and load points should follow an increasing trend - if any([any(diff(filter(!=(0), row)) .< 0) for row in eachrow(heat_rate_mat)]) - @error """ Heat rates should follow an increasing trend - """ - error("Invalid inputs detected for piecewise fuel usage") - elseif any([any(diff(filter(!=(0), row)) .< 0) for row in eachrow(load_point_mat)]) - @error """load points should follow an increasing trend - """ - error("Invalid inputs detected for piecewise fuel usage") - end + # it's possible to construct piecewise fuel consumption with n of heat rate and n-1 of load point. + # if a user feed n of heat rate and more than n of load point, throw a error message, and then use + # n of heat rate and n-1 load point to construct the piecewise fuel usage fuction + if size(heat_rate_mat)[2] < size(load_point_mat)[2] + @error """ The numbers of heatrate data are less than load points, we found $(size(heat_rate_mat)[2]) of heat rate, + and $(size(load_point_mat)[2]) of load points. We will just use $(size(heat_rate_mat)[2]) of heat rate, and $(size(heat_rate_mat)[2]-1) + load point to create piecewise fuel usage + """ + end + + # check if values for piecewise fuel consumption make sense. Negative heat rate or load point are not allowed + if any(heat_rate_mat .< 0) | any(load_point_mat .< 0) + @error """ Neither heat rate nor load point can be negative + """ + error("Invalid inputs detected for piecewise fuel usage") + end + # for non-zero values, heat rates and load points should follow an increasing trend + if any([any(diff(filter(!=(0), row)) .< 0) for row in eachrow(heat_rate_mat)]) + @error """ Heat rates should follow an increasing trend + """ + error("Invalid inputs detected for piecewise fuel usage") + elseif any([any(diff(filter(!=(0), row)) .< 0) for row in eachrow(load_point_mat)]) + @error """load points should follow an increasing trend + """ + error("Invalid inputs detected for piecewise fuel usage") + end end """ @@ -762,20 +768,23 @@ Reads piecewise fuel usage data from the vector of generators, create a PWFU_dat - `gen::Vector{<:AbstractResource}`: The vector of generators in the model - `inputs::Dict`: The dictionary containing the input data """ -function process_piecewisefuelusage!(setup::Dict, gen::Vector{<:AbstractResource}, inputs::Dict) +function process_piecewisefuelusage!(setup::Dict, + gen::Vector{<:AbstractResource}, + inputs::Dict) inputs["PWFU_Num_Segments"] = 0 inputs["THERM_COMMIT_PWFU"] = Int64[] - - if any(haskey.(gen, :pwfu_fuel_usage_zero_load_mmbtu_per_h)) + if any(haskey.(gen, :pwfu_fuel_usage_zero_load_mmbtu_per_h)) thermal_gen = gen.Thermal has_pwfu = haskey.(thermal_gen, :pwfu_fuel_usage_zero_load_mmbtu_per_h) @assert all(has_pwfu) "Piecewise fuel usage data is not consistent across thermal generators" - heat_rate_mat_therm = extract_matrix_from_resources(thermal_gen, "pwfu_heat_rate_mmbtu_per_mwh") - load_point_mat_therm = extract_matrix_from_resources(thermal_gen, "pwfu_load_point_mw") - - num_segments = size(heat_rate_mat_therm)[2] + heat_rate_mat_therm = extract_matrix_from_resources(thermal_gen, + "pwfu_heat_rate_mmbtu_per_mwh") + load_point_mat_therm = extract_matrix_from_resources(thermal_gen, + "pwfu_load_point_mw") + + num_segments = size(heat_rate_mat_therm)[2] # create a matrix to store the heat rate and load point for each generator in the model heat_rate_mat = zeros(length(gen), num_segments) @@ -784,74 +793,78 @@ function process_piecewisefuelusage!(setup::Dict, gen::Vector{<:AbstractResource heat_rate_mat[THERM, :] = heat_rate_mat_therm load_point_mat[THERM, :] = load_point_mat_therm - # check data input - validate_piecewisefuelusage(heat_rate_mat, load_point_mat) + # check data input + validate_piecewisefuelusage(heat_rate_mat, load_point_mat) # determine if a generator contains piecewise fuel usage segment based on non-zero heatrate - nonzero_rows = any(heat_rate_mat .!= 0 , dims = 2)[:] - HAS_PWFU = resource_id.(gen[nonzero_rows]) + nonzero_rows = any(heat_rate_mat .!= 0, dims = 2)[:] + HAS_PWFU = resource_id.(gen[nonzero_rows]) - # translate the inital fuel usage, heat rate, and load points into intercept for each segment + # translate the inital fuel usage, heat rate, and load points into intercept for each segment fuel_usage_zero_load = zeros(length(gen)) - fuel_usage_zero_load[THERM] = pwfu_fuel_usage_zero_load_mmbtu_per_h.(thermal_gen) - # construct a matrix for intercept - intercept_mat = zeros(size(heat_rate_mat)) - # PWFU_Fuel_Usage_MMBTU_per_h is always the intercept of the first segment - intercept_mat[:,1] = fuel_usage_zero_load - - # create a function to compute intercept if we have more than one segment - function calculate_intercepts(slope, intercept_1, load_point) - m, n = size(slope) - # Initialize the intercepts matrix with zeros - intercepts = zeros(m, n) - # The first segment's intercepts should be intercept_1 vector - intercepts[:, 1] = intercept_1 - # Calculate intercepts for the other segments using the load points (i.e., intersection points) - for j in 1:n-1 - for i in 1:m - current_slope = slope[i, j+1] - previous_slope = slope[i, j] - # If the current slope is 0, then skip the calculation and return 0 - if current_slope == 0 - intercepts[i, j+1] = 0.0 - else - # y = a*x + b; => b = y - ax - # Calculate y-coordinate of the intersection - y = previous_slope * load_point[i, j] + intercepts[i, j] - # determine the new intercept - b = y - current_slope * load_point[i, j] - intercepts[i, j+1] = b - end - end - end - return intercepts - end - - if num_segments > 1 - # determine the intercept for the rest of segment if num_segments > 1 - intercept_mat = calculate_intercepts(heat_rate_mat, fuel_usage_zero_load, load_point_mat) - end - - # create a PWFU_data that contain processed intercept and slope (i.e., heat rate) - intercept_cols = [Symbol("pwfu_intercept_", i) for i in 1:num_segments] - intercept_df = DataFrame(intercept_mat, Symbol.(intercept_cols)) - slope_cols = Symbol.(filter(colname -> startswith(string(colname),"pwfu_heat_rate_mmbtu_per_mwh"), collect(attributes(thermal_gen[1])))) + fuel_usage_zero_load[THERM] = pwfu_fuel_usage_zero_load_mmbtu_per_h.(thermal_gen) + # construct a matrix for intercept + intercept_mat = zeros(size(heat_rate_mat)) + # PWFU_Fuel_Usage_MMBTU_per_h is always the intercept of the first segment + intercept_mat[:, 1] = fuel_usage_zero_load + + # create a function to compute intercept if we have more than one segment + function calculate_intercepts(slope, intercept_1, load_point) + m, n = size(slope) + # Initialize the intercepts matrix with zeros + intercepts = zeros(m, n) + # The first segment's intercepts should be intercept_1 vector + intercepts[:, 1] = intercept_1 + # Calculate intercepts for the other segments using the load points (i.e., intersection points) + for j in 1:(n - 1) + for i in 1:m + current_slope = slope[i, j + 1] + previous_slope = slope[i, j] + # If the current slope is 0, then skip the calculation and return 0 + if current_slope == 0 + intercepts[i, j + 1] = 0.0 + else + # y = a*x + b; => b = y - ax + # Calculate y-coordinate of the intersection + y = previous_slope * load_point[i, j] + intercepts[i, j] + # determine the new intercept + b = y - current_slope * load_point[i, j] + intercepts[i, j + 1] = b + end + end + end + return intercepts + end + + if num_segments > 1 + # determine the intercept for the rest of segment if num_segments > 1 + intercept_mat = calculate_intercepts(heat_rate_mat, + fuel_usage_zero_load, + load_point_mat) + end + + # create a PWFU_data that contain processed intercept and slope (i.e., heat rate) + intercept_cols = [Symbol("pwfu_intercept_", i) for i in 1:num_segments] + intercept_df = DataFrame(intercept_mat, Symbol.(intercept_cols)) + slope_cols = Symbol.(filter(colname -> startswith(string(colname), + "pwfu_heat_rate_mmbtu_per_mwh"), + collect(attributes(thermal_gen[1])))) sort!(slope_cols, by = x -> parse(Int, split(string(x), "_")[end])) - slope_df = DataFrame(heat_rate_mat, Symbol.(slope_cols)) - PWFU_data = hcat(slope_df, intercept_df) - # no need to scale sclope, but intercept should be scaled when parameterscale is on (MMBTU -> billion BTU) - scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 - PWFU_data[!, intercept_cols] ./= scale_factor - - inputs["slope_cols"] = slope_cols - inputs["intercept_cols"] = intercept_cols - inputs["PWFU_data"] = PWFU_data - inputs["PWFU_Num_Segments"] = num_segments - inputs["THERM_COMMIT_PWFU"] = intersect(ids_with_unit_commitment(gen), HAS_PWFU) - - @info "Piecewise fuel usage data successfully read!" - end - return nothing + slope_df = DataFrame(heat_rate_mat, Symbol.(slope_cols)) + PWFU_data = hcat(slope_df, intercept_df) + # no need to scale sclope, but intercept should be scaled when parameterscale is on (MMBTU -> billion BTU) + scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 + PWFU_data[!, intercept_cols] ./= scale_factor + + inputs["slope_cols"] = slope_cols + inputs["intercept_cols"] = intercept_cols + inputs["PWFU_data"] = PWFU_data + inputs["PWFU_Num_Segments"] = num_segments + inputs["THERM_COMMIT_PWFU"] = intersect(ids_with_unit_commitment(gen), HAS_PWFU) + + @info "Piecewise fuel usage data successfully read!" + end + return nothing end @doc raw""" @@ -863,59 +876,61 @@ For co-located VRE-storage resources, this function returns the storage type """ function split_storage_resources!(inputs::Dict, gen::Vector{<:AbstractResource}) - # All Storage Resources - inputs["VS_STOR"] = union(storage_dc_charge(gen), storage_dc_discharge(gen), - storage_ac_charge(gen), storage_ac_discharge(gen)) - - STOR = inputs["VS_STOR"] + # All Storage Resources + inputs["VS_STOR"] = union(storage_dc_charge(gen), storage_dc_discharge(gen), + storage_ac_charge(gen), storage_ac_discharge(gen)) + + STOR = inputs["VS_STOR"] - # Storage DC Discharge Resources - inputs["VS_STOR_DC_DISCHARGE"] = storage_dc_discharge(gen) - inputs["VS_SYM_DC_DISCHARGE"] = storage_sym_dc_discharge(gen) - inputs["VS_ASYM_DC_DISCHARGE"] = storage_asym_dc_discharge(gen) + # Storage DC Discharge Resources + inputs["VS_STOR_DC_DISCHARGE"] = storage_dc_discharge(gen) + inputs["VS_SYM_DC_DISCHARGE"] = storage_sym_dc_discharge(gen) + inputs["VS_ASYM_DC_DISCHARGE"] = storage_asym_dc_discharge(gen) - # Storage DC Charge Resources - inputs["VS_STOR_DC_CHARGE"] = storage_dc_charge(gen) - inputs["VS_SYM_DC_CHARGE"] = storage_sym_dc_charge(gen) + # Storage DC Charge Resources + inputs["VS_STOR_DC_CHARGE"] = storage_dc_charge(gen) + inputs["VS_SYM_DC_CHARGE"] = storage_sym_dc_charge(gen) inputs["VS_ASYM_DC_CHARGE"] = storage_asym_dc_charge(gen) - # Storage AC Discharge Resources - inputs["VS_STOR_AC_DISCHARGE"] = storage_ac_discharge(gen) - inputs["VS_SYM_AC_DISCHARGE"] = storage_sym_ac_discharge(gen) - inputs["VS_ASYM_AC_DISCHARGE"] = storage_asym_ac_discharge(gen) + # Storage AC Discharge Resources + inputs["VS_STOR_AC_DISCHARGE"] = storage_ac_discharge(gen) + inputs["VS_SYM_AC_DISCHARGE"] = storage_sym_ac_discharge(gen) + inputs["VS_ASYM_AC_DISCHARGE"] = storage_asym_ac_discharge(gen) - # Storage AC Charge Resources - inputs["VS_STOR_AC_CHARGE"] = storage_ac_charge(gen) - inputs["VS_SYM_AC_CHARGE"] = storage_sym_ac_charge(gen) - inputs["VS_ASYM_AC_CHARGE"] = storage_asym_ac_charge(gen) + # Storage AC Charge Resources + inputs["VS_STOR_AC_CHARGE"] = storage_ac_charge(gen) + inputs["VS_SYM_AC_CHARGE"] = storage_sym_ac_charge(gen) + inputs["VS_ASYM_AC_CHARGE"] = storage_asym_ac_charge(gen) - # Storage LDS & Non-LDS Resources - inputs["VS_LDS"] = is_LDS_VRE_STOR(gen) - inputs["VS_nonLDS"] = setdiff(STOR, inputs["VS_LDS"]) + # Storage LDS & Non-LDS Resources + inputs["VS_LDS"] = is_LDS_VRE_STOR(gen) + inputs["VS_nonLDS"] = setdiff(STOR, inputs["VS_LDS"]) # Symmetric and asymmetric storage resources - inputs["VS_ASYM"] = union(inputs["VS_ASYM_DC_CHARGE"], inputs["VS_ASYM_DC_DISCHARGE"], - inputs["VS_ASYM_AC_DISCHARGE"], inputs["VS_ASYM_AC_CHARGE"]) - inputs["VS_SYM_DC"] = intersect(inputs["VS_SYM_DC_CHARGE"], inputs["VS_SYM_DC_DISCHARGE"]) - inputs["VS_SYM_AC"] = intersect(inputs["VS_SYM_AC_CHARGE"], inputs["VS_SYM_AC_DISCHARGE"]) + inputs["VS_ASYM"] = union(inputs["VS_ASYM_DC_CHARGE"], inputs["VS_ASYM_DC_DISCHARGE"], + inputs["VS_ASYM_AC_DISCHARGE"], inputs["VS_ASYM_AC_CHARGE"]) + inputs["VS_SYM_DC"] = intersect(inputs["VS_SYM_DC_CHARGE"], + inputs["VS_SYM_DC_DISCHARGE"]) + inputs["VS_SYM_AC"] = intersect(inputs["VS_SYM_AC_CHARGE"], + inputs["VS_SYM_AC_DISCHARGE"]) # Send warnings for symmetric/asymmetric resources - if (!isempty(setdiff(inputs["VS_SYM_DC_DISCHARGE"], inputs["VS_SYM_DC_CHARGE"])) - || !isempty(setdiff(inputs["VS_SYM_DC_CHARGE"], inputs["VS_SYM_DC_DISCHARGE"])) - || !isempty(setdiff(inputs["VS_SYM_AC_DISCHARGE"], inputs["VS_SYM_AC_CHARGE"])) - || !isempty(setdiff(inputs["VS_SYM_AC_CHARGE"], inputs["VS_SYM_AC_DISCHARGE"]))) + if (!isempty(setdiff(inputs["VS_SYM_DC_DISCHARGE"], inputs["VS_SYM_DC_CHARGE"])) + || !isempty(setdiff(inputs["VS_SYM_DC_CHARGE"], inputs["VS_SYM_DC_DISCHARGE"])) + || !isempty(setdiff(inputs["VS_SYM_AC_DISCHARGE"], inputs["VS_SYM_AC_CHARGE"])) + || !isempty(setdiff(inputs["VS_SYM_AC_CHARGE"], inputs["VS_SYM_AC_DISCHARGE"]))) @warn("Symmetric capacities must both be DC or AC.") end - # Send warnings for battery resources discharging - if !isempty(intersect(inputs["VS_STOR_DC_DISCHARGE"], inputs["VS_STOR_AC_DISCHARGE"])) - @warn("Both AC and DC discharging functionalities are turned on.") - end + # Send warnings for battery resources discharging + if !isempty(intersect(inputs["VS_STOR_DC_DISCHARGE"], inputs["VS_STOR_AC_DISCHARGE"])) + @warn("Both AC and DC discharging functionalities are turned on.") + end - # Send warnings for battery resources charging - if !isempty(intersect(inputs["VS_STOR_DC_CHARGE"], inputs["VS_STOR_AC_CHARGE"])) - @warn("Both AC and DC charging functionalities are turned on.") - end + # Send warnings for battery resources charging + if !isempty(intersect(inputs["VS_STOR_DC_CHARGE"], inputs["VS_STOR_AC_CHARGE"])) + @warn("Both AC and DC charging functionalities are turned on.") + end end """ @@ -926,7 +941,7 @@ Updates the retrofit_id of a resource that can be retrofit or is a retrofit opti # Arguments - `r::AbstractResource`: The resource to update. """ -function update_retrofit_id(r::AbstractResource) +function update_retrofit_id(r::AbstractResource) if haskey(r, :retrofit_id) && (can_retrofit(r) == true || is_retrofit_option(r) == true) r.retrofit_id = string(r.retrofit_id, "_", region(r)) else @@ -946,21 +961,25 @@ Adds resources to the `inputs` `Dict` with the key "RESOURCES" together with sev - `gen (Vector{<:AbstractResource})`: Array of GenX resources. """ -function add_resources_to_input_data!(inputs::Dict, setup::Dict, case_path::AbstractString, gen::Vector{<:AbstractResource}) - +function add_resources_to_input_data!(inputs::Dict, + setup::Dict, + case_path::AbstractString, + gen::Vector{<:AbstractResource}) + # Number of resources G = length(gen) inputs["G"] = G # Number of time steps (periods) T = inputs["T"] - + ## HYDRO # Set of all reservoir hydro resources inputs["HYDRO_RES"] = hydro(gen) # Set of hydro resources modeled with known reservoir energy capacity if !isempty(inputs["HYDRO_RES"]) - inputs["HYDRO_RES_KNOWN_CAP"] = intersect(inputs["HYDRO_RES"], ids_with_positive(gen, hydro_energy_to_power_ratio)) + inputs["HYDRO_RES_KNOWN_CAP"] = intersect(inputs["HYDRO_RES"], + ids_with_positive(gen, hydro_energy_to_power_ratio)) end ## STORAGE @@ -969,12 +988,12 @@ function add_resources_to_input_data!(inputs::Dict, setup::Dict, case_path::Abst # Set of storage resources with asymmetric (separte) charge/discharge capacity components inputs["STOR_ASYMMETRIC"] = asymmetric_storage(gen) # Set of all storage resources - inputs["STOR_ALL"] = union(inputs["STOR_SYMMETRIC"],inputs["STOR_ASYMMETRIC"]) + inputs["STOR_ALL"] = union(inputs["STOR_SYMMETRIC"], inputs["STOR_ASYMMETRIC"]) # Set of storage resources with long duration storage capabilitites inputs["STOR_HYDRO_LONG_DURATION"] = intersect(inputs["HYDRO_RES"], is_LDS(gen)) inputs["STOR_HYDRO_SHORT_DURATION"] = intersect(inputs["HYDRO_RES"], is_SDS(gen)) - inputs["STOR_LONG_DURATION"] = intersect(inputs["STOR_ALL"], is_LDS(gen)) + inputs["STOR_LONG_DURATION"] = intersect(inputs["STOR_ALL"], is_LDS(gen)) inputs["STOR_SHORT_DURATION"] = intersect(inputs["STOR_ALL"], is_SDS(gen)) ## VRE @@ -1011,10 +1030,10 @@ function add_resources_to_input_data!(inputs::Dict, setup::Dict, case_path::Abst # Set of thermal resources without unit commitment inputs["THERM_NO_COMMIT"] = no_unit_commitment(gen) # Start-up cost is sum of fixed cost per start startup - inputs["C_Start"] = zeros(Float64, G, T) + inputs["C_Start"] = zeros(Float64, G, T) for g in inputs["THERM_COMMIT"] start_up_cost = start_cost_per_mw(gen[g]) * cap_size(gen[g]) - inputs["C_Start"][g,:] .= start_up_cost + inputs["C_Start"][g, :] .= start_up_cost end # Piecewise fuel usage option process_piecewisefuelusage!(setup, gen, inputs) @@ -1027,27 +1046,28 @@ function add_resources_to_input_data!(inputs::Dict, setup::Dict, case_path::Abst # For now, the only resources eligible for UC are themal resources inputs["COMMIT"] = inputs["THERM_COMMIT"] - # Set of CCS resources (optional set): + # Set of CCS resources (optional set): inputs["CCS"] = ids_with_positive(gen, co2_capture_fraction) # Single-fuel resources - inputs["SINGLE_FUEL"] = ids_with_singlefuel(gen) - # Multi-fuel resources - inputs["MULTI_FUELS"] = ids_with_multifuels(gen) - if !isempty(inputs["MULTI_FUELS"]) # If there are any resources using multi fuels, read relevant data - load_multi_fuels_data!(inputs, gen, setup, case_path) - end + inputs["SINGLE_FUEL"] = ids_with_singlefuel(gen) + # Multi-fuel resources + inputs["MULTI_FUELS"] = ids_with_multifuels(gen) + if !isempty(inputs["MULTI_FUELS"]) # If there are any resources using multi fuels, read relevant data + load_multi_fuels_data!(inputs, gen, setup, case_path) + end buildable = is_buildable(gen) retirable = is_retirable(gen) units_can_retrofit = ids_can_retrofit(gen) - + # Set of all resources eligible for new capacity inputs["NEW_CAP"] = intersect(buildable, ids_with(gen, max_cap_mw)) # Set of all resources eligible for capacity retirements inputs["RET_CAP"] = intersect(retirable, ids_with_nonneg(gen, existing_cap_mw)) # Set of all resources eligible for capacity retrofitting (by Yifu, same with retirement) - inputs["RETROFIT_CAP"] = intersect(units_can_retrofit, ids_with_nonneg(gen, existing_cap_mw)) + inputs["RETROFIT_CAP"] = intersect(units_can_retrofit, + ids_with_nonneg(gen, existing_cap_mw)) inputs["RETROFIT_OPTIONS"] = ids_retrofit_options(gen) # Retrofit @@ -1060,14 +1080,15 @@ function add_resources_to_input_data!(inputs::Dict, setup::Dict, case_path::Abst # in the same cluster either all have Contribute_Min_Retirement set to 1 or none of them do if setup["MultiStage"] == 1 for retrofit_res in inputs["RETROFIT_CAP"] - if !has_all_options_contributing(gen[retrofit_res], gen) && !has_all_options_not_contributing(gen[retrofit_res], gen) + if !has_all_options_contributing(gen[retrofit_res], gen) && + !has_all_options_not_contributing(gen[retrofit_res], gen) msg = "Retrofit options in the same cluster either all have Contribute_Min_Retirement set to 1 or none of them do. \n" * - "Check column Contribute_Min_Retirement in the \"Resource_multistage_data.csv\" file for resource $(resource_name(gen[retrofit_res]))." + "Check column Contribute_Min_Retirement in the \"Resource_multistage_data.csv\" file for resource $(resource_name(gen[retrofit_res]))." @error msg error("Invalid input detected for Contribute_Min_Retirement.") - end - if has_all_options_not_contributing(gen[retrofit_res], gen) && setup["MultiStageSettingsDict"]["Myopic"]==1 + if has_all_options_not_contributing(gen[retrofit_res], gen) && + setup["MultiStageSettingsDict"]["Myopic"] == 1 @error "When performing myopic multistage expansion all retrofit options need to have Contribute_Min_Retirement set to 1 to avoid model infeasibilities." error("Invalid input detected for Contribute_Min_Retirement.") end @@ -1079,34 +1100,44 @@ function add_resources_to_input_data!(inputs::Dict, setup::Dict, case_path::Abst ret_cap_energy = Set{Int64}() if !isempty(inputs["STOR_ALL"]) # Set of all storage resources eligible for new energy capacity - new_cap_energy = intersect(buildable, ids_with(gen, max_cap_mwh), inputs["STOR_ALL"]) + new_cap_energy = intersect(buildable, + ids_with(gen, max_cap_mwh), + inputs["STOR_ALL"]) # Set of all storage resources eligible for energy capacity retirements - ret_cap_energy = intersect(retirable, ids_with_nonneg(gen, existing_cap_mwh), inputs["STOR_ALL"]) + ret_cap_energy = intersect(retirable, + ids_with_nonneg(gen, existing_cap_mwh), + inputs["STOR_ALL"]) end inputs["NEW_CAP_ENERGY"] = new_cap_energy inputs["RET_CAP_ENERGY"] = ret_cap_energy - new_cap_charge = Set{Int64}() - ret_cap_charge = Set{Int64}() - if !isempty(inputs["STOR_ASYMMETRIC"]) - # Set of asymmetric charge/discharge storage resources eligible for new charge capacity - new_cap_charge = intersect(buildable, ids_with(gen, max_charge_cap_mw), inputs["STOR_ASYMMETRIC"]) - # Set of asymmetric charge/discharge storage resources eligible for charge capacity retirements - ret_cap_charge = intersect(retirable, ids_with_nonneg(gen, existing_charge_cap_mw), inputs["STOR_ASYMMETRIC"]) - end - inputs["NEW_CAP_CHARGE"] = new_cap_charge - inputs["RET_CAP_CHARGE"] = ret_cap_charge + new_cap_charge = Set{Int64}() + ret_cap_charge = Set{Int64}() + if !isempty(inputs["STOR_ASYMMETRIC"]) + # Set of asymmetric charge/discharge storage resources eligible for new charge capacity + new_cap_charge = intersect(buildable, + ids_with(gen, max_charge_cap_mw), + inputs["STOR_ASYMMETRIC"]) + # Set of asymmetric charge/discharge storage resources eligible for charge capacity retirements + ret_cap_charge = intersect(retirable, + ids_with_nonneg(gen, existing_charge_cap_mw), + inputs["STOR_ASYMMETRIC"]) + end + inputs["NEW_CAP_CHARGE"] = new_cap_charge + inputs["RET_CAP_CHARGE"] = ret_cap_charge ## Co-located resources # VRE and storage inputs["VRE_STOR"] = vre_stor(gen) # Check if VRE-STOR resources exist - if !isempty(inputs["VRE_STOR"]) + if !isempty(inputs["VRE_STOR"]) # Solar PV Resources inputs["VS_SOLAR"] = solar(gen) # DC Resources - inputs["VS_DC"] = union(storage_dc_discharge(gen), storage_dc_charge(gen), solar(gen)) + inputs["VS_DC"] = union(storage_dc_discharge(gen), + storage_dc_charge(gen), + solar(gen)) # Wind Resources inputs["VS_WIND"] = wind(gen) @@ -1116,39 +1147,71 @@ function add_resources_to_input_data!(inputs::Dict, setup::Dict, case_path::Abst gen_VRE_STOR = gen.VreStorage # Set of all VRE-STOR resources eligible for new solar capacity - inputs["NEW_CAP_SOLAR"] = intersect(buildable, solar(gen), ids_with(gen_VRE_STOR, max_cap_solar_mw)) + inputs["NEW_CAP_SOLAR"] = intersect(buildable, + solar(gen), + ids_with(gen_VRE_STOR, max_cap_solar_mw)) # Set of all VRE_STOR resources eligible for solar capacity retirements - inputs["RET_CAP_SOLAR"] = intersect(retirable, solar(gen), ids_with_nonneg(gen_VRE_STOR, existing_cap_solar_mw)) + inputs["RET_CAP_SOLAR"] = intersect(retirable, + solar(gen), + ids_with_nonneg(gen_VRE_STOR, existing_cap_solar_mw)) # Set of all VRE-STOR resources eligible for new wind capacity - inputs["NEW_CAP_WIND"] = intersect(buildable, wind(gen), ids_with(gen_VRE_STOR, max_cap_wind_mw)) + inputs["NEW_CAP_WIND"] = intersect(buildable, + wind(gen), + ids_with(gen_VRE_STOR, max_cap_wind_mw)) # Set of all VRE_STOR resources eligible for wind capacity retirements - inputs["RET_CAP_WIND"] = intersect(retirable, wind(gen), ids_with_nonneg(gen_VRE_STOR, existing_cap_wind_mw)) + inputs["RET_CAP_WIND"] = intersect(retirable, + wind(gen), + ids_with_nonneg(gen_VRE_STOR, existing_cap_wind_mw)) # Set of all VRE-STOR resources eligible for new inverter capacity - inputs["NEW_CAP_DC"] = intersect(buildable, ids_with(gen_VRE_STOR, max_cap_inverter_mw), inputs["VS_DC"]) + inputs["NEW_CAP_DC"] = intersect(buildable, + ids_with(gen_VRE_STOR, max_cap_inverter_mw), + inputs["VS_DC"]) # Set of all VRE_STOR resources eligible for inverter capacity retirements - inputs["RET_CAP_DC"] = intersect(retirable, ids_with_nonneg(gen_VRE_STOR, existing_cap_inverter_mw), inputs["VS_DC"]) + inputs["RET_CAP_DC"] = intersect(retirable, + ids_with_nonneg(gen_VRE_STOR, existing_cap_inverter_mw), + inputs["VS_DC"]) # Set of all storage resources eligible for new energy capacity - inputs["NEW_CAP_STOR"] = intersect(buildable, ids_with(gen_VRE_STOR, max_cap_mwh), inputs["VS_STOR"]) + inputs["NEW_CAP_STOR"] = intersect(buildable, + ids_with(gen_VRE_STOR, max_cap_mwh), + inputs["VS_STOR"]) # Set of all storage resources eligible for energy capacity retirements - inputs["RET_CAP_STOR"] = intersect(retirable, ids_with_nonneg(gen_VRE_STOR, existing_cap_mwh), inputs["VS_STOR"]) + inputs["RET_CAP_STOR"] = intersect(retirable, + ids_with_nonneg(gen_VRE_STOR, existing_cap_mwh), + inputs["VS_STOR"]) if !isempty(inputs["VS_ASYM"]) # Set of asymmetric charge DC storage resources eligible for new charge capacity - inputs["NEW_CAP_CHARGE_DC"] = intersect(buildable, ids_with(gen_VRE_STOR, max_cap_charge_dc_mw), inputs["VS_ASYM_DC_CHARGE"]) + inputs["NEW_CAP_CHARGE_DC"] = intersect(buildable, + ids_with(gen_VRE_STOR, max_cap_charge_dc_mw), + inputs["VS_ASYM_DC_CHARGE"]) # Set of asymmetric charge DC storage resources eligible for charge capacity retirements - inputs["RET_CAP_CHARGE_DC"] = intersect(retirable, ids_with_nonneg(gen_VRE_STOR, existing_cap_charge_dc_mw), inputs["VS_ASYM_DC_CHARGE"]) + inputs["RET_CAP_CHARGE_DC"] = intersect(retirable, + ids_with_nonneg(gen_VRE_STOR, existing_cap_charge_dc_mw), + inputs["VS_ASYM_DC_CHARGE"]) # Set of asymmetric discharge DC storage resources eligible for new discharge capacity - inputs["NEW_CAP_DISCHARGE_DC"] = intersect(buildable, ids_with(gen_VRE_STOR, max_cap_discharge_dc_mw), inputs["VS_ASYM_DC_DISCHARGE"]) + inputs["NEW_CAP_DISCHARGE_DC"] = intersect(buildable, + ids_with(gen_VRE_STOR, max_cap_discharge_dc_mw), + inputs["VS_ASYM_DC_DISCHARGE"]) # Set of asymmetric discharge DC storage resources eligible for discharge capacity retirements - inputs["RET_CAP_DISCHARGE_DC"] = intersect(retirable, ids_with_nonneg(gen_VRE_STOR, existing_cap_discharge_dc_mw), inputs["VS_ASYM_DC_DISCHARGE"]) + inputs["RET_CAP_DISCHARGE_DC"] = intersect(retirable, + ids_with_nonneg(gen_VRE_STOR, existing_cap_discharge_dc_mw), + inputs["VS_ASYM_DC_DISCHARGE"]) # Set of asymmetric charge AC storage resources eligible for new charge capacity - inputs["NEW_CAP_CHARGE_AC"] = intersect(buildable, ids_with(gen_VRE_STOR, max_cap_charge_ac_mw), inputs["VS_ASYM_AC_CHARGE"]) + inputs["NEW_CAP_CHARGE_AC"] = intersect(buildable, + ids_with(gen_VRE_STOR, max_cap_charge_ac_mw), + inputs["VS_ASYM_AC_CHARGE"]) # Set of asymmetric charge AC storage resources eligible for charge capacity retirements - inputs["RET_CAP_CHARGE_AC"] = intersect(retirable, ids_with_nonneg(gen_VRE_STOR, existing_cap_charge_ac_mw), inputs["VS_ASYM_AC_CHARGE"]) + inputs["RET_CAP_CHARGE_AC"] = intersect(retirable, + ids_with_nonneg(gen_VRE_STOR, existing_cap_charge_ac_mw), + inputs["VS_ASYM_AC_CHARGE"]) # Set of asymmetric discharge AC storage resources eligible for new discharge capacity - inputs["NEW_CAP_DISCHARGE_AC"] = intersect(buildable, ids_with(gen_VRE_STOR, max_cap_discharge_ac_mw), inputs["VS_ASYM_AC_DISCHARGE"]) + inputs["NEW_CAP_DISCHARGE_AC"] = intersect(buildable, + ids_with(gen_VRE_STOR, max_cap_discharge_ac_mw), + inputs["VS_ASYM_AC_DISCHARGE"]) # Set of asymmetric discharge AC storage resources eligible for discharge capacity retirements - inputs["RET_CAP_DISCHARGE_AC"] = intersect(retirable, ids_with_nonneg(gen_VRE_STOR, existing_cap_discharge_ac_mw), inputs["VS_ASYM_AC_DISCHARGE"]) - end + inputs["RET_CAP_DISCHARGE_AC"] = intersect(retirable, + ids_with_nonneg(gen_VRE_STOR, existing_cap_discharge_ac_mw), + inputs["VS_ASYM_AC_DISCHARGE"]) + end # Names for systemwide resources inputs["RESOURCE_NAMES_VRE_STOR"] = resource_name(gen_VRE_STOR) @@ -1174,7 +1237,7 @@ function add_resources_to_input_data!(inputs::Dict, setup::Dict, case_path::Abst # Zones resources are located in zones = zone_id(gen) - + # Resource identifiers by zone (just zones in resource order + resource and zone concatenated) inputs["R_ZONES"] = zones inputs["RESOURCE_ZONES"] = inputs["RESOURCE_NAMES"] .* "_z" .* string.(zones) @@ -1185,7 +1248,7 @@ function add_resources_to_input_data!(inputs::Dict, setup::Dict, case_path::Abst inputs["HAS_FUEL"] = union(inputs["HAS_FUEL"], inputs["MULTI_FUELS"]) sort!(inputs["HAS_FUEL"]) end - + inputs["RESOURCES"] = gen return nothing end @@ -1205,10 +1268,11 @@ function summary(rs::Vector{<:AbstractResource}) println(repeat("-", line_width)) println("\tResource type \t\tNumber of resources") println(repeat("=", line_width)) - for r_type ∈ resource_types + for r_type in resource_types num_rs = length(rs[nameof.(typeof.(rs)) .== r_type]) if num_rs > 0 - r_type ∉ keys(rs_summary_names) && error("Resource type $r_type not found in summary map. Please add it to the map.") + r_type ∉ keys(rs_summary_names) && + error("Resource type $r_type not found in summary map. Please add it to the map.") println("\t", rs_summary_names[r_type], "\t\t", num_rs) end end @@ -1232,11 +1296,14 @@ This function loads resources data from the resources_path folder and create the Raises: DeprecationWarning: If the `Generators_data.csv` file is found, a deprecation warning is issued, together with an error message. """ -function load_resources_data!(inputs::Dict, setup::Dict, case_path::AbstractString, resources_path::AbstractString) +function load_resources_data!(inputs::Dict, + setup::Dict, + case_path::AbstractString, + resources_path::AbstractString) if isfile(joinpath(case_path, "Generators_data.csv")) msg = "The `Generators_data.csv` file was deprecated in release v0.4. " * - "Please use the new interface for generators creation, and see the documentation for additional details." - Base.depwarn(msg, :load_resources_data!, force=true) + "Please use the new interface for generators creation, and see the documentation for additional details." + Base.depwarn(msg, :load_resources_data!, force = true) error("Exiting GenX...") end # create vector of resources from dataframes @@ -1249,7 +1316,7 @@ function load_resources_data!(inputs::Dict, setup::Dict, case_path::AbstractStri # read module files add module-related attributes to resource dataframe add_modules_to_resources!(resources, setup, resources_path) - + # add resources information to inputs dict add_resources_to_input_data!(inputs, setup, case_path, resources) @@ -1264,36 +1331,38 @@ end Function for reading input parameters related to multi fuels """ -function load_multi_fuels_data!(inputs::Dict, gen::Vector{<:AbstractResource}, setup::Dict, path::AbstractString) - - inputs["NUM_FUELS"] = num_fuels.(gen) # Number of fuels that this resource can use - max_fuels = maximum(inputs["NUM_FUELS"]) - inputs["FUEL_COLS"] = [ Symbol(string("Fuel",f)) for f in 1:max_fuels ] - fuel_types = [fuel_cols.(gen, tag=f) for f in 1:max_fuels] - heat_rates = [heat_rate_cols.(gen, tag=f) for f in 1:max_fuels] - max_cofire = [max_cofire_cols.(gen, tag=f) for f in 1:max_fuels] - min_cofire = [min_cofire_cols.(gen, tag=f) for f in 1:max_fuels] - max_cofire_start = [max_cofire_start_cols.(gen, tag=f) for f in 1:max_fuels] - min_cofire_start = [min_cofire_start_cols.(gen, tag=f) for f in 1:max_fuels] - inputs["HEAT_RATES"] = heat_rates - inputs["MAX_COFIRE"] = max_cofire - inputs["MIN_COFIRE"] = min_cofire - inputs["MAX_COFIRE_START"] = max_cofire_start - inputs["MIN_COFIRE_START"] = min_cofire_start - inputs["FUEL_TYPES"] = fuel_types - inputs["MAX_NUM_FUELS"] = max_fuels +function load_multi_fuels_data!(inputs::Dict, + gen::Vector{<:AbstractResource}, + setup::Dict, + path::AbstractString) + inputs["NUM_FUELS"] = num_fuels.(gen) # Number of fuels that this resource can use + max_fuels = maximum(inputs["NUM_FUELS"]) + inputs["FUEL_COLS"] = [Symbol(string("Fuel", f)) for f in 1:max_fuels] + fuel_types = [fuel_cols.(gen, tag = f) for f in 1:max_fuels] + heat_rates = [heat_rate_cols.(gen, tag = f) for f in 1:max_fuels] + max_cofire = [max_cofire_cols.(gen, tag = f) for f in 1:max_fuels] + min_cofire = [min_cofire_cols.(gen, tag = f) for f in 1:max_fuels] + max_cofire_start = [max_cofire_start_cols.(gen, tag = f) for f in 1:max_fuels] + min_cofire_start = [min_cofire_start_cols.(gen, tag = f) for f in 1:max_fuels] + inputs["HEAT_RATES"] = heat_rates + inputs["MAX_COFIRE"] = max_cofire + inputs["MIN_COFIRE"] = min_cofire + inputs["MAX_COFIRE_START"] = max_cofire_start + inputs["MIN_COFIRE_START"] = min_cofire_start + inputs["FUEL_TYPES"] = fuel_types + inputs["MAX_NUM_FUELS"] = max_fuels inputs["MAX_NUM_FUELS"] = max_fuels - # check whether non-zero heat rates are used for resources that only use a single fuel - for f in 1:max_fuels - for hr in heat_rates[f][inputs["SINGLE_FUEL"]] - if hr > 0 - error("Heat rates for multi fuels must be zero when only one fuel is used") - end - end - end - # do not allow the multi-fuel option when piece-wise heat rates are used + # check whether non-zero heat rates are used for resources that only use a single fuel + for f in 1:max_fuels + for hr in heat_rates[f][inputs["SINGLE_FUEL"]] + if hr > 0 + error("Heat rates for multi fuels must be zero when only one fuel is used") + end + end + end + # do not allow the multi-fuel option when piece-wise heat rates are used if haskey(inputs, "THERM_COMMIT_PWFU") && !isempty(inputs["THERM_COMMIT_PWFU"]) - error("Multi-fuel option is not available when piece-wise heat rates are used. Please remove multi fuels to avoid this error.") - end + error("Multi-fuel option is not available when piece-wise heat rates are used. Please remove multi fuels to avoid this error.") + end end diff --git a/src/load_inputs/load_vre_stor_variability.jl b/src/load_inputs/load_vre_stor_variability.jl index 591d2d9876..188780c6ec 100644 --- a/src/load_inputs/load_vre_stor_variability.jl +++ b/src/load_inputs/load_vre_stor_variability.jl @@ -7,39 +7,41 @@ Read input parameters related to hourly maximum capacity factors for the solar P """ function load_vre_stor_variability!(setup::Dict, path::AbstractString, inputs::Dict) - # Hourly capacity factors + # Hourly capacity factors TDR_directory = joinpath(path, setup["TimeDomainReductionFolder"]) # if TDR is used, my_dir = TDR_directory, else my_dir = "system" my_dir = get_systemfiles_path(setup, TDR_directory, path) - - filename1 = "Vre_and_stor_solar_variability.csv" - vre_stor_solar = load_dataframe(joinpath(my_dir, filename1)) - filename2 = "Vre_and_stor_wind_variability.csv" - vre_stor_wind = load_dataframe(joinpath(my_dir, filename2)) + filename1 = "Vre_and_stor_solar_variability.csv" + vre_stor_solar = load_dataframe(joinpath(my_dir, filename1)) - all_resources = inputs["RESOURCE_NAMES"] + filename2 = "Vre_and_stor_wind_variability.csv" + vre_stor_wind = load_dataframe(joinpath(my_dir, filename2)) - function ensure_column_zeros!(vre_stor_df, all_resources) - existing_variability = names(vre_stor_df) - for r in all_resources - if r ∉ existing_variability - ensure_column!(vre_stor_df, r, 0.0) - end - end - end + all_resources = inputs["RESOURCE_NAMES"] - ensure_column_zeros!(vre_stor_solar, all_resources) - ensure_column_zeros!(vre_stor_wind, all_resources) + function ensure_column_zeros!(vre_stor_df, all_resources) + existing_variability = names(vre_stor_df) + for r in all_resources + if r ∉ existing_variability + ensure_column!(vre_stor_df, r, 0.0) + end + end + end - # Reorder DataFrame to R_ID order (order provided in Vre_and_stor_data.csv) - select!(vre_stor_solar, [:Time_Index; Symbol.(all_resources) ]) - select!(vre_stor_wind, [:Time_Index; Symbol.(all_resources) ]) + ensure_column_zeros!(vre_stor_solar, all_resources) + ensure_column_zeros!(vre_stor_wind, all_resources) - # Maximum power output and variability of each energy resource - inputs["pP_Max_Solar"] = transpose(Matrix{Float64}(vre_stor_solar[1:inputs["T"],2:(inputs["G"]+1)])) - inputs["pP_Max_Wind"] = transpose(Matrix{Float64}(vre_stor_wind[1:inputs["T"],2:(inputs["G"]+1)])) + # Reorder DataFrame to R_ID order (order provided in Vre_and_stor_data.csv) + select!(vre_stor_solar, [:Time_Index; Symbol.(all_resources)]) + select!(vre_stor_wind, [:Time_Index; Symbol.(all_resources)]) - println(filename1 * " Successfully Read!") - println(filename2 * " Successfully Read!") + # Maximum power output and variability of each energy resource + inputs["pP_Max_Solar"] = transpose(Matrix{Float64}(vre_stor_solar[1:inputs["T"], + 2:(inputs["G"] + 1)])) + inputs["pP_Max_Wind"] = transpose(Matrix{Float64}(vre_stor_wind[1:inputs["T"], + 2:(inputs["G"] + 1)])) + + println(filename1 * " Successfully Read!") + println(filename2 * " Successfully Read!") end diff --git a/src/model/core/co2.jl b/src/model/core/co2.jl index 0b4f861bba..95c3dd27de 100644 --- a/src/model/core/co2.jl +++ b/src/model/core/co2.jl @@ -51,7 +51,6 @@ eEmissionsCaptureByPlant_{g,t} = CO2\_Capture\_Fraction_y * vFuel_{y,t} * CO2_{ """ function co2!(EP::Model, inputs::Dict) - println("CO2 Module") gen = inputs["RESOURCES"] @@ -66,65 +65,76 @@ function co2!(EP::Model, inputs::Dict) omega = inputs["omega"] if !isempty(MULTI_FUELS) max_fuels = inputs["MAX_NUM_FUELS"] - end + end ### Expressions ### # CO2 emissions from power plants in "Generators_data.csv" # If all the CO2 capture fractions from Generators_data are zeros, the CO2 emissions from thermal generators are determined by fuel consumption times CO2 content per MMBTU if isempty(CCS) - @expression(EP, eEmissionsByPlant[y=1:G, t=1:T], + @expression(EP, eEmissionsByPlant[y = 1:G, t = 1:T], if y in SINGLE_FUEL - ((1-biomass(gen[y])) *(EP[:vFuel][y, t] + EP[:vStartFuel][y, t]) * fuel_CO2[fuel(gen[y])]) + ((1 - biomass(gen[y])) * (EP[:vFuel][y, t] + EP[:vStartFuel][y, t]) * + fuel_CO2[fuel(gen[y])]) else - sum(((1-biomass(gen[y])) *(EP[:vMulFuels][y, i, t] + EP[:vMulStartFuels][y, i, t]) * fuel_CO2[fuel_cols(gen[y], tag=i)]) for i = 1:max_fuels) - end) - else + sum(((1 - biomass(gen[y])) * + (EP[:vMulFuels][y, i, t] + EP[:vMulStartFuels][y, i, t]) * + fuel_CO2[fuel_cols(gen[y], tag = i)]) for i in 1:max_fuels) + end) + else @info "Using the CO2 module to determine the CO2 emissions of CCS-equipped plants" # CO2_Capture_Fraction refers to the CO2 capture rate of CCS equiped power plants at a steady state # CO2_Capture_Fraction_Startup refers to the CO2 capture rate of CCS equiped power plants during startup events - @expression(EP, eEmissionsByPlant[y=1:G, t=1:T], + @expression(EP, eEmissionsByPlant[y = 1:G, t = 1:T], if y in SINGLE_FUEL - (1-biomass(gen[y]) - co2_capture_fraction(gen[y])) * EP[:vFuel][y, t] * fuel_CO2[fuel(gen[y])]+ - (1-biomass(gen[y]) - co2_capture_fraction_startup(gen[y])) * EP[:eStartFuel][y, t] * fuel_CO2[fuel(gen[y])] + (1 - biomass(gen[y]) - co2_capture_fraction(gen[y])) * EP[:vFuel][y, t] * + fuel_CO2[fuel(gen[y])] + + (1 - biomass(gen[y]) - co2_capture_fraction_startup(gen[y])) * + EP[:eStartFuel][y, t] * fuel_CO2[fuel(gen[y])] else - sum((1-biomass(gen[y]) - co2_capture_fraction(gen[y])) * EP[:vMulFuels][y, i, t] * fuel_CO2[fuel_cols(gen[y], tag=i)] for i = 1:max_fuels)+ - sum((1-biomass(gen[y]) - co2_capture_fraction_startup(gen[y])) * EP[:vMulStartFuels][y, i, t] * fuel_CO2[fuel_cols(gen[y], tag=i)] for i = 1:max_fuels) + sum((1 - biomass(gen[y]) - co2_capture_fraction(gen[y])) * + EP[:vMulFuels][y, i, t] * fuel_CO2[fuel_cols(gen[y], tag = i)] + for i in 1:max_fuels) + + sum((1 - biomass(gen[y]) - co2_capture_fraction_startup(gen[y])) * + EP[:vMulStartFuels][y, i, t] * fuel_CO2[fuel_cols(gen[y], tag = i)] + for i in 1:max_fuels) end) # CO2 captured from power plants in "Generators_data.csv" - @expression(EP, eEmissionsCaptureByPlant[y in CCS, t=1:T], + @expression(EP, eEmissionsCaptureByPlant[y in CCS, t = 1:T], if y in SINGLE_FUEL - co2_capture_fraction(gen[y]) * EP[:vFuel][y, t] * fuel_CO2[fuel(gen[y])]+ - co2_capture_fraction_startup(gen[y]) * EP[:eStartFuel][y, t] * fuel_CO2[fuel(gen[y])] + co2_capture_fraction(gen[y]) * EP[:vFuel][y, t] * fuel_CO2[fuel(gen[y])] + + co2_capture_fraction_startup(gen[y]) * EP[:eStartFuel][y, t] * + fuel_CO2[fuel(gen[y])] else - sum(co2_capture_fraction(gen[y]) * EP[:vMulFuels][y, i, t] * fuel_CO2[fuel_cols(gen[y], tag=i)] for i = 1:max_fuels)+ - sum(co2_capture_fraction_startup(gen[y]) * EP[:vMulStartFuels][y, i, t] * fuel_CO2[fuel_cols(gen[y], tag=i)] for i = 1:max_fuels) + sum(co2_capture_fraction(gen[y]) * EP[:vMulFuels][y, i, t] * + fuel_CO2[fuel_cols(gen[y], tag = i)] for i in 1:max_fuels) + + sum(co2_capture_fraction_startup(gen[y]) * EP[:vMulStartFuels][y, i, t] * + fuel_CO2[fuel_cols(gen[y], tag = i)] for i in 1:max_fuels) end) - @expression(EP, eEmissionsCaptureByPlantYear[y in CCS], - sum(omega[t] * eEmissionsCaptureByPlant[y, t] + @expression(EP, eEmissionsCaptureByPlantYear[y in CCS], + sum(omega[t] * eEmissionsCaptureByPlant[y, t] for t in 1:T)) # add CO2 sequestration cost to objective function # when scale factor is on tCO2/MWh = > kt CO2/GWh - @expression(EP, ePlantCCO2Sequestration[y in CCS], - sum(omega[t] * eEmissionsCaptureByPlant[y, t] * + @expression(EP, ePlantCCO2Sequestration[y in CCS], + sum(omega[t] * eEmissionsCaptureByPlant[y, t] * ccs_disposal_cost_per_metric_ton(gen[y]) for t in 1:T)) - - @expression(EP, eZonalCCO2Sequestration[z=1:Z], - sum(ePlantCCO2Sequestration[y] - for y in intersect(resources_in_zone_by_rid(gen,z), CCS))) - - @expression(EP, eTotaleCCO2Sequestration, + + @expression(EP, eZonalCCO2Sequestration[z = 1:Z], + sum(ePlantCCO2Sequestration[y] + for y in intersect(resources_in_zone_by_rid(gen, z), CCS))) + + @expression(EP, eTotaleCCO2Sequestration, sum(eZonalCCO2Sequestration[z] for z in 1:Z)) - + add_to_expression!(EP[:eObj], EP[:eTotaleCCO2Sequestration]) end # emissions by zone - @expression(EP, eEmissionsByZone[z = 1:Z, t = 1:T], - sum(eEmissionsByPlant[y, t] for y in resources_in_zone_by_rid(gen,z))) + @expression(EP, eEmissionsByZone[z = 1:Z, t = 1:T], + sum(eEmissionsByPlant[y, t] for y in resources_in_zone_by_rid(gen, z))) return EP - end diff --git a/src/model/core/discharge/discharge.jl b/src/model/core/discharge/discharge.jl index 6955bfffb1..d67881c942 100644 --- a/src/model/core/discharge/discharge.jl +++ b/src/model/core/discharge/discharge.jl @@ -11,40 +11,40 @@ This module additionally defines contributions to the objective function from va ``` """ function discharge!(EP::Model, inputs::Dict, setup::Dict) + println("Discharge Module") - println("Discharge Module") + gen = inputs["RESOURCES"] - gen = inputs["RESOURCES"] + G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) + T = inputs["T"] # Number of time steps - G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) - T = inputs["T"] # Number of time steps + ### Variables ### - ### Variables ### + # Energy injected into the grid by resource "y" at hour "t" + @variable(EP, vP[y = 1:G, t = 1:T]>=0) - # Energy injected into the grid by resource "y" at hour "t" - @variable(EP, vP[y=1:G,t=1:T] >=0); + ### Expressions ### - ### Expressions ### + ## Objective Function Expressions ## - ## Objective Function Expressions ## + # Variable costs of "generation" for resource "y" during hour "t" = variable O&M + @expression(EP, + eCVar_out[y = 1:G, t = 1:T], + (inputs["omega"][t]*(var_om_cost_per_mwh(gen[y]) * vP[y, t]))) + # Sum individual resource contributions to variable discharging costs to get total variable discharging costs + @expression(EP, eTotalCVarOutT[t = 1:T], sum(eCVar_out[y, t] for y in 1:G)) + @expression(EP, eTotalCVarOut, sum(eTotalCVarOutT[t] for t in 1:T)) - # Variable costs of "generation" for resource "y" during hour "t" = variable O&M - @expression(EP, eCVar_out[y=1:G,t=1:T], (inputs["omega"][t]*(var_om_cost_per_mwh(gen[y])*vP[y,t]))) - # Sum individual resource contributions to variable discharging costs to get total variable discharging costs - @expression(EP, eTotalCVarOutT[t=1:T], sum(eCVar_out[y,t] for y in 1:G)) - @expression(EP, eTotalCVarOut, sum(eTotalCVarOutT[t] for t in 1:T)) - - # Add total variable discharging cost contribution to the objective function - add_to_expression!(EP[:eObj], eTotalCVarOut) - - # ESR Policy - if setup["EnergyShareRequirement"] >= 1 - - @expression(EP, eESRDischarge[ESR=1:inputs["nESR"]], - + sum(inputs["omega"][t] * esr(gen[y],tag=ESR) * EP[:vP][y,t] for y=ids_with_policy(gen, esr, tag=ESR), t=1:T) - - sum(inputs["dfESR"][z,ESR]*inputs["omega"][t]*inputs["pD"][t,z] for t=1:T, z=findall(x->x>0,inputs["dfESR"][:,ESR])) - ) - add_similar_to_expression!(EP[:eESR], eESRDischarge) - end + # Add total variable discharging cost contribution to the objective function + add_to_expression!(EP[:eObj], eTotalCVarOut) + # ESR Policy + if setup["EnergyShareRequirement"] >= 1 + @expression(EP, eESRDischarge[ESR = 1:inputs["nESR"]], + +sum(inputs["omega"][t] * esr(gen[y], tag = ESR) * EP[:vP][y, t] + for y in ids_with_policy(gen, esr, tag = ESR), t in 1:T) + -sum(inputs["dfESR"][z, ESR] * inputs["omega"][t] * inputs["pD"][t, z] + for t in 1:T, z in findall(x -> x > 0, inputs["dfESR"][:, ESR]))) + add_similar_to_expression!(EP[:eESR], eESRDischarge) + end end diff --git a/src/model/core/discharge/investment_discharge.jl b/src/model/core/discharge/investment_discharge.jl index 1bd1a5a07e..2db459fcb8 100755 --- a/src/model/core/discharge/investment_discharge.jl +++ b/src/model/core/discharge/investment_discharge.jl @@ -33,136 +33,150 @@ In addition, this function adds investment and fixed O\&M related costs related ``` """ function investment_discharge!(EP::Model, inputs::Dict, setup::Dict) + println("Investment Discharge Module") + MultiStage = setup["MultiStage"] - println("Investment Discharge Module") - MultiStage = setup["MultiStage"] + gen = inputs["RESOURCES"] - gen = inputs["RESOURCES"] + G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) - G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) - - NEW_CAP = inputs["NEW_CAP"] # Set of all resources eligible for new capacity - RET_CAP = inputs["RET_CAP"] # Set of all resources eligible for capacity retirements - COMMIT = inputs["COMMIT"] # Set of all resources eligible for unit commitment - RETROFIT_CAP = inputs["RETROFIT_CAP"] # Set of all resources being retrofitted + NEW_CAP = inputs["NEW_CAP"] # Set of all resources eligible for new capacity + RET_CAP = inputs["RET_CAP"] # Set of all resources eligible for capacity retirements + COMMIT = inputs["COMMIT"] # Set of all resources eligible for unit commitment + RETROFIT_CAP = inputs["RETROFIT_CAP"] # Set of all resources being retrofitted - ### Variables ### + ### Variables ### - # Retired capacity of resource "y" from existing capacity - @variable(EP, vRETCAP[y in RET_CAP] >= 0); + # Retired capacity of resource "y" from existing capacity + @variable(EP, vRETCAP[y in RET_CAP]>=0) # New installed capacity of resource "y" - @variable(EP, vCAP[y in NEW_CAP] >= 0); - - if MultiStage == 1 - @variable(EP, vEXISTINGCAP[y=1:G] >= 0); - end - - # Being retrofitted capacity of resource y - @variable(EP, vRETROFITCAP[y in RETROFIT_CAP] >= 0); - - - ### Expressions ### - - if MultiStage == 1 - @expression(EP, eExistingCap[y in 1:G], vEXISTINGCAP[y]) - else - @expression(EP, eExistingCap[y in 1:G], existing_cap_mw(gen[y])) - end - - @expression(EP, eTotalCap[y in 1:G], - if y in intersect(NEW_CAP, RET_CAP, RETROFIT_CAP) # Resources eligible for new capacity, retirements and being retrofitted - if y in COMMIT - eExistingCap[y] + cap_size(gen[y])*(EP[:vCAP][y] - EP[:vRETCAP][y] - EP[:vRETROFITCAP][y]) - else - eExistingCap[y] + EP[:vCAP][y] - EP[:vRETCAP][y] - EP[:vRETROFITCAP][y] - end - elseif y in intersect(setdiff(RET_CAP, NEW_CAP), setdiff(RET_CAP, RETROFIT_CAP)) # Resources eligible for only capacity retirements - if y in COMMIT - eExistingCap[y] - cap_size(gen[y])*EP[:vRETCAP][y] - else - eExistingCap[y] - EP[:vRETCAP][y] - end - elseif y in setdiff(intersect(RET_CAP, NEW_CAP), RETROFIT_CAP) # Resources eligible for retirement and new capacity - if y in COMMIT - eExistingCap[y] + cap_size(gen[y])* (EP[:vCAP][y] - EP[:vRETCAP][y]) - else - eExistingCap[y] + EP[:vCAP][y] - EP[:vRETCAP][y] - end - elseif y in setdiff(intersect(RET_CAP, RETROFIT_CAP), NEW_CAP) # Resources eligible for retirement and retrofitting - if y in COMMIT - eExistingCap[y] - cap_size(gen[y]) * (EP[:vRETROFITCAP][y] + EP[:vRETCAP][y]) - else - eExistingCap[y] - (EP[:vRETROFITCAP][y] + EP[:vRETCAP][y]) - end - elseif y in intersect(setdiff(NEW_CAP, RET_CAP),setdiff(NEW_CAP, RETROFIT_CAP)) # Resources eligible for only new capacity - if y in COMMIT - eExistingCap[y] + cap_size(gen[y])*EP[:vCAP][y] - else - eExistingCap[y] + EP[:vCAP][y] - end - else # Resources not eligible for new capacity or retirement - eExistingCap[y] + EP[:vZERO] - end -) - - ### Need editting ## - @expression(EP, eCFix[y in 1:G], - if y in NEW_CAP # Resources eligible for new capacity (Non-Retrofit) - if y in COMMIT - inv_cost_per_mwyr(gen[y])*cap_size(gen[y])*vCAP[y] + fixed_om_cost_per_mwyr(gen[y])*eTotalCap[y] - else - inv_cost_per_mwyr(gen[y])*vCAP[y] + fixed_om_cost_per_mwyr(gen[y])*eTotalCap[y] - end - else - fixed_om_cost_per_mwyr(gen[y])*eTotalCap[y] - end -) - # Sum individual resource contributions to fixed costs to get total fixed costs - @expression(EP, eTotalCFix, sum(EP[:eCFix][y] for y in 1:G)) - - # Add term to objective function expression - if MultiStage == 1 - # OPEX multiplier scales fixed costs to account for multiple years between two model stages - # We divide by OPEXMULT since we are going to multiply the entire objective function by this term later, - # and we have already accounted for multiple years between stages for fixed costs. - add_to_expression!(EP[:eObj], 1/inputs["OPEXMULT"], eTotalCFix) - else - add_to_expression!(EP[:eObj], eTotalCFix) - end - - ### Constratints ### - - if MultiStage == 1 - # Existing capacity variable is equal to existing capacity specified in the input file - @constraint(EP, cExistingCap[y in 1:G], EP[:vEXISTINGCAP][y] == existing_cap_mw(gen[y])) - end - - ## Constraints on retirements and capacity additions - # Cannot retire more capacity than existing capacity - @constraint(EP, cMaxRetNoCommit[y in setdiff(RET_CAP,COMMIT)], vRETCAP[y] <= eExistingCap[y]) - @constraint(EP, cMaxRetCommit[y in intersect(RET_CAP,COMMIT)], cap_size(gen[y])*vRETCAP[y] <= eExistingCap[y]) - @constraint(EP, cMaxRetroNoCommit[y in setdiff(RETROFIT_CAP,COMMIT)], vRETROFITCAP[y] + vRETCAP[y] <= eExistingCap[y]) - @constraint(EP, cMaxRetroCommit[y in intersect(RETROFIT_CAP,COMMIT)], cap_size(gen[y]) * (vRETROFITCAP[y] + vRETCAP[y]) <= eExistingCap[y]) - - ## Constraints on new built capacity - # Constraint on maximum capacity (if applicable) [set input to -1 if no constraint on maximum capacity] - # DEV NOTE: This constraint may be violated in some cases where Existing_Cap_MW is >= Max_Cap_MW and lead to infeasabilty - MAX_CAP = ids_with_positive(gen, max_cap_mw) - @constraint(EP, cMaxCap[y in MAX_CAP], eTotalCap[y] <= max_cap_mw(gen[y])) - - # Constraint on minimum capacity (if applicable) [set input to -1 if no constraint on minimum capacity] - # DEV NOTE: This constraint may be violated in some cases where Existing_Cap_MW is <= Min_Cap_MW and lead to infeasabilty - MIN_CAP = ids_with_positive(gen, min_cap_mw) - @constraint(EP, cMinCap[y in MIN_CAP], eTotalCap[y] >= min_cap_mw(gen[y])) - - if setup["MinCapReq"] == 1 - @expression(EP, eMinCapResInvest[mincap = 1:inputs["NumberOfMinCapReqs"]], sum(EP[:eTotalCap][y] for y in ids_with_policy(gen, min_cap, tag=mincap))) - add_similar_to_expression!(EP[:eMinCapRes], eMinCapResInvest) - end - - if setup["MaxCapReq"] == 1 - @expression(EP, eMaxCapResInvest[maxcap = 1:inputs["NumberOfMaxCapReqs"]], sum(EP[:eTotalCap][y] for y in ids_with_policy(gen, max_cap, tag=maxcap))) - add_similar_to_expression!(EP[:eMaxCapRes], eMaxCapResInvest) - end + @variable(EP, vCAP[y in NEW_CAP]>=0) + + if MultiStage == 1 + @variable(EP, vEXISTINGCAP[y = 1:G]>=0) + end + + # Being retrofitted capacity of resource y + @variable(EP, vRETROFITCAP[y in RETROFIT_CAP]>=0) + + ### Expressions ### + + if MultiStage == 1 + @expression(EP, eExistingCap[y in 1:G], vEXISTINGCAP[y]) + else + @expression(EP, eExistingCap[y in 1:G], existing_cap_mw(gen[y])) + end + + @expression(EP, eTotalCap[y in 1:G], + if y in intersect(NEW_CAP, RET_CAP, RETROFIT_CAP) # Resources eligible for new capacity, retirements and being retrofitted + if y in COMMIT + eExistingCap[y] + + cap_size(gen[y]) * (EP[:vCAP][y] - EP[:vRETCAP][y] - EP[:vRETROFITCAP][y]) + else + eExistingCap[y] + EP[:vCAP][y] - EP[:vRETCAP][y] - EP[:vRETROFITCAP][y] + end + elseif y in intersect(setdiff(RET_CAP, NEW_CAP), setdiff(RET_CAP, RETROFIT_CAP)) # Resources eligible for only capacity retirements + if y in COMMIT + eExistingCap[y] - cap_size(gen[y]) * EP[:vRETCAP][y] + else + eExistingCap[y] - EP[:vRETCAP][y] + end + elseif y in setdiff(intersect(RET_CAP, NEW_CAP), RETROFIT_CAP) # Resources eligible for retirement and new capacity + if y in COMMIT + eExistingCap[y] + cap_size(gen[y]) * (EP[:vCAP][y] - EP[:vRETCAP][y]) + else + eExistingCap[y] + EP[:vCAP][y] - EP[:vRETCAP][y] + end + elseif y in setdiff(intersect(RET_CAP, RETROFIT_CAP), NEW_CAP) # Resources eligible for retirement and retrofitting + if y in COMMIT + eExistingCap[y] - + cap_size(gen[y]) * (EP[:vRETROFITCAP][y] + EP[:vRETCAP][y]) + else + eExistingCap[y] - (EP[:vRETROFITCAP][y] + EP[:vRETCAP][y]) + end + elseif y in intersect(setdiff(NEW_CAP, RET_CAP), setdiff(NEW_CAP, RETROFIT_CAP)) # Resources eligible for only new capacity + if y in COMMIT + eExistingCap[y] + cap_size(gen[y]) * EP[:vCAP][y] + else + eExistingCap[y] + EP[:vCAP][y] + end + else # Resources not eligible for new capacity or retirement + eExistingCap[y] + EP[:vZERO] + end) + + ### Need editting ## + @expression(EP, eCFix[y in 1:G], + if y in NEW_CAP # Resources eligible for new capacity (Non-Retrofit) + if y in COMMIT + inv_cost_per_mwyr(gen[y]) * cap_size(gen[y]) * vCAP[y] + + fixed_om_cost_per_mwyr(gen[y]) * eTotalCap[y] + else + inv_cost_per_mwyr(gen[y]) * vCAP[y] + + fixed_om_cost_per_mwyr(gen[y]) * eTotalCap[y] + end + else + fixed_om_cost_per_mwyr(gen[y]) * eTotalCap[y] + end) + # Sum individual resource contributions to fixed costs to get total fixed costs + @expression(EP, eTotalCFix, sum(EP[:eCFix][y] for y in 1:G)) + + # Add term to objective function expression + if MultiStage == 1 + # OPEX multiplier scales fixed costs to account for multiple years between two model stages + # We divide by OPEXMULT since we are going to multiply the entire objective function by this term later, + # and we have already accounted for multiple years between stages for fixed costs. + add_to_expression!(EP[:eObj], 1 / inputs["OPEXMULT"], eTotalCFix) + else + add_to_expression!(EP[:eObj], eTotalCFix) + end + + ### Constratints ### + + if MultiStage == 1 + # Existing capacity variable is equal to existing capacity specified in the input file + @constraint(EP, + cExistingCap[y in 1:G], + EP[:vEXISTINGCAP][y]==existing_cap_mw(gen[y])) + end + + ## Constraints on retirements and capacity additions + # Cannot retire more capacity than existing capacity + @constraint(EP, + cMaxRetNoCommit[y in setdiff(RET_CAP, COMMIT)], + vRETCAP[y]<=eExistingCap[y]) + @constraint(EP, + cMaxRetCommit[y in intersect(RET_CAP, COMMIT)], + cap_size(gen[y]) * vRETCAP[y]<=eExistingCap[y]) + @constraint(EP, + cMaxRetroNoCommit[y in setdiff(RETROFIT_CAP, COMMIT)], + vRETROFITCAP[y] + vRETCAP[y]<=eExistingCap[y]) + @constraint(EP, + cMaxRetroCommit[y in intersect(RETROFIT_CAP, COMMIT)], + cap_size(gen[y]) * (vRETROFITCAP[y] + vRETCAP[y])<=eExistingCap[y]) + + ## Constraints on new built capacity + # Constraint on maximum capacity (if applicable) [set input to -1 if no constraint on maximum capacity] + # DEV NOTE: This constraint may be violated in some cases where Existing_Cap_MW is >= Max_Cap_MW and lead to infeasabilty + MAX_CAP = ids_with_positive(gen, max_cap_mw) + @constraint(EP, cMaxCap[y in MAX_CAP], eTotalCap[y]<=max_cap_mw(gen[y])) + + # Constraint on minimum capacity (if applicable) [set input to -1 if no constraint on minimum capacity] + # DEV NOTE: This constraint may be violated in some cases where Existing_Cap_MW is <= Min_Cap_MW and lead to infeasabilty + MIN_CAP = ids_with_positive(gen, min_cap_mw) + @constraint(EP, cMinCap[y in MIN_CAP], eTotalCap[y]>=min_cap_mw(gen[y])) + + if setup["MinCapReq"] == 1 + @expression(EP, + eMinCapResInvest[mincap = 1:inputs["NumberOfMinCapReqs"]], + sum(EP[:eTotalCap][y] for y in ids_with_policy(gen, min_cap, tag = mincap))) + add_similar_to_expression!(EP[:eMinCapRes], eMinCapResInvest) + end + + if setup["MaxCapReq"] == 1 + @expression(EP, + eMaxCapResInvest[maxcap = 1:inputs["NumberOfMaxCapReqs"]], + sum(EP[:eTotalCap][y] for y in ids_with_policy(gen, max_cap, tag = maxcap))) + add_similar_to_expression!(EP[:eMaxCapRes], eMaxCapResInvest) + end end diff --git a/src/model/core/fuel.jl b/src/model/core/fuel.jl index b8a8395ff2..253cc50985 100644 --- a/src/model/core/fuel.jl +++ b/src/model/core/fuel.jl @@ -80,7 +80,7 @@ vMulFuels_{y, i, t} <= vPower_{y,t} \times MaxCofire_{i} """ function fuel!(EP::Model, inputs::Dict, setup::Dict) println("Fuel Module") - gen = inputs["RESOURCES"] + gen = inputs["RESOURCES"] T = inputs["T"] # Number of time steps (hours) Z = inputs["Z"] # Number of zones @@ -89,17 +89,17 @@ function fuel!(EP::Model, inputs::Dict, setup::Dict) HAS_FUEL = inputs["HAS_FUEL"] MULTI_FUELS = inputs["MULTI_FUELS"] SINGLE_FUEL = inputs["SINGLE_FUEL"] - + fuels = inputs["fuels"] fuel_costs = inputs["fuel_costs"] omega = inputs["omega"] NUM_FUEL = length(fuels) - + # create variable for fuel consumption for output # for resources that only use a single fuel - @variable(EP, vFuel[y in SINGLE_FUEL, t = 1:T] >= 0) - @variable(EP, vStartFuel[y in SINGLE_FUEL, t = 1:T] >= 0) + @variable(EP, vFuel[y in SINGLE_FUEL, t = 1:T]>=0) + @variable(EP, vStartFuel[y in SINGLE_FUEL, t = 1:T]>=0) # for resources that use multi fuels # vMulFuels[y, f, t]: y - resource ID; f - fuel ID; t: time @@ -108,71 +108,76 @@ function fuel!(EP::Model, inputs::Dict, setup::Dict) heat_rates = inputs["HEAT_RATES"] min_cofire = inputs["MIN_COFIRE"] max_cofire = inputs["MAX_COFIRE"] - min_cofire_start =inputs["MIN_COFIRE_START"] - max_cofire_start =inputs["MAX_COFIRE_START"] - - COFIRE_MAX = [findall(g -> max_cofire_cols(g, tag=i) < 1, gen[MULTI_FUELS]) for i in 1:max_fuels] - COFIRE_MAX_START = [findall(g -> max_cofire_start_cols(g, tag=i) < 1, gen[MULTI_FUELS]) for i in 1:max_fuels] - COFIRE_MIN = [findall(g -> min_cofire_cols(g, tag=i) > 0, gen[MULTI_FUELS]) for i in 1:max_fuels] - COFIRE_MIN_START = [findall(g -> min_cofire_start_cols(g, tag=i) > 0, gen[MULTI_FUELS]) for i in 1:max_fuels] - - @variable(EP, vMulFuels[y in MULTI_FUELS, i = 1:max_fuels, t = 1:T] >= 0) - @variable(EP, vMulStartFuels[y in MULTI_FUELS, i = 1:max_fuels, t = 1:T] >= 0) - end + min_cofire_start = inputs["MIN_COFIRE_START"] + max_cofire_start = inputs["MAX_COFIRE_START"] + + COFIRE_MAX = [findall(g -> max_cofire_cols(g, tag = i) < 1, gen[MULTI_FUELS]) + for i in 1:max_fuels] + COFIRE_MAX_START = [findall(g -> max_cofire_start_cols(g, tag = i) < 1, + gen[MULTI_FUELS]) for i in 1:max_fuels] + COFIRE_MIN = [findall(g -> min_cofire_cols(g, tag = i) > 0, gen[MULTI_FUELS]) + for i in 1:max_fuels] + COFIRE_MIN_START = [findall(g -> min_cofire_start_cols(g, tag = i) > 0, + gen[MULTI_FUELS]) for i in 1:max_fuels] + + @variable(EP, vMulFuels[y in MULTI_FUELS, i = 1:max_fuels, t = 1:T]>=0) + @variable(EP, vMulStartFuels[y in MULTI_FUELS, i = 1:max_fuels, t = 1:T]>=0) + end ### Expressions #### # Fuel consumed on start-up (MMBTU or kMMBTU (scaled)) # if unit commitment is modelled @expression(EP, eStartFuel[y in 1:G, t = 1:T], if y in THERM_COMMIT - (cap_size(gen[y]) * EP[:vSTART][y, t] * - start_fuel_mmbtu_per_mw(gen[y])) + (cap_size(gen[y]) * EP[:vSTART][y, t] * + start_fuel_mmbtu_per_mw(gen[y])) else 0 end) - + # time-series fuel consumption by plant @expression(EP, ePlantFuel_generation[y in 1:G, t = 1:T], if y in SINGLE_FUEL # for single fuel plants EP[:vFuel][y, t] else # for multi fuel plants - sum(EP[:vMulFuels][y, i, t] for i in 1:max_fuels) + sum(EP[:vMulFuels][y, i, t] for i in 1:max_fuels) end) @expression(EP, ePlantFuel_start[y in 1:G, t = 1:T], if y in SINGLE_FUEL # for single fuel plants EP[:vStartFuel][y, t] else # for multi fuel plants - sum(EP[:vMulStartFuels][y, i, t] for i in 1:max_fuels) + sum(EP[:vMulStartFuels][y, i, t] for i in 1:max_fuels) end) # for multi-fuel resources # annual fuel consumption by plant and fuel type if !isempty(MULTI_FUELS) - @expression(EP, ePlantFuelConsumptionYear_multi_generation[y in MULTI_FUELS, i in 1:max_fuels], + @expression(EP, + ePlantFuelConsumptionYear_multi_generation[y in MULTI_FUELS, i in 1:max_fuels], sum(omega[t] * EP[:vMulFuels][y, i, t] for t in 1:T)) - @expression(EP, ePlantFuelConsumptionYear_multi_start[y in MULTI_FUELS, i in 1:max_fuels], + @expression(EP, + ePlantFuelConsumptionYear_multi_start[y in MULTI_FUELS, i in 1:max_fuels], sum(omega[t] * EP[:vMulStartFuels][y, i, t] for t in 1:T)) - @expression(EP, ePlantFuelConsumptionYear_multi[y in MULTI_FUELS, i in 1:max_fuels], - EP[:ePlantFuelConsumptionYear_multi_generation][y, i] + EP[:ePlantFuelConsumptionYear_multi_start][y, i]) + @expression(EP, ePlantFuelConsumptionYear_multi[y in MULTI_FUELS, i in 1:max_fuels], + EP[:ePlantFuelConsumptionYear_multi_generation][y, + i]+EP[:ePlantFuelConsumptionYear_multi_start][y, i]) end # fuel_cost is in $/MMBTU (M$/billion BTU if scaled) # vFuel and eStartFuel is MMBTU (or billion BTU if scaled) # eCFuel_start or eCFuel_out is $ or Million$ - + # Start up fuel cost # for multi-fuel resources if !isempty(MULTI_FUELS) # time-series fuel consumption costs by plant and fuel type during startup - @expression(EP, eCFuelOut_multi_start[y in MULTI_FUELS , i in 1:max_fuels, t = 1:T], - fuel_costs[fuel_cols(gen[y], tag=i)][t] * EP[:vMulStartFuels][y, i, t] - ) + @expression(EP, eCFuelOut_multi_start[y in MULTI_FUELS, i in 1:max_fuels, t = 1:T], + fuel_costs[fuel_cols(gen[y], tag = i)][t]*EP[:vMulStartFuels][y, i, t]) # annual plant level fuel cost by fuel type during generation - @expression(EP, ePlantCFuelOut_multi_start[y in MULTI_FUELS, i in 1:max_fuels], + @expression(EP, ePlantCFuelOut_multi_start[y in MULTI_FUELS, i in 1:max_fuels], sum(omega[t] * EP[:eCFuelOut_multi_start][y, i, t] for t in 1:T)) - end - @expression(EP, eCFuelStart[y = 1:G, t = 1:T], + @expression(EP, eCFuelStart[y = 1:G, t = 1:T], if y in SINGLE_FUEL (fuel_costs[fuel(gen[y])][t] * EP[:vStartFuel][y, t]) else @@ -180,44 +185,40 @@ function fuel!(EP::Model, inputs::Dict, setup::Dict) end) # plant level start-up fuel cost for output - @expression(EP, ePlantCFuelStart[y = 1:G], + @expression(EP, ePlantCFuelStart[y = 1:G], sum(omega[t] * EP[:eCFuelStart][y, t] for t in 1:T)) # zonal level total fuel cost for output - @expression(EP, eZonalCFuelStart[z = 1:Z], - sum(EP[:ePlantCFuelStart][y] for y in resources_in_zone_by_rid(gen,z))) + @expression(EP, eZonalCFuelStart[z = 1:Z], + sum(EP[:ePlantCFuelStart][y] for y in resources_in_zone_by_rid(gen, z))) # Fuel cost for power generation # for multi-fuel resources if !isempty(MULTI_FUELS) # time-series fuel consumption costs by plant and fuel type during generation - @expression(EP, eCFuelOut_multi[y in MULTI_FUELS , i in 1:max_fuels, t = 1:T], - fuel_costs[fuel_cols(gen[y], tag=i)][t] * EP[:vMulFuels][y,i,t] - ) + @expression(EP, eCFuelOut_multi[y in MULTI_FUELS, i in 1:max_fuels, t = 1:T], + fuel_costs[fuel_cols(gen[y], tag = i)][t]*EP[:vMulFuels][y, i, t]) # annual plant level fuel cost by fuel type during generation - @expression(EP, ePlantCFuelOut_multi[y in MULTI_FUELS, i in 1:max_fuels], + @expression(EP, ePlantCFuelOut_multi[y in MULTI_FUELS, i in 1:max_fuels], sum(omega[t] * EP[:eCFuelOut_multi][y, i, t] for t in 1:T)) - end - @expression(EP, eCFuelOut[y = 1:G, t = 1:T], + @expression(EP, eCFuelOut[y = 1:G, t = 1:T], if y in SINGLE_FUEL (fuel_costs[fuel(gen[y])][t] * EP[:vFuel][y, t]) else sum(EP[:eCFuelOut_multi][y, i, t] for i in 1:max_fuels) end) # plant level start-up fuel cost for output - @expression(EP, ePlantCFuelOut[y = 1:G], + @expression(EP, ePlantCFuelOut[y = 1:G], sum(omega[t] * EP[:eCFuelOut][y, t] for t in 1:T)) # zonal level total fuel cost for output - @expression(EP, eZonalCFuelOut[z = 1:Z], - sum(EP[:ePlantCFuelOut][y] for y in resources_in_zone_by_rid(gen,z))) - + @expression(EP, eZonalCFuelOut[z = 1:Z], + sum(EP[:ePlantCFuelOut][y] for y in resources_in_zone_by_rid(gen, z))) # system level total fuel cost for output @expression(EP, eTotalCFuelOut, sum(eZonalCFuelOut[z] for z in 1:Z)) @expression(EP, eTotalCFuelStart, sum(eZonalCFuelStart[z] for z in 1:Z)) - add_to_expression!(EP[:eObj], EP[:eTotalCFuelOut] + EP[:eTotalCFuelStart]) #fuel consumption (MMBTU or Billion BTU) @@ -225,40 +226,43 @@ function fuel!(EP::Model, inputs::Dict, setup::Dict) if !isempty(MULTI_FUELS) @expression(EP, eFuelConsumption_multi[f in 1:NUM_FUEL, t in 1:T], sum((EP[:vMulFuels][y, i, t] + EP[:vMulStartFuels][y, i, t]) #i: fuel id - for i in 1:max_fuels, - y in intersect(resource_id.(gen[fuel_cols.(gen, tag=i) .== string(fuels[f])]), MULTI_FUELS)) - ) + for i in 1:max_fuels, + y in intersect(resource_id.(gen[fuel_cols.(gen, tag = i) .== string(fuels[f])]), + MULTI_FUELS))) end @expression(EP, eFuelConsumption_single[f in 1:NUM_FUEL, t in 1:T], - sum(EP[:vFuel][y, t] + EP[:eStartFuel][y,t] + sum(EP[:vFuel][y, t] + EP[:eStartFuel][y, t] for y in intersect(resources_with_fuel(gen, fuels[f]), SINGLE_FUEL))) - + @expression(EP, eFuelConsumption[f in 1:NUM_FUEL, t in 1:T], if !isempty(MULTI_FUELS) - eFuelConsumption_multi[f, t] + eFuelConsumption_single[f,t] + eFuelConsumption_multi[f, t] + eFuelConsumption_single[f, t] else - eFuelConsumption_single[f,t] + eFuelConsumption_single[f, t] end) @expression(EP, eFuelConsumptionYear[f in 1:NUM_FUEL], sum(omega[t] * EP[:eFuelConsumption][f, t] for t in 1:T)) - ### Constraint ### ### only apply constraint to generators with fuel type other than None - @constraint(EP, cFuelCalculation_single[y in intersect(SINGLE_FUEL, setdiff(HAS_FUEL, THERM_COMMIT)), t = 1:T], - EP[:vFuel][y, t] - EP[:vP][y, t] * heat_rate_mmbtu_per_mwh(gen[y]) == 0) + @constraint(EP, + cFuelCalculation_single[y in intersect(SINGLE_FUEL, setdiff(HAS_FUEL, THERM_COMMIT)), + t = 1:T], + EP[:vFuel][y, t] - EP[:vP][y, t] * heat_rate_mmbtu_per_mwh(gen[y])==0) if !isempty(MULTI_FUELS) - @constraint(EP, cFuelCalculation_multi[y in intersect(MULTI_FUELS, setdiff(HAS_FUEL, THERM_COMMIT)), t = 1:T], - sum(EP[:vMulFuels][y, i, t]/heat_rates[i][y] for i in 1:max_fuels) - EP[:vP][y, t] == 0 - ) + @constraint(EP, + cFuelCalculation_multi[y in intersect(MULTI_FUELS, + setdiff(HAS_FUEL, THERM_COMMIT)), + t = 1:T], + sum(EP[:vMulFuels][y, i, t] / heat_rates[i][y] for i in 1:max_fuels) - + EP[:vP][y, t]==0) end - - if !isempty(THERM_COMMIT) + if !isempty(THERM_COMMIT) # Only apply piecewise fuel consumption to thermal generators in THERM_COMMIT_PWFU set THERM_COMMIT_PWFU = inputs["THERM_COMMIT_PWFU"] # segemnt for piecewise fuel usage @@ -270,61 +274,74 @@ function fuel!(EP::Model, inputs::Dict, setup::Dict) segment_intercept(y, seg) = PWFU_data[y, intercept_cols[seg]] segment_slope(y, seg) = PWFU_data[y, slope_cols[seg]] # constraint for piecewise fuel consumption - @constraint(EP, PiecewiseFuelUsage[y in THERM_COMMIT_PWFU, t = 1:T, seg in segs], - EP[:vFuel][y, t] >= (EP[:vP][y, t] * segment_slope(y, seg) + - EP[:vCOMMIT][y, t] * segment_intercept(y, seg))) + @constraint(EP, + PiecewiseFuelUsage[y in THERM_COMMIT_PWFU, t = 1:T, seg in segs], + EP[:vFuel][y, + t]>=(EP[:vP][y, t] * segment_slope(y, seg) + + EP[:vCOMMIT][y, t] * segment_intercept(y, seg))) end - + # constraint for fuel consumption at a constant heat rate - @constraint(EP, FuelCalculationCommit_single[y in intersect(setdiff(THERM_COMMIT,THERM_COMMIT_PWFU), SINGLE_FUEL), t = 1:T], - EP[:vFuel][y, t] - EP[:vP][y, t] * heat_rate_mmbtu_per_mwh(gen[y]) == 0) + @constraint(EP, + FuelCalculationCommit_single[y in intersect(setdiff(THERM_COMMIT, + THERM_COMMIT_PWFU), + SINGLE_FUEL), + t = 1:T], + EP[:vFuel][y, t] - EP[:vP][y, t] * heat_rate_mmbtu_per_mwh(gen[y])==0) if !isempty(MULTI_FUELS) - @constraint(EP, FuelCalculationCommit_multi[y in intersect(setdiff(THERM_COMMIT,THERM_COMMIT_PWFU), MULTI_FUELS), t = 1:T], - sum(EP[:vMulFuels][y, i, t]/heat_rates[i][y] for i in 1:max_fuels) - EP[:vP][y, t] .== 0 - ) + @constraint(EP, + FuelCalculationCommit_multi[y in intersect(setdiff(THERM_COMMIT, + THERM_COMMIT_PWFU), + MULTI_FUELS), + t = 1:T], + sum(EP[:vMulFuels][y, i, t] / heat_rates[i][y] for i in 1:max_fuels) - + EP[:vP][y, t].==0) end end # constraints on start up fuel use @constraint(EP, cStartFuel_single[y in intersect(THERM_COMMIT, SINGLE_FUEL), t = 1:T], - EP[:vStartFuel][y, t] - (cap_size(gen[y]) * EP[:vSTART][y, t] * start_fuel_mmbtu_per_mw(gen[y])) .== 0 - ) + EP[:vStartFuel][y, t] - + (cap_size(gen[y]) * EP[:vSTART][y, t] * start_fuel_mmbtu_per_mw(gen[y])).==0) if !isempty(MULTI_FUELS) - @constraint(EP, cStartFuel_multi[y in intersect(THERM_COMMIT, MULTI_FUELS), t = 1:T], - sum(EP[:vMulStartFuels][y, i, t] for i in 1:max_fuels) - (cap_size(gen[y]) * EP[:vSTART][y, t] * start_fuel_mmbtu_per_mw(gen[y])) .== 0 - ) + @constraint(EP, + cStartFuel_multi[y in intersect(THERM_COMMIT, MULTI_FUELS), t = 1:T], + sum(EP[:vMulStartFuels][y, i, t] for i in 1:max_fuels) - + (cap_size(gen[y]) * EP[:vSTART][y, t] * start_fuel_mmbtu_per_mw(gen[y])).==0) end # constraints on co-fire ratio of different fuels used by one generator # for example, # fuel2/heat rate >= min_cofire_level * total power # fuel2/heat rate <= max_cofire_level * total power without retrofit - if !isempty(MULTI_FUELS) + if !isempty(MULTI_FUELS) for i in 1:max_fuels # during power generation # cofire constraints without the name due to the loop - @constraint(EP, [y in intersect(MULTI_FUELS, COFIRE_MIN[i]), t = 1:T], - EP[:vMulFuels][y, i, t] >= min_cofire[i][y] * EP[:ePlantFuel_generation][y,t] - ) - @constraint(EP, [y in intersect(MULTI_FUELS, COFIRE_MAX[i]), t = 1:T], - EP[:vMulFuels][y, i, t] <= max_cofire[i][y] * EP[:ePlantFuel_generation][y,t] - ) + @constraint(EP, [y in intersect(MULTI_FUELS, COFIRE_MIN[i]), t = 1:T], + EP[:vMulFuels][y, + i, + t]>=min_cofire[i][y] * EP[:ePlantFuel_generation][y, t]) + @constraint(EP, [y in intersect(MULTI_FUELS, COFIRE_MAX[i]), t = 1:T], + EP[:vMulFuels][y, + i, + t]<=max_cofire[i][y] * EP[:ePlantFuel_generation][y, t]) # startup - @constraint(EP, [y in intersect(MULTI_FUELS, COFIRE_MIN_START[i]), t = 1:T], - EP[:vMulStartFuels][y, i, t] >= min_cofire_start[i][y] * EP[:ePlantFuel_start][y,t] - ) - @constraint(EP, [y in intersect(MULTI_FUELS, COFIRE_MAX_START[i]), t = 1:T], - EP[:vMulStartFuels][y, i, t] <= max_cofire_start[i][y] * EP[:ePlantFuel_start][y,t] - ) + @constraint(EP, [y in intersect(MULTI_FUELS, COFIRE_MIN_START[i]), t = 1:T], + EP[:vMulStartFuels][y, + i, + t]>=min_cofire_start[i][y] * EP[:ePlantFuel_start][y, t]) + @constraint(EP, [y in intersect(MULTI_FUELS, COFIRE_MAX_START[i]), t = 1:T], + EP[:vMulStartFuels][y, + i, + t]<=max_cofire_start[i][y] * EP[:ePlantFuel_start][y, t]) end end return EP end - function resources_with_fuel(rs::Vector{<:AbstractResource}, fuel_name::AbstractString) condition::BitVector = fuel.(rs) .== fuel_name return resource_id.(rs[condition]) end - diff --git a/src/model/core/non_served_energy.jl b/src/model/core/non_served_energy.jl index 4df302172f..0686a92cab 100644 --- a/src/model/core/non_served_energy.jl +++ b/src/model/core/non_served_energy.jl @@ -52,54 +52,61 @@ Additionally, total demand curtailed in each time step cannot exceed total deman ``` """ function non_served_energy!(EP::Model, inputs::Dict, setup::Dict) + println("Non-served Energy Module") - println("Non-served Energy Module") + T = inputs["T"] # Number of time steps + Z = inputs["Z"] # Number of zones + SEG = inputs["SEG"] # Number of demand curtailment segments - T = inputs["T"] # Number of time steps - Z = inputs["Z"] # Number of zones - SEG = inputs["SEG"] # Number of demand curtailment segments + ### Variables ### - ### Variables ### + # Non-served energy/curtailed demand in the segment "s" at hour "t" in zone "z" + @variable(EP, vNSE[s = 1:SEG, t = 1:T, z = 1:Z]>=0) - # Non-served energy/curtailed demand in the segment "s" at hour "t" in zone "z" - @variable(EP, vNSE[s=1:SEG,t=1:T,z=1:Z] >= 0); + ### Expressions ### - ### Expressions ### + ## Objective Function Expressions ## - ## Objective Function Expressions ## + # Cost of non-served energy/curtailed demand at hour "t" in zone "z" + @expression(EP, + eCNSE[s = 1:SEG, t = 1:T, z = 1:Z], + (inputs["omega"][t]*inputs["pC_D_Curtail"][s]*vNSE[s, t, z])) - # Cost of non-served energy/curtailed demand at hour "t" in zone "z" - @expression(EP, eCNSE[s=1:SEG,t=1:T,z=1:Z], (inputs["omega"][t]*inputs["pC_D_Curtail"][s]*vNSE[s,t,z])) + # Sum individual demand segment contributions to non-served energy costs to get total non-served energy costs + # Julia is fastest when summing over one row one column at a time + @expression(EP, eTotalCNSETS[t = 1:T, z = 1:Z], sum(eCNSE[s, t, z] for s in 1:SEG)) + @expression(EP, eTotalCNSET[t = 1:T], sum(eTotalCNSETS[t, z] for z in 1:Z)) + @expression(EP, eTotalCNSE, sum(eTotalCNSET[t] for t in 1:T)) - # Sum individual demand segment contributions to non-served energy costs to get total non-served energy costs - # Julia is fastest when summing over one row one column at a time - @expression(EP, eTotalCNSETS[t=1:T,z=1:Z], sum(eCNSE[s,t,z] for s in 1:SEG)) - @expression(EP, eTotalCNSET[t=1:T], sum(eTotalCNSETS[t,z] for z in 1:Z)) - @expression(EP, eTotalCNSE, sum(eTotalCNSET[t] for t in 1:T)) + # Add total cost contribution of non-served energy/curtailed demand to the objective function + add_to_expression!(EP[:eObj], eTotalCNSE) - # Add total cost contribution of non-served energy/curtailed demand to the objective function - add_to_expression!(EP[:eObj], eTotalCNSE) + ## Power Balance Expressions ## + @expression(EP, ePowerBalanceNse[t = 1:T, z = 1:Z], sum(vNSE[s, t, z] for s in 1:SEG)) - ## Power Balance Expressions ## - @expression(EP, ePowerBalanceNse[t=1:T, z=1:Z], sum(vNSE[s,t,z] for s=1:SEG)) + # Add non-served energy/curtailed demand contribution to power balance expression + add_similar_to_expression!(EP[:ePowerBalance], ePowerBalanceNse) - # Add non-served energy/curtailed demand contribution to power balance expression - add_similar_to_expression!(EP[:ePowerBalance], ePowerBalanceNse) + # Capacity Reserves Margin policy + if setup["CapacityReserveMargin"] > 0 + if SEG >= 2 + @expression(EP, + eCapResMarBalanceNSE[res = 1:inputs["NCapacityReserveMargin"], t = 1:T], + sum(EP[:vNSE][s, t, z] + for s in 2:SEG, z in findall(x -> x != 0, inputs["dfCapRes"][:, res]))) + add_similar_to_expression!(EP[:eCapResMarBalance], eCapResMarBalanceNSE) + end + end - # Capacity Reserves Margin policy - if setup["CapacityReserveMargin"] > 0 - if SEG >=2 - @expression(EP, eCapResMarBalanceNSE[res=1:inputs["NCapacityReserveMargin"], t=1:T], sum(EP[:vNSE][s,t,z] for s in 2:SEG, z in findall(x->x!=0,inputs["dfCapRes"][:,res]))) - add_similar_to_expression!(EP[:eCapResMarBalance], eCapResMarBalanceNSE) - end - end + ### Constratints ### - ### Constratints ### - - # Demand curtailed in each segment of curtailable demands cannot exceed maximum allowable share of demand - @constraint(EP, cNSEPerSeg[s=1:SEG, t=1:T, z=1:Z], vNSE[s,t,z] <= inputs["pMax_D_Curtail"][s]*inputs["pD"][t,z]) - - # Total demand curtailed in each time step (hourly) cannot exceed total demand - @constraint(EP, cMaxNSE[t=1:T, z=1:Z], sum(vNSE[s,t,z] for s=1:SEG) <= inputs["pD"][t,z]) + # Demand curtailed in each segment of curtailable demands cannot exceed maximum allowable share of demand + @constraint(EP, + cNSEPerSeg[s = 1:SEG, t = 1:T, z = 1:Z], + vNSE[s, t, z]<=inputs["pMax_D_Curtail"][s] * inputs["pD"][t, z]) + # Total demand curtailed in each time step (hourly) cannot exceed total demand + @constraint(EP, + cMaxNSE[t = 1:T, z = 1:Z], + sum(vNSE[s, t, z] for s in 1:SEG)<=inputs["pD"][t, z]) end diff --git a/src/model/core/operational_reserves.jl b/src/model/core/operational_reserves.jl index ef26fbe01e..c77c8363a4 100644 --- a/src/model/core/operational_reserves.jl +++ b/src/model/core/operational_reserves.jl @@ -9,17 +9,16 @@ This function sets up reserve decisions and constraints, using the operational_reserves_core()` and operational_reserves_contingency()` functions. """ function operational_reserves!(EP::Model, inputs::Dict, setup::Dict) + UCommit = setup["UCommit"] - UCommit = setup["UCommit"] - - if inputs["pStatic_Contingency"] > 0 || (UCommit >= 1 && inputs["pDynamic_Contingency"] >= 1) - operational_reserves_contingency!(EP, inputs, setup) - end + if inputs["pStatic_Contingency"] > 0 || + (UCommit >= 1 && inputs["pDynamic_Contingency"] >= 1) + operational_reserves_contingency!(EP, inputs, setup) + end - operational_reserves_core!(EP, inputs, setup) + operational_reserves_core!(EP, inputs, setup) end - @doc raw""" operational_reserves_contingency!(EP::Model, inputs::Dict, setup::Dict) @@ -68,71 +67,78 @@ Option 3 (dynamic commitment-based contingency) is expressed by the following se where $M_y$ is a `big M' constant equal to the largest possible capacity that can be installed for generation cluster $y$, and $Contingency\_Aux_{y,z,t} \in [0,1]$ is a binary auxiliary variable that is forced by the second and third equations above to be 1 if the commitment state for that generation cluster $\nu_{y,z,t} > 0$ for any generator $y \in \mathcal{UC}$ and zone $z$ and time period $t$, and can be 0 otherwise. Note that this dynamic commitment-based contingency can only be specified if discrete unit commitment decisions are used (e.g. it will not work if relaxed unit commitment is used). """ function operational_reserves_contingency!(EP::Model, inputs::Dict, setup::Dict) + println("Operational Reserves Contingency Module") - println("Operational Reserves Contingency Module") - - gen = inputs["RESOURCES"] - - T = inputs["T"] # Number of time steps (hours) - UCommit = setup["UCommit"] - COMMIT = inputs["COMMIT"] - - if UCommit >= 1 - pDynamic_Contingency = inputs["pDynamic_Contingency"] - end - - ### Variables ### - - # NOTE: If Dynamic_Contingency == 0, then contingency is a fixed parameter equal the value specified in Operational_reserves.csv via pStatic_Contingency. - if UCommit == 1 && pDynamic_Contingency == 1 - # Contingency = largest installed thermal unit - @variable(EP, vLARGEST_CONTINGENCY >= 0) - # Auxiliary variable that is 0 if vCAP = 0, 1 otherwise - @variable(EP, vCONTINGENCY_AUX[y in COMMIT], Bin) - elseif UCommit == 1 && pDynamic_Contingency == 2 - # Contingency = largest committed thermal unit in each time period - @variable(EP, vLARGEST_CONTINGENCY[t=1:T] >= 0) - # Auxiliary variable that is 0 if vCOMMIT = 0, 1 otherwise - @variable(EP, vCONTINGENCY_AUX[y in COMMIT, t=1:T], Bin) - end - - ### Expressions ### - if UCommit == 1 && pDynamic_Contingency == 1 - # Largest contingency defined as largest installed generator - println("Dynamic Contingency Type 1: Modeling the largest contingency as the largest installed generator") - @expression(EP, eContingencyReq[t=1:T], vLARGEST_CONTINGENCY) - elseif UCommit == 1 && pDynamic_Contingency == 2 - # Largest contingency defined for each hour as largest committed generator - println("Dynamic Contingency Type 2: Modeling the largest contingency as the largest largest committed generator") - @expression(EP, eContingencyReq[t=1:T], vLARGEST_CONTINGENCY[t]) - else - # Largest contingency defined fixed as user-specifed static contingency in MW - println("Static Contingency: Modeling the largest contingency as user-specifed static contingency") - @expression(EP, eContingencyReq[t=1:T], inputs["pStatic_Contingency"]) - end - - ### Constraints ### - - # Dynamic contingency related constraints - # option 1: ensures vLARGEST_CONTINGENCY is greater than the capacity of the largest installed generator - if UCommit == 1 && pDynamic_Contingency == 1 - @constraint(EP, cContingency[y in COMMIT], vLARGEST_CONTINGENCY >= cap_size(gen[y])*vCONTINGENCY_AUX[y] ) - # Ensure vCONTINGENCY_AUX = 0 if total capacity = 0 - @constraint(EP, cContAux1[y in COMMIT], vCONTINGENCY_AUX[y] <= EP[:eTotalCap][y]) - # Ensure vCONTINGENCY_AUX = 1 if total capacity > 0 - @constraint(EP, cContAux2[y in COMMIT], EP[:eTotalCap][y] <= inputs["pContingency_BigM"][y]*vCONTINGENCY_AUX[y]) - - # option 2: ensures vLARGEST_CONTINGENCY is greater than the capacity of the largest commited generator in each hour - elseif UCommit == 1 && pDynamic_Contingency == 2 - @constraint(EP, cContingency[y in COMMIT, t=1:T], vLARGEST_CONTINGENCY[t] >= cap_size(gen[y])*vCONTINGENCY_AUX[y,t] ) - # Ensure vCONTINGENCY_AUX = 0 if vCOMMIT = 0 - @constraint(EP, cContAux[y in COMMIT, t=1:T], vCONTINGENCY_AUX[y,t] <= EP[:vCOMMIT][y,t]) - # Ensure vCONTINGENCY_AUX = 1 if vCOMMIT > 0 - @constraint(EP, cContAux2[y in COMMIT, t=1:T], EP[:vCOMMIT][y, t] <= inputs["pContingency_BigM"][y]*vCONTINGENCY_AUX[y,t]) - end + gen = inputs["RESOURCES"] -end + T = inputs["T"] # Number of time steps (hours) + UCommit = setup["UCommit"] + COMMIT = inputs["COMMIT"] + + if UCommit >= 1 + pDynamic_Contingency = inputs["pDynamic_Contingency"] + end + ### Variables ### + + # NOTE: If Dynamic_Contingency == 0, then contingency is a fixed parameter equal the value specified in Operational_reserves.csv via pStatic_Contingency. + if UCommit == 1 && pDynamic_Contingency == 1 + # Contingency = largest installed thermal unit + @variable(EP, vLARGEST_CONTINGENCY>=0) + # Auxiliary variable that is 0 if vCAP = 0, 1 otherwise + @variable(EP, vCONTINGENCY_AUX[y in COMMIT], Bin) + elseif UCommit == 1 && pDynamic_Contingency == 2 + # Contingency = largest committed thermal unit in each time period + @variable(EP, vLARGEST_CONTINGENCY[t = 1:T]>=0) + # Auxiliary variable that is 0 if vCOMMIT = 0, 1 otherwise + @variable(EP, vCONTINGENCY_AUX[y in COMMIT, t = 1:T], Bin) + end + + ### Expressions ### + if UCommit == 1 && pDynamic_Contingency == 1 + # Largest contingency defined as largest installed generator + println("Dynamic Contingency Type 1: Modeling the largest contingency as the largest installed generator") + @expression(EP, eContingencyReq[t = 1:T], vLARGEST_CONTINGENCY) + elseif UCommit == 1 && pDynamic_Contingency == 2 + # Largest contingency defined for each hour as largest committed generator + println("Dynamic Contingency Type 2: Modeling the largest contingency as the largest largest committed generator") + @expression(EP, eContingencyReq[t = 1:T], vLARGEST_CONTINGENCY[t]) + else + # Largest contingency defined fixed as user-specifed static contingency in MW + println("Static Contingency: Modeling the largest contingency as user-specifed static contingency") + @expression(EP, eContingencyReq[t = 1:T], inputs["pStatic_Contingency"]) + end + + ### Constraints ### + + # Dynamic contingency related constraints + # option 1: ensures vLARGEST_CONTINGENCY is greater than the capacity of the largest installed generator + if UCommit == 1 && pDynamic_Contingency == 1 + @constraint(EP, + cContingency[y in COMMIT], + vLARGEST_CONTINGENCY>=cap_size(gen[y]) * vCONTINGENCY_AUX[y]) + # Ensure vCONTINGENCY_AUX = 0 if total capacity = 0 + @constraint(EP, cContAux1[y in COMMIT], vCONTINGENCY_AUX[y]<=EP[:eTotalCap][y]) + # Ensure vCONTINGENCY_AUX = 1 if total capacity > 0 + @constraint(EP, + cContAux2[y in COMMIT], + EP[:eTotalCap][y]<=inputs["pContingency_BigM"][y] * vCONTINGENCY_AUX[y]) + + # option 2: ensures vLARGEST_CONTINGENCY is greater than the capacity of the largest commited generator in each hour + elseif UCommit == 1 && pDynamic_Contingency == 2 + @constraint(EP, + cContingency[y in COMMIT, t = 1:T], + vLARGEST_CONTINGENCY[t]>=cap_size(gen[y]) * vCONTINGENCY_AUX[y, t]) + # Ensure vCONTINGENCY_AUX = 0 if vCOMMIT = 0 + @constraint(EP, + cContAux[y in COMMIT, t = 1:T], + vCONTINGENCY_AUX[y, t]<=EP[:vCOMMIT][y, t]) + # Ensure vCONTINGENCY_AUX = 1 if vCOMMIT > 0 + @constraint(EP, + cContAux2[y in COMMIT, t = 1:T], + EP[:vCOMMIT][y, t]<=inputs["pContingency_BigM"][y] * vCONTINGENCY_AUX[y, t]) + end +end @doc raw""" operational_reserves_core!(EP::Model, inputs::Dict, setup::Dict) @@ -202,67 +208,82 @@ and $\epsilon^{demand}_{rsv}$ and $\epsilon^{vre}_{rsv}$ are parameters specifyi """ function operational_reserves_core!(EP::Model, inputs::Dict, setup::Dict) - # DEV NOTE: After simplifying reserve changes are integrated/confirmed, should we revise such that reserves can be modeled without UC constraints on? - # Is there a use case for economic dispatch constraints with reserves? + # DEV NOTE: After simplifying reserve changes are integrated/confirmed, should we revise such that reserves can be modeled without UC constraints on? + # Is there a use case for economic dispatch constraints with reserves? - println("Operational Reserves Core Module") + println("Operational Reserves Core Module") - gen = inputs["RESOURCES"] - UCommit = setup["UCommit"] + gen = inputs["RESOURCES"] + UCommit = setup["UCommit"] - T = inputs["T"] # Number of time steps (hours) + T = inputs["T"] # Number of time steps (hours) - REG = inputs["REG"] - RSV = inputs["RSV"] + REG = inputs["REG"] + RSV = inputs["RSV"] STOR_ALL = inputs["STOR_ALL"] pDemand = inputs["pD"] pP_Max(y, t) = inputs["pP_Max"][y, t] - systemwide_hourly_demand = sum(pDemand, dims=2) - must_run_vre_generation(t) = sum(pP_Max(y, t) * EP[:eTotalCap][y] for y in intersect(inputs["VRE"], inputs["MUST_RUN"]); init=0) - - ### Variables ### - - ## Integer Unit Commitment configuration for variables - - ## Decision variables for operational reserves - @variable(EP, vREG[y in REG, t=1:T] >= 0) # Contribution to regulation (primary reserves), assumed to be symmetric (up & down directions equal) - @variable(EP, vRSV[y in RSV, t=1:T] >= 0) # Contribution to operating reserves (secondary reserves or contingency reserves); only model upward reserve requirements - - # Storage techs have two pairs of auxilary variables to reflect contributions to regulation and reserves - # when charging and discharging (primary variable becomes equal to sum of these auxilary variables) - @variable(EP, vREG_discharge[y in intersect(STOR_ALL, REG), t=1:T] >= 0) # Contribution to regulation (primary reserves) (mirrored variable used for storage devices) - @variable(EP, vRSV_discharge[y in intersect(STOR_ALL, RSV), t=1:T] >= 0) # Contribution to operating reserves (secondary reserves) (mirrored variable used for storage devices) - @variable(EP, vREG_charge[y in intersect(STOR_ALL, REG), t=1:T] >= 0) # Contribution to regulation (primary reserves) (mirrored variable used for storage devices) - @variable(EP, vRSV_charge[y in intersect(STOR_ALL, RSV), t=1:T] >= 0) # Contribution to operating reserves (secondary reserves) (mirrored variable used for storage devices) - - @variable(EP, vUNMET_RSV[t=1:T] >= 0) # Unmet operating reserves penalty/cost - - ### Expressions ### - ## Total system reserve expressions - # Regulation requirements as a percentage of demand and scheduled variable renewable energy production in each hour - # Reg up and down requirements are symmetric - @expression(EP, eRegReq[t=1:T], inputs["pReg_Req_Demand"] * systemwide_hourly_demand[t] + - inputs["pReg_Req_VRE"] * must_run_vre_generation(t)) - # Operating reserve up / contingency reserve requirements as ˚a percentage of demand and scheduled variable renewable energy production in each hour - # and the largest single contingency (generator or transmission line outage) - @expression(EP, eRsvReq[t=1:T], inputs["pRsv_Req_Demand"] * systemwide_hourly_demand[t] + - inputs["pRsv_Req_VRE"] * must_run_vre_generation(t)) + systemwide_hourly_demand = sum(pDemand, dims = 2) + function must_run_vre_generation(t) + sum(pP_Max(y, t) * EP[:eTotalCap][y] + for y in intersect(inputs["VRE"], inputs["MUST_RUN"]); + init = 0) + end - # N-1 contingency requirement is considered only if Unit Commitment is being modeled - if UCommit >= 1 && (inputs["pDynamic_Contingency"] >= 1 || inputs["pStatic_Contingency"] > 0) + ### Variables ### + + ## Integer Unit Commitment configuration for variables + + ## Decision variables for operational reserves + @variable(EP, vREG[y in REG, t = 1:T]>=0) # Contribution to regulation (primary reserves), assumed to be symmetric (up & down directions equal) + @variable(EP, vRSV[y in RSV, t = 1:T]>=0) # Contribution to operating reserves (secondary reserves or contingency reserves); only model upward reserve requirements + + # Storage techs have two pairs of auxilary variables to reflect contributions to regulation and reserves + # when charging and discharging (primary variable becomes equal to sum of these auxilary variables) + @variable(EP, vREG_discharge[y in intersect(STOR_ALL, REG), t = 1:T]>=0) # Contribution to regulation (primary reserves) (mirrored variable used for storage devices) + @variable(EP, vRSV_discharge[y in intersect(STOR_ALL, RSV), t = 1:T]>=0) # Contribution to operating reserves (secondary reserves) (mirrored variable used for storage devices) + @variable(EP, vREG_charge[y in intersect(STOR_ALL, REG), t = 1:T]>=0) # Contribution to regulation (primary reserves) (mirrored variable used for storage devices) + @variable(EP, vRSV_charge[y in intersect(STOR_ALL, RSV), t = 1:T]>=0) # Contribution to operating reserves (secondary reserves) (mirrored variable used for storage devices) + + @variable(EP, vUNMET_RSV[t = 1:T]>=0) # Unmet operating reserves penalty/cost + + ### Expressions ### + ## Total system reserve expressions + # Regulation requirements as a percentage of demand and scheduled variable renewable energy production in each hour + # Reg up and down requirements are symmetric + @expression(EP, + eRegReq[t = 1:T], + inputs["pReg_Req_Demand"] * + systemwide_hourly_demand[t]+ + inputs["pReg_Req_VRE"] * must_run_vre_generation(t)) + # Operating reserve up / contingency reserve requirements as ˚a percentage of demand and scheduled variable renewable energy production in each hour + # and the largest single contingency (generator or transmission line outage) + @expression(EP, + eRsvReq[t = 1:T], + inputs["pRsv_Req_Demand"] * + systemwide_hourly_demand[t]+ + inputs["pRsv_Req_VRE"] * must_run_vre_generation(t)) + + # N-1 contingency requirement is considered only if Unit Commitment is being modeled + if UCommit >= 1 && + (inputs["pDynamic_Contingency"] >= 1 || inputs["pStatic_Contingency"] > 0) add_to_expression!(EP[:eRsvReq], EP[:eContingencyReq]) - end - - ## Objective Function Expressions ## + end - # Penalty for unmet operating reserves - @expression(EP, eCRsvPen[t=1:T], inputs["omega"][t]*inputs["pC_Rsv_Penalty"]*vUNMET_RSV[t]) - @expression(EP, eTotalCRsvPen, sum(eCRsvPen[t] for t=1:T) + - sum(reg_cost(gen[y])*vRSV[y,t] for y in RSV, t=1:T) + - sum(rsv_cost(gen[y])*vREG[y,t] for y in REG, t=1:T) ) - add_to_expression!(EP[:eObj], eTotalCRsvPen) + ## Objective Function Expressions ## + + # Penalty for unmet operating reserves + @expression(EP, + eCRsvPen[t = 1:T], + inputs["omega"][t]*inputs["pC_Rsv_Penalty"]*vUNMET_RSV[t]) + @expression(EP, + eTotalCRsvPen, + sum(eCRsvPen[t] for t in 1:T)+ + sum(reg_cost(gen[y]) * vRSV[y, t] for y in RSV, t in 1:T)+ + sum(rsv_cost(gen[y]) * vREG[y, t] for y in REG, t in 1:T)) + add_to_expression!(EP[:eObj], eTotalCRsvPen) end function operational_reserves_constraints!(EP, inputs) @@ -283,9 +304,13 @@ function operational_reserves_constraints!(EP, inputs) # contributing to regulation are assumed to contribute equal capacity to both up # and down directions if !isempty(REG) - @constraint(EP, cReg[t=1:T], sum(vREG[y,t] for y in REG) >= eRegulationRequirement[t]) + @constraint(EP, + cReg[t = 1:T], + sum(vREG[y, t] for y in REG)>=eRegulationRequirement[t]) end if !isempty(RSV) - @constraint(EP, cRsvReq[t=1:T], sum(vRSV[y,t] for y in RSV) + vUNMET_RSV[t] >= eReserveRequirement[t]) + @constraint(EP, + cRsvReq[t = 1:T], + sum(vRSV[y, t] for y in RSV) + vUNMET_RSV[t]>=eReserveRequirement[t]) end end diff --git a/src/model/core/transmission/dcopf_transmission.jl b/src/model/core/transmission/dcopf_transmission.jl index 1b2b853ddd..c833c532f2 100644 --- a/src/model/core/transmission/dcopf_transmission.jl +++ b/src/model/core/transmission/dcopf_transmission.jl @@ -23,32 +23,37 @@ Finally, we enforce the reference voltage phase angle constraint: """ function dcopf_transmission!(EP::Model, inputs::Dict, setup::Dict) - - println("DC-OPF Module") - - T = inputs["T"] # Number of time steps (hours) - Z = inputs["Z"] # Number of zones - L = inputs["L"] # Number of transmission lines - - ### DC-OPF variables ### - - # Voltage angle variables of each zone "z" at hour "t" - @variable(EP, vANGLE[z=1:Z,t=1:T]) - - ### DC-OPF constraints ### - - # Power flow constraint:: vFLOW = DC_OPF_coeff * (vANGLE[START_ZONE] - vANGLE[END_ZONE]) - @constraint(EP, cPOWER_FLOW_OPF[l=1:L, t=1:T], EP[:vFLOW][l,t] == inputs["pDC_OPF_coeff"][l] * sum(inputs["pNet_Map"][l,z] * vANGLE[z,t] for z=1:Z)) - - # Bus angle limits (except slack bus) - @constraints(EP, begin - cANGLE_ub[l=1:L, t=1:T], sum(inputs["pNet_Map"][l,z] * vANGLE[z,t] for z=1:Z) <= inputs["Line_Angle_Limit"][l] - cANGLE_lb[l=1:L, t=1:T], sum(inputs["pNet_Map"][l,z] * vANGLE[z,t] for z=1:Z) >= -inputs["Line_Angle_Limit"][l] - end) - - # Slack Bus angle limit - @constraint(EP, cANGLE_SLACK[t=1:T], vANGLE[1,t]== 0) - - - + println("DC-OPF Module") + + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones + L = inputs["L"] # Number of transmission lines + + ### DC-OPF variables ### + + # Voltage angle variables of each zone "z" at hour "t" + @variable(EP, vANGLE[z = 1:Z, t = 1:T]) + + ### DC-OPF constraints ### + + # Power flow constraint:: vFLOW = DC_OPF_coeff * (vANGLE[START_ZONE] - vANGLE[END_ZONE]) + @constraint(EP, + cPOWER_FLOW_OPF[l = 1:L, t = 1:T], + EP[:vFLOW][l, + t]==inputs["pDC_OPF_coeff"][l] * + sum(inputs["pNet_Map"][l, z] * vANGLE[z, t] for z in 1:Z)) + + # Bus angle limits (except slack bus) + @constraints(EP, + begin + cANGLE_ub[l = 1:L, t = 1:T], + sum(inputs["pNet_Map"][l, z] * vANGLE[z, t] for z in 1:Z) <= + inputs["Line_Angle_Limit"][l] + cANGLE_lb[l = 1:L, t = 1:T], + sum(inputs["pNet_Map"][l, z] * vANGLE[z, t] for z in 1:Z) >= + -inputs["Line_Angle_Limit"][l] + end) + + # Slack Bus angle limit + @constraint(EP, cANGLE_SLACK[t = 1:T], vANGLE[1, t]==0) end diff --git a/src/model/core/transmission/investment_transmission.jl b/src/model/core/transmission/investment_transmission.jl index 685d5d8046..813c06aab3 100644 --- a/src/model/core/transmission/investment_transmission.jl +++ b/src/model/core/transmission/investment_transmission.jl @@ -24,87 +24,90 @@ ``` """ function investment_transmission!(EP::Model, inputs::Dict, setup::Dict) - - println("Investment Transmission Module") - - L = inputs["L"] # Number of transmission lines - NetworkExpansion = setup["NetworkExpansion"] - MultiStage = setup["MultiStage"] - - if NetworkExpansion == 1 - # Network lines and zones that are expandable have non-negative maximum reinforcement inputs - EXPANSION_LINES = inputs["EXPANSION_LINES"] - end - - ### Variables ### - - if MultiStage == 1 - @variable(EP, vTRANSMAX[l=1:L] >= 0) - end - - if NetworkExpansion == 1 - # Transmission network capacity reinforcements per line - @variable(EP, vNEW_TRANS_CAP[l in EXPANSION_LINES] >= 0) - end - - - ### Expressions ### - - if MultiStage == 1 - @expression(EP, eTransMax[l=1:L], vTRANSMAX[l]) - else - @expression(EP, eTransMax[l=1:L], inputs["pTrans_Max"][l]) - end - - ## Transmission power flow and loss related expressions: - # Total availabile maximum transmission capacity is the sum of existing maximum transmission capacity plus new transmission capacity - if NetworkExpansion == 1 - @expression(EP, eAvail_Trans_Cap[l=1:L], - if l in EXPANSION_LINES - eTransMax[l] + vNEW_TRANS_CAP[l] - else - eTransMax[l] + EP[:vZERO] - end - ) - else - @expression(EP, eAvail_Trans_Cap[l=1:L], eTransMax[l] + EP[:vZERO]) - end - - ## Objective Function Expressions ## - - if NetworkExpansion == 1 - @expression(EP, eTotalCNetworkExp, sum(vNEW_TRANS_CAP[l]*inputs["pC_Line_Reinforcement"][l] for l in EXPANSION_LINES)) - - if MultiStage == 1 - # OPEX multiplier to count multiple years between two model stages - # We divide by OPEXMULT since we are going to multiply the entire objective function by this term later, - # and we have already accounted for multiple years between stages for fixed costs. - add_to_expression!(EP[:eObj], (1/inputs["OPEXMULT"]), eTotalCNetworkExp) - else - add_to_expression!(EP[:eObj], eTotalCNetworkExp) - end - end - - ## End Objective Function Expressions ## - - ### Constraints ### - - if MultiStage == 1 - # Linking constraint for existing transmission capacity - @constraint(EP, cExistingTransCap[l=1:L], vTRANSMAX[l] == inputs["pTrans_Max"][l]) - end - - - # If network expansion is used: - if NetworkExpansion == 1 - # Transmission network related power flow and capacity constraints - if MultiStage == 1 - # Constrain maximum possible flow for lines eligible for expansion regardless of previous expansions - @constraint(EP, cMaxFlowPossible[l in EXPANSION_LINES], eAvail_Trans_Cap[l] <= inputs["pTrans_Max_Possible"][l]) - end - # Constrain maximum single-stage line capacity reinforcement for lines eligible for expansion - @constraint(EP, cMaxLineReinforcement[l in EXPANSION_LINES], vNEW_TRANS_CAP[l] <= inputs["pMax_Line_Reinforcement"][l]) - end - #END network expansion contraints + println("Investment Transmission Module") + + L = inputs["L"] # Number of transmission lines + NetworkExpansion = setup["NetworkExpansion"] + MultiStage = setup["MultiStage"] + + if NetworkExpansion == 1 + # Network lines and zones that are expandable have non-negative maximum reinforcement inputs + EXPANSION_LINES = inputs["EXPANSION_LINES"] + end + + ### Variables ### + + if MultiStage == 1 + @variable(EP, vTRANSMAX[l = 1:L]>=0) + end + + if NetworkExpansion == 1 + # Transmission network capacity reinforcements per line + @variable(EP, vNEW_TRANS_CAP[l in EXPANSION_LINES]>=0) + end + + ### Expressions ### + + if MultiStage == 1 + @expression(EP, eTransMax[l = 1:L], vTRANSMAX[l]) + else + @expression(EP, eTransMax[l = 1:L], inputs["pTrans_Max"][l]) + end + + ## Transmission power flow and loss related expressions: + # Total availabile maximum transmission capacity is the sum of existing maximum transmission capacity plus new transmission capacity + if NetworkExpansion == 1 + @expression(EP, eAvail_Trans_Cap[l = 1:L], + if l in EXPANSION_LINES + eTransMax[l] + vNEW_TRANS_CAP[l] + else + eTransMax[l] + EP[:vZERO] + end) + else + @expression(EP, eAvail_Trans_Cap[l = 1:L], eTransMax[l]+EP[:vZERO]) + end + + ## Objective Function Expressions ## + + if NetworkExpansion == 1 + @expression(EP, + eTotalCNetworkExp, + sum(vNEW_TRANS_CAP[l] * inputs["pC_Line_Reinforcement"][l] + for l in EXPANSION_LINES)) + + if MultiStage == 1 + # OPEX multiplier to count multiple years between two model stages + # We divide by OPEXMULT since we are going to multiply the entire objective function by this term later, + # and we have already accounted for multiple years between stages for fixed costs. + add_to_expression!(EP[:eObj], (1 / inputs["OPEXMULT"]), eTotalCNetworkExp) + else + add_to_expression!(EP[:eObj], eTotalCNetworkExp) + end + end + + ## End Objective Function Expressions ## + + ### Constraints ### + + if MultiStage == 1 + # Linking constraint for existing transmission capacity + @constraint(EP, cExistingTransCap[l = 1:L], vTRANSMAX[l]==inputs["pTrans_Max"][l]) + end + + # If network expansion is used: + if NetworkExpansion == 1 + # Transmission network related power flow and capacity constraints + if MultiStage == 1 + # Constrain maximum possible flow for lines eligible for expansion regardless of previous expansions + @constraint(EP, + cMaxFlowPossible[l in EXPANSION_LINES], + eAvail_Trans_Cap[l]<=inputs["pTrans_Max_Possible"][l]) + end + # Constrain maximum single-stage line capacity reinforcement for lines eligible for expansion + @constraint(EP, + cMaxLineReinforcement[l in EXPANSION_LINES], + vNEW_TRANS_CAP[l]<=inputs["pMax_Line_Reinforcement"][l]) + end + #END network expansion contraints end diff --git a/src/model/core/transmission/transmission.jl b/src/model/core/transmission/transmission.jl index 12aa50cd85..342b2d7610 100644 --- a/src/model/core/transmission/transmission.jl +++ b/src/model/core/transmission/transmission.jl @@ -84,177 +84,235 @@ As with losses option 2, this segment-wise approximation of a quadratic loss fun ``` """ function transmission!(EP::Model, inputs::Dict, setup::Dict) - - println("Transmission Module") - T = inputs["T"] # Number of time steps (hours) - Z = inputs["Z"] # Number of zones - L = inputs["L"] # Number of transmission lines - - UCommit = setup["UCommit"] - CapacityReserveMargin = setup["CapacityReserveMargin"] - EnergyShareRequirement = setup["EnergyShareRequirement"] - IncludeLossesInESR = setup["IncludeLossesInESR"] - - ## sets and indices for transmission losses - TRANS_LOSS_SEGS = inputs["TRANS_LOSS_SEGS"] # Number of segments used in piecewise linear approximations quadratic loss functions - can only take values of TRANS_LOSS_SEGS =1, 2 - LOSS_LINES = inputs["LOSS_LINES"] # Lines for which loss coefficients apply (are non-zero); - - - ### Variables ### - - # Power flow on each transmission line "l" at hour "t" - @variable(EP, vFLOW[l=1:L,t=1:T]); - - if (TRANS_LOSS_SEGS==1) #loss is a constant times absolute value of power flow - # Positive and negative flow variables - @variable(EP, vTAUX_NEG[l in LOSS_LINES,t=1:T] >= 0) - @variable(EP, vTAUX_POS[l in LOSS_LINES,t=1:T] >= 0) - - if UCommit == 1 - # Single binary variable to ensure positive or negative flows only - @variable(EP, vTAUX_POS_ON[l in LOSS_LINES,t=1:T],Bin) - # Continuous variable representing product of binary variable (vTAUX_POS_ON) and avail transmission capacity - @variable(EP, vPROD_TRANSCAP_ON[l in LOSS_LINES,t=1:T]>=0) - end - else # TRANS_LOSS_SEGS>1 - # Auxiliary variables for linear piecewise interpolation of quadratic losses - @variable(EP, vTAUX_NEG[l in LOSS_LINES, s=0:TRANS_LOSS_SEGS, t=1:T] >= 0) - @variable(EP, vTAUX_POS[l in LOSS_LINES, s=0:TRANS_LOSS_SEGS, t=1:T] >= 0) - if UCommit == 1 - # Binary auxilary variables for each segment >1 to ensure segments fill in order - @variable(EP, vTAUX_POS_ON[l in LOSS_LINES, s=1:TRANS_LOSS_SEGS, t=1:T], Bin) - @variable(EP, vTAUX_NEG_ON[l in LOSS_LINES, s=1:TRANS_LOSS_SEGS, t=1:T], Bin) - end + println("Transmission Module") + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones + L = inputs["L"] # Number of transmission lines + + UCommit = setup["UCommit"] + CapacityReserveMargin = setup["CapacityReserveMargin"] + EnergyShareRequirement = setup["EnergyShareRequirement"] + IncludeLossesInESR = setup["IncludeLossesInESR"] + + ## sets and indices for transmission losses + TRANS_LOSS_SEGS = inputs["TRANS_LOSS_SEGS"] # Number of segments used in piecewise linear approximations quadratic loss functions - can only take values of TRANS_LOSS_SEGS =1, 2 + LOSS_LINES = inputs["LOSS_LINES"] # Lines for which loss coefficients apply (are non-zero); + + ### Variables ### + + # Power flow on each transmission line "l" at hour "t" + @variable(EP, vFLOW[l = 1:L, t = 1:T]) + + if (TRANS_LOSS_SEGS == 1) #loss is a constant times absolute value of power flow + # Positive and negative flow variables + @variable(EP, vTAUX_NEG[l in LOSS_LINES, t = 1:T]>=0) + @variable(EP, vTAUX_POS[l in LOSS_LINES, t = 1:T]>=0) + + if UCommit == 1 + # Single binary variable to ensure positive or negative flows only + @variable(EP, vTAUX_POS_ON[l in LOSS_LINES, t = 1:T], Bin) + # Continuous variable representing product of binary variable (vTAUX_POS_ON) and avail transmission capacity + @variable(EP, vPROD_TRANSCAP_ON[l in LOSS_LINES, t = 1:T]>=0) + end + else # TRANS_LOSS_SEGS>1 + # Auxiliary variables for linear piecewise interpolation of quadratic losses + @variable(EP, vTAUX_NEG[l in LOSS_LINES, s = 0:TRANS_LOSS_SEGS, t = 1:T]>=0) + @variable(EP, vTAUX_POS[l in LOSS_LINES, s = 0:TRANS_LOSS_SEGS, t = 1:T]>=0) + if UCommit == 1 + # Binary auxilary variables for each segment >1 to ensure segments fill in order + @variable(EP, + vTAUX_POS_ON[l in LOSS_LINES, s = 1:TRANS_LOSS_SEGS, t = 1:T], + Bin) + @variable(EP, + vTAUX_NEG_ON[l in LOSS_LINES, s = 1:TRANS_LOSS_SEGS, t = 1:T], + Bin) + end end - # Transmission losses on each transmission line "l" at hour "t" - @variable(EP, vTLOSS[l in LOSS_LINES,t=1:T] >= 0) - - ### Expressions ### - - ## Transmission power flow and loss related expressions: - - # Net power flow outgoing from zone "z" at hour "t" in MW - @expression(EP, eNet_Export_Flows[z=1:Z,t=1:T], sum(inputs["pNet_Map"][l,z] * vFLOW[l,t] for l=1:L)) - - # Losses from power flows into or out of zone "z" in MW - @expression(EP, eLosses_By_Zone[z=1:Z,t=1:T], sum(abs(inputs["pNet_Map"][l,z]) * (1/2) *vTLOSS[l,t] for l in LOSS_LINES)) - - ## Power Balance Expressions ## - - @expression(EP, ePowerBalanceNetExportFlows[t=1:T, z=1:Z], - -eNet_Export_Flows[z,t]) - @expression(EP, ePowerBalanceLossesByZone[t=1:T, z=1:Z], - -eLosses_By_Zone[z,t]) + # Transmission losses on each transmission line "l" at hour "t" + @variable(EP, vTLOSS[l in LOSS_LINES, t = 1:T]>=0) - add_similar_to_expression!(EP[:ePowerBalance], ePowerBalanceLossesByZone) - add_similar_to_expression!(EP[:ePowerBalance], ePowerBalanceNetExportFlows) + ### Expressions ### - # Capacity Reserves Margin policy - if CapacityReserveMargin > 0 - if Z > 1 - @expression(EP, eCapResMarBalanceTrans[res=1:inputs["NCapacityReserveMargin"], t=1:T], sum(inputs["dfTransCapRes_excl"][l,res] * inputs["dfDerateTransCapRes"][l,res]* EP[:vFLOW][l,t] for l in 1:L)) - add_similar_to_expression!(EP[:eCapResMarBalance], -eCapResMarBalanceTrans) - end - end + ## Transmission power flow and loss related expressions: - ### Constraints ### + # Net power flow outgoing from zone "z" at hour "t" in MW + @expression(EP, + eNet_Export_Flows[z = 1:Z, t = 1:T], + sum(inputs["pNet_Map"][l, z] * vFLOW[l, t] for l in 1:L)) - ## Power flow and transmission (between zone) loss related constraints + # Losses from power flows into or out of zone "z" in MW + @expression(EP, + eLosses_By_Zone[z = 1:Z, t = 1:T], + sum(abs(inputs["pNet_Map"][l, z]) * (1 / 2) * vTLOSS[l, t] for l in LOSS_LINES)) - # Maximum power flows, power flow on each transmission line cannot exceed maximum capacity of the line at any hour "t" - @constraints(EP, begin - cMaxFlow_out[l=1:L, t=1:T], vFLOW[l,t] <= EP[:eAvail_Trans_Cap][l] - cMaxFlow_in[l=1:L, t=1:T], vFLOW[l,t] >= -EP[:eAvail_Trans_Cap][l] - end) + ## Power Balance Expressions ## - # Transmission loss related constraints - linear losses as a function of absolute value - if TRANS_LOSS_SEGS == 1 + @expression(EP, ePowerBalanceNetExportFlows[t = 1:T, z = 1:Z], + -eNet_Export_Flows[z, t]) + @expression(EP, ePowerBalanceLossesByZone[t = 1:T, z = 1:Z], + -eLosses_By_Zone[z, t]) - @constraints(EP, begin - # Losses are alpha times absolute values - cTLoss[l in LOSS_LINES, t=1:T], vTLOSS[l,t] == inputs["pPercent_Loss"][l]*(vTAUX_POS[l,t]+vTAUX_NEG[l,t]) + add_similar_to_expression!(EP[:ePowerBalance], ePowerBalanceLossesByZone) + add_similar_to_expression!(EP[:ePowerBalance], ePowerBalanceNetExportFlows) - # Power flow is sum of positive and negative components - cTAuxSum[l in LOSS_LINES, t=1:T], vTAUX_POS[l,t]-vTAUX_NEG[l,t] == vFLOW[l,t] - - # Sum of auxiliary flow variables in either direction cannot exceed maximum line flow capacity - cTAuxLimit[l in LOSS_LINES, t=1:T], vTAUX_POS[l,t]+vTAUX_NEG[l,t] <= EP[:eAvail_Trans_Cap][l] - end) - - if UCommit == 1 - # Constraints to limit phantom losses that can occur to avoid discrete cycling costs/opportunity costs due to min down - @constraints(EP, begin - cTAuxPosUB[l in LOSS_LINES, t=1:T], vTAUX_POS[l,t] <= vPROD_TRANSCAP_ON[l,t] - - # Either negative or positive flows are activated, not both - cTAuxNegUB[l in LOSS_LINES, t=1:T], vTAUX_NEG[l,t] <= EP[:eAvail_Trans_Cap][l]-vPROD_TRANSCAP_ON[l,t] - - # McCormick representation of product of continuous and binary variable - # (in this case, of: vPROD_TRANSCAP_ON[l,t] = EP[:eAvail_Trans_Cap][l] * vTAUX_POS_ON[l,t]) - # McCormick constraint 1 - [l in LOSS_LINES,t=1:T], vPROD_TRANSCAP_ON[l,t] <= inputs["pTrans_Max_Possible"][l]*vTAUX_POS_ON[l,t] - - # McCormick constraint 2 - [l in LOSS_LINES,t=1:T], vPROD_TRANSCAP_ON[l,t] <= EP[:eAvail_Trans_Cap][l] - - # McCormick constraint 3 - [l in LOSS_LINES,t=1:T], vPROD_TRANSCAP_ON[l,t] >= EP[:eAvail_Trans_Cap][l]-(1-vTAUX_POS_ON[l,t])*inputs["pTrans_Max_Possible"][l] - end) - end - - end # End if(TRANS_LOSS_SEGS == 1) block - - # When number of segments is greater than 1 - if (TRANS_LOSS_SEGS > 1) - ## between zone transmission loss constraints - # Losses are expressed as a piecewise approximation of a quadratic function of power flows across each line - # Eq 1: Total losses are function of loss coefficient times the sum of auxilary segment variables across all segments of piecewise approximation - # (Includes both positive domain and negative domain segments) - @constraint(EP, cTLoss[l in LOSS_LINES, t=1:T], vTLOSS[l,t] == - (inputs["pTrans_Loss_Coef"][l]*sum((2*s-1)*(inputs["pTrans_Max_Possible"][l]/TRANS_LOSS_SEGS)*vTAUX_POS[l,s,t] for s=1:TRANS_LOSS_SEGS)) + - (inputs["pTrans_Loss_Coef"][l]*sum((2*s-1)*(inputs["pTrans_Max_Possible"][l]/TRANS_LOSS_SEGS)*vTAUX_NEG[l,s,t] for s=1:TRANS_LOSS_SEGS)) ) - # Eq 2: Sum of auxilary segment variables (s >= 1) minus the "zero" segment (which allows values to go negative) - # from both positive and negative domains must total the actual power flow across the line - @constraints(EP, begin - cTAuxSumPos[l in LOSS_LINES, t=1:T], sum(vTAUX_POS[l,s,t] for s=1:TRANS_LOSS_SEGS)-vTAUX_POS[l,0,t] == vFLOW[l,t] - cTAuxSumNeg[l in LOSS_LINES, t=1:T], sum(vTAUX_NEG[l,s,t] for s=1:TRANS_LOSS_SEGS) - vTAUX_NEG[l,0,t] == -vFLOW[l,t] - end) - if UCommit == 0 || UCommit == 2 - # Eq 3: Each auxilary segment variables (s >= 1) must be less than the maximum power flow in the zone / number of segments - @constraints(EP, begin - cTAuxMaxPos[l in LOSS_LINES, s=1:TRANS_LOSS_SEGS, t=1:T], vTAUX_POS[l,s,t] <= (inputs["pTrans_Max_Possible"][l]/TRANS_LOSS_SEGS) - cTAuxMaxNeg[l in LOSS_LINES, s=1:TRANS_LOSS_SEGS, t=1:T], vTAUX_NEG[l,s,t] <= (inputs["pTrans_Max_Possible"][l]/TRANS_LOSS_SEGS) - end) - else # Constraints that can be ommitted if problem is convex (i.e. if not using MILP unit commitment constraints) - # Eqs 3-4: Ensure that auxilary segment variables do not exceed maximum value per segment and that they - # "fill" in order: i.e. one segment cannot be non-zero unless prior segment is at it's maximum value - # (These constraints are necessary to prevents phantom losses in MILP problems) - @constraints(EP, begin - cTAuxOrderPos1[l in LOSS_LINES, s=1:TRANS_LOSS_SEGS, t=1:T], vTAUX_POS[l,s,t] <= (inputs["pTrans_Max_Possible"][l]/TRANS_LOSS_SEGS)*vTAUX_POS_ON[l,s,t] - cTAuxOrderNeg1[l in LOSS_LINES, s=1:TRANS_LOSS_SEGS, t=1:T], vTAUX_NEG[l,s,t] <= (inputs["pTrans_Max_Possible"][l]/TRANS_LOSS_SEGS)*vTAUX_NEG_ON[l,s,t] - cTAuxOrderPos2[l in LOSS_LINES, s=1:(TRANS_LOSS_SEGS-1), t=1:T], vTAUX_POS[l,s,t] >= (inputs["pTrans_Max_Possible"][l]/TRANS_LOSS_SEGS)*vTAUX_POS_ON[l,s+1,t] - cTAuxOrderNeg2[l in LOSS_LINES, s=1:(TRANS_LOSS_SEGS-1), t=1:T], vTAUX_NEG[l,s,t] >= (inputs["pTrans_Max_Possible"][l]/TRANS_LOSS_SEGS)*vTAUX_NEG_ON[l,s+1,t] - end) - - # Eq 5: Binary constraints to deal with absolute value of vFLOW. - @constraints(EP, begin - # If flow is positive, vTAUX_POS segment 0 must be zero; If flow is negative, vTAUX_POS segment 0 must be positive - # (and takes on value of the full negative flow), forcing all vTAUX_POS other segments (s>=1) to be zero - cTAuxSegmentZeroPos[l in LOSS_LINES, t=1:T], vTAUX_POS[l,0,t] <= inputs["pTrans_Max_Possible"][l]*(1-vTAUX_POS_ON[l,1,t]) - - # If flow is negative, vTAUX_NEG segment 0 must be zero; If flow is positive, vTAUX_NEG segment 0 must be positive - # (and takes on value of the full positive flow), forcing all other vTAUX_NEG segments (s>=1) to be zero - cTAuxSegmentZeroNeg[l in LOSS_LINES, t=1:T], vTAUX_NEG[l,0,t] <= inputs["pTrans_Max_Possible"][l]*(1-vTAUX_NEG_ON[l,1,t]) - end) - end - end # End if(TRANS_LOSS_SEGS > 0) block - - # ESR Lossses - if EnergyShareRequirement >= 1 && IncludeLossesInESR ==1 - @expression(EP, eESRTran[ESR=1:inputs["nESR"]], - sum(inputs["dfESR"][z,ESR]*sum(inputs["omega"][t]*EP[:eLosses_By_Zone][z,t] for t in 1:T) for z=findall(x->x>0,inputs["dfESR"][:,ESR]))) - add_similar_to_expression!(EP[:eESR], -eESRTran) - end + # Capacity Reserves Margin policy + if CapacityReserveMargin > 0 + if Z > 1 + @expression(EP, + eCapResMarBalanceTrans[res = 1:inputs["NCapacityReserveMargin"], t = 1:T], + sum(inputs["dfTransCapRes_excl"][l, res] * + inputs["dfDerateTransCapRes"][l, res] * EP[:vFLOW][l, t] for l in 1:L)) + add_similar_to_expression!(EP[:eCapResMarBalance], -eCapResMarBalanceTrans) + end + end -end \ No newline at end of file + ### Constraints ### + + ## Power flow and transmission (between zone) loss related constraints + + # Maximum power flows, power flow on each transmission line cannot exceed maximum capacity of the line at any hour "t" + @constraints(EP, + begin + cMaxFlow_out[l = 1:L, t = 1:T], vFLOW[l, t] <= EP[:eAvail_Trans_Cap][l] + cMaxFlow_in[l = 1:L, t = 1:T], vFLOW[l, t] >= -EP[:eAvail_Trans_Cap][l] + end) + + # Transmission loss related constraints - linear losses as a function of absolute value + if TRANS_LOSS_SEGS == 1 + @constraints(EP, + begin + # Losses are alpha times absolute values + cTLoss[l in LOSS_LINES, t = 1:T], + vTLOSS[l, t] == + inputs["pPercent_Loss"][l] * (vTAUX_POS[l, t] + vTAUX_NEG[l, t]) + + # Power flow is sum of positive and negative components + cTAuxSum[l in LOSS_LINES, t = 1:T], + vTAUX_POS[l, t] - vTAUX_NEG[l, t] == vFLOW[l, t] + + # Sum of auxiliary flow variables in either direction cannot exceed maximum line flow capacity + cTAuxLimit[l in LOSS_LINES, t = 1:T], + vTAUX_POS[l, t] + vTAUX_NEG[l, t] <= EP[:eAvail_Trans_Cap][l] + end) + + if UCommit == 1 + # Constraints to limit phantom losses that can occur to avoid discrete cycling costs/opportunity costs due to min down + @constraints(EP, + begin + cTAuxPosUB[l in LOSS_LINES, t = 1:T], + vTAUX_POS[l, t] <= vPROD_TRANSCAP_ON[l, t] + + # Either negative or positive flows are activated, not both + cTAuxNegUB[l in LOSS_LINES, t = 1:T], + vTAUX_NEG[l, t] <= EP[:eAvail_Trans_Cap][l] - vPROD_TRANSCAP_ON[l, t] + + # McCormick representation of product of continuous and binary variable + # (in this case, of: vPROD_TRANSCAP_ON[l,t] = EP[:eAvail_Trans_Cap][l] * vTAUX_POS_ON[l,t]) + # McCormick constraint 1 + [l in LOSS_LINES, t = 1:T], + vPROD_TRANSCAP_ON[l, t] <= + inputs["pTrans_Max_Possible"][l] * vTAUX_POS_ON[l, t] + + # McCormick constraint 2 + [l in LOSS_LINES, t = 1:T], + vPROD_TRANSCAP_ON[l, t] <= EP[:eAvail_Trans_Cap][l] + + # McCormick constraint 3 + [l in LOSS_LINES, t = 1:T], + vPROD_TRANSCAP_ON[l, t] >= + EP[:eAvail_Trans_Cap][l] - + (1 - vTAUX_POS_ON[l, t]) * inputs["pTrans_Max_Possible"][l] + end) + end + end # End if(TRANS_LOSS_SEGS == 1) block + + # When number of segments is greater than 1 + if (TRANS_LOSS_SEGS > 1) + ## between zone transmission loss constraints + # Losses are expressed as a piecewise approximation of a quadratic function of power flows across each line + # Eq 1: Total losses are function of loss coefficient times the sum of auxilary segment variables across all segments of piecewise approximation + # (Includes both positive domain and negative domain segments) + @constraint(EP, + cTLoss[l in LOSS_LINES, t = 1:T], + vTLOSS[l, + t]== + (inputs["pTrans_Loss_Coef"][l] * + sum((2 * s - 1) * (inputs["pTrans_Max_Possible"][l] / TRANS_LOSS_SEGS) * + vTAUX_POS[l, s, t] for s in 1:TRANS_LOSS_SEGS)) + + (inputs["pTrans_Loss_Coef"][l] * + sum((2 * s - 1) * (inputs["pTrans_Max_Possible"][l] / TRANS_LOSS_SEGS) * + vTAUX_NEG[l, s, t] for s in 1:TRANS_LOSS_SEGS))) + # Eq 2: Sum of auxilary segment variables (s >= 1) minus the "zero" segment (which allows values to go negative) + # from both positive and negative domains must total the actual power flow across the line + @constraints(EP, + begin + cTAuxSumPos[l in LOSS_LINES, t = 1:T], + sum(vTAUX_POS[l, s, t] for s in 1:TRANS_LOSS_SEGS) - vTAUX_POS[l, 0, t] == + vFLOW[l, t] + cTAuxSumNeg[l in LOSS_LINES, t = 1:T], + sum(vTAUX_NEG[l, s, t] for s in 1:TRANS_LOSS_SEGS) - vTAUX_NEG[l, 0, t] == + -vFLOW[l, t] + end) + if UCommit == 0 || UCommit == 2 + # Eq 3: Each auxilary segment variables (s >= 1) must be less than the maximum power flow in the zone / number of segments + @constraints(EP, + begin + cTAuxMaxPos[l in LOSS_LINES, s = 1:TRANS_LOSS_SEGS, t = 1:T], + vTAUX_POS[l, s, t] <= + (inputs["pTrans_Max_Possible"][l] / TRANS_LOSS_SEGS) + cTAuxMaxNeg[l in LOSS_LINES, s = 1:TRANS_LOSS_SEGS, t = 1:T], + vTAUX_NEG[l, s, t] <= + (inputs["pTrans_Max_Possible"][l] / TRANS_LOSS_SEGS) + end) + else # Constraints that can be ommitted if problem is convex (i.e. if not using MILP unit commitment constraints) + # Eqs 3-4: Ensure that auxilary segment variables do not exceed maximum value per segment and that they + # "fill" in order: i.e. one segment cannot be non-zero unless prior segment is at it's maximum value + # (These constraints are necessary to prevents phantom losses in MILP problems) + @constraints(EP, + begin + cTAuxOrderPos1[l in LOSS_LINES, s = 1:TRANS_LOSS_SEGS, t = 1:T], + vTAUX_POS[l, s, t] <= + (inputs["pTrans_Max_Possible"][l] / TRANS_LOSS_SEGS) * + vTAUX_POS_ON[l, s, t] + cTAuxOrderNeg1[l in LOSS_LINES, s = 1:TRANS_LOSS_SEGS, t = 1:T], + vTAUX_NEG[l, s, t] <= + (inputs["pTrans_Max_Possible"][l] / TRANS_LOSS_SEGS) * + vTAUX_NEG_ON[l, s, t] + cTAuxOrderPos2[l in LOSS_LINES, s = 1:(TRANS_LOSS_SEGS - 1), t = 1:T], + vTAUX_POS[l, s, t] >= + (inputs["pTrans_Max_Possible"][l] / TRANS_LOSS_SEGS) * + vTAUX_POS_ON[l, s + 1, t] + cTAuxOrderNeg2[l in LOSS_LINES, s = 1:(TRANS_LOSS_SEGS - 1), t = 1:T], + vTAUX_NEG[l, s, t] >= + (inputs["pTrans_Max_Possible"][l] / TRANS_LOSS_SEGS) * + vTAUX_NEG_ON[l, s + 1, t] + end) + + # Eq 5: Binary constraints to deal with absolute value of vFLOW. + @constraints(EP, + begin + # If flow is positive, vTAUX_POS segment 0 must be zero; If flow is negative, vTAUX_POS segment 0 must be positive + # (and takes on value of the full negative flow), forcing all vTAUX_POS other segments (s>=1) to be zero + cTAuxSegmentZeroPos[l in LOSS_LINES, t = 1:T], + vTAUX_POS[l, 0, t] <= + inputs["pTrans_Max_Possible"][l] * (1 - vTAUX_POS_ON[l, 1, t]) + + # If flow is negative, vTAUX_NEG segment 0 must be zero; If flow is positive, vTAUX_NEG segment 0 must be positive + # (and takes on value of the full positive flow), forcing all other vTAUX_NEG segments (s>=1) to be zero + cTAuxSegmentZeroNeg[l in LOSS_LINES, t = 1:T], + vTAUX_NEG[l, 0, t] <= + inputs["pTrans_Max_Possible"][l] * (1 - vTAUX_NEG_ON[l, 1, t]) + end) + end + end # End if(TRANS_LOSS_SEGS > 0) block + + # ESR Lossses + if EnergyShareRequirement >= 1 && IncludeLossesInESR == 1 + @expression(EP, eESRTran[ESR = 1:inputs["nESR"]], + sum(inputs["dfESR"][z, ESR] * + sum(inputs["omega"][t] * EP[:eLosses_By_Zone][z, t] for t in 1:T) + for z in findall(x -> x > 0, inputs["dfESR"][:, ESR]))) + add_similar_to_expression!(EP[:eESR], -eESRTran) + end +end diff --git a/src/model/core/ucommit.jl b/src/model/core/ucommit.jl index c85a03b31a..5db836a24b 100644 --- a/src/model/core/ucommit.jl +++ b/src/model/core/ucommit.jl @@ -23,52 +23,53 @@ The total cost of start-ups across all generators subject to unit commitment ($y The sum of start-up costs is added to the objective function. """ function ucommit!(EP::Model, inputs::Dict, setup::Dict) - - println("Unit Commitment Module") - - T = inputs["T"] # Number of time steps (hours) - COMMIT = inputs["COMMIT"] # For not, thermal resources are the only ones eligible for Unit Committment - - ### Variables ### - - ## Decision variables for unit commitment - # commitment state variable - @variable(EP, vCOMMIT[y in COMMIT, t=1:T] >= 0) - # startup event variable - @variable(EP, vSTART[y in COMMIT, t=1:T] >= 0) - # shutdown event variable - @variable(EP, vSHUT[y in COMMIT, t=1:T] >= 0) - - ### Expressions ### - - ## Objective Function Expressions ## - - # Startup costs of "generation" for resource "y" during hour "t" - @expression(EP, eCStart[y in COMMIT, t=1:T],(inputs["omega"][t]*inputs["C_Start"][y,t]*vSTART[y,t])) - - # Julia is fastest when summing over one row one column at a time - @expression(EP, eTotalCStartT[t=1:T], sum(eCStart[y,t] for y in COMMIT)) - @expression(EP, eTotalCStart, sum(eTotalCStartT[t] for t=1:T)) - - add_to_expression!(EP[:eObj], eTotalCStart) - - ### Constratints ### - ## Declaration of integer/binary variables - if setup["UCommit"] == 1 # Integer UC constraints - for y in COMMIT - set_integer.(vCOMMIT[y,:]) - set_integer.(vSTART[y,:]) - set_integer.(vSHUT[y,:]) - if y in inputs["RET_CAP"] - set_integer(EP[:vRETCAP][y]) - end - if y in inputs["NEW_CAP"] - set_integer(EP[:vCAP][y]) - end - if y in inputs["RETROFIT_CAP"] - set_integer(EP[:vRETROFITCAP][y]) - end - end - end #END unit commitment configuration - return EP + println("Unit Commitment Module") + + T = inputs["T"] # Number of time steps (hours) + COMMIT = inputs["COMMIT"] # For not, thermal resources are the only ones eligible for Unit Committment + + ### Variables ### + + ## Decision variables for unit commitment + # commitment state variable + @variable(EP, vCOMMIT[y in COMMIT, t = 1:T]>=0) + # startup event variable + @variable(EP, vSTART[y in COMMIT, t = 1:T]>=0) + # shutdown event variable + @variable(EP, vSHUT[y in COMMIT, t = 1:T]>=0) + + ### Expressions ### + + ## Objective Function Expressions ## + + # Startup costs of "generation" for resource "y" during hour "t" + @expression(EP, + eCStart[y in COMMIT, t = 1:T], + (inputs["omega"][t]*inputs["C_Start"][y, t]*vSTART[y, t])) + + # Julia is fastest when summing over one row one column at a time + @expression(EP, eTotalCStartT[t = 1:T], sum(eCStart[y, t] for y in COMMIT)) + @expression(EP, eTotalCStart, sum(eTotalCStartT[t] for t in 1:T)) + + add_to_expression!(EP[:eObj], eTotalCStart) + + ### Constratints ### + ## Declaration of integer/binary variables + if setup["UCommit"] == 1 # Integer UC constraints + for y in COMMIT + set_integer.(vCOMMIT[y, :]) + set_integer.(vSTART[y, :]) + set_integer.(vSHUT[y, :]) + if y in inputs["RET_CAP"] + set_integer(EP[:vRETCAP][y]) + end + if y in inputs["NEW_CAP"] + set_integer(EP[:vCAP][y]) + end + if y in inputs["RETROFIT_CAP"] + set_integer(EP[:vRETROFITCAP][y]) + end + end + end #END unit commitment configuration + return EP end diff --git a/src/model/expression_manipulation.jl b/src/model/expression_manipulation.jl index fe4fe6e7be..7e783eae55 100644 --- a/src/model/expression_manipulation.jl +++ b/src/model/expression_manipulation.jl @@ -25,7 +25,9 @@ This can lead to errors later if a method can only operate on expressions. We don't currently have a method to do this with non-contiguous indexing. """ -function create_empty_expression!(EP::Model, exprname::Symbol, dims::NTuple{N, Int64}) where N +function create_empty_expression!(EP::Model, + exprname::Symbol, + dims::NTuple{N, Int64}) where {N} temp = Array{AffExpr}(undef, dims) fill_with_zeros!(temp) EP[exprname] = temp @@ -49,7 +51,7 @@ end Fill an array of expressions with zeros in-place. """ -function fill_with_zeros!(arr::AbstractArray{GenericAffExpr{C,T}, dims}) where {C,T,dims} +function fill_with_zeros!(arr::AbstractArray{GenericAffExpr{C, T}, dims}) where {C, T, dims} for i::Int64 in eachindex(IndexLinear(), arr)::Base.OneTo{Int64} arr[i] = AffExpr(0.0) end @@ -64,7 +66,8 @@ Fill an array of expressions with the specified constant, in-place. In the future we could expand this to non AffExpr, using GenericAffExpr e.g. if we wanted to use Float32 instead of Float64 """ -function fill_with_const!(arr::AbstractArray{GenericAffExpr{C,T}, dims}, con::Real) where {C,T,dims} +function fill_with_const!(arr::AbstractArray{GenericAffExpr{C, T}, dims}, + con::Real) where {C, T, dims} for i in eachindex(arr) arr[i] = AffExpr(con) end @@ -77,7 +80,7 @@ end ###### ###### ###### ###### ###### ###### # function extract_time_series_to_expression(var::Matrix{VariableRef}, - set::AbstractVector{Int}) + set::AbstractVector{Int}) TIME_DIM = 2 time_range = 1:size(var)[TIME_DIM] @@ -87,8 +90,13 @@ function extract_time_series_to_expression(var::Matrix{VariableRef}, return expr end -function extract_time_series_to_expression(var::JuMP.Containers.DenseAxisArray{VariableRef, 2, Tuple{X, Base.OneTo{Int64}}, Y}, - set::AbstractVector{Int}) where {X, Y} +function extract_time_series_to_expression(var::JuMP.Containers.DenseAxisArray{ + VariableRef, + 2, + Tuple{X, Base.OneTo{Int64}}, + Y, + }, + set::AbstractVector{Int}) where {X, Y} TIME_DIM = 2 time_range = var.axes[TIME_DIM] @@ -104,7 +112,7 @@ end ###### ###### ###### ###### ###### ###### # Version for single element -function add_similar_to_expression!(expr1::GenericAffExpr{C,T}, expr2::V) where {C,T,V} +function add_similar_to_expression!(expr1::GenericAffExpr{C, T}, expr2::V) where {C, T, V} add_to_expression!(expr1, expr2) return nothing end @@ -116,7 +124,8 @@ Add an array of some type `V` to an array of expressions, in-place. This will work on JuMP DenseContainers which do not have linear indexing from 1:length(arr). However, the accessed parts of both arrays must have the same dimensions. """ -function add_similar_to_expression!(expr1::AbstractArray{GenericAffExpr{C,T}, dim1}, expr2::AbstractArray{V, dim2}) where {C,T,V,dim1,dim2} +function add_similar_to_expression!(expr1::AbstractArray{GenericAffExpr{C, T}, dim1}, + expr2::AbstractArray{V, dim2}) where {C, T, V, dim1, dim2} # This is defined for Arrays of different dimensions # despite the fact it will definitely throw an error # because the error will tell the user / developer @@ -134,7 +143,7 @@ end ###### ###### ###### ###### ###### ###### # Version for single element -function add_term_to_expression!(expr1::GenericAffExpr{C,T}, expr2::V) where {C,T,V} +function add_term_to_expression!(expr1::GenericAffExpr{C, T}, expr2::V) where {C, T, V} add_to_expression!(expr1, expr2) return nothing end @@ -145,7 +154,8 @@ end Add an entry of type `V` to an array of expressions, in-place. This will work on JuMP DenseContainers which do not have linear indexing from 1:length(arr). """ -function add_term_to_expression!(expr1::AbstractArray{GenericAffExpr{C,T}, dims}, expr2::V) where {C,T,V,dims} +function add_term_to_expression!(expr1::AbstractArray{GenericAffExpr{C, T}, dims}, + expr2::V) where {C, T, V, dims} for i in eachindex(expr1) add_to_expression!(expr1[i], expr2) end @@ -162,7 +172,8 @@ end Check that two arrays have the same dimensions. If not, return an error message which includes the dimensions of both arrays. """ -function check_sizes_match(expr1::AbstractArray{C, dim1}, expr2::AbstractArray{T, dim2}) where {C,T,dim1, dim2} +function check_sizes_match(expr1::AbstractArray{C, dim1}, + expr2::AbstractArray{T, dim2}) where {C, T, dim1, dim2} # After testing, this appears to be just as fast as a method for Array{GenericAffExpr{C,T}, dims} or Array{AffExpr, dims} if size(expr1) != size(expr2) error(" @@ -181,7 +192,7 @@ as the method only works on the constituent types making up the GenericAffExpr, Also, the default MethodError from add_to_expression! is sometime more informative than the error message here. """ function check_addable_to_expr(C::DataType, T::DataType) - if !(hasmethod(add_to_expression!, (C,T))) + if !(hasmethod(add_to_expression!, (C, T))) error("No method found for add_to_expression! with types $(C) and $(T)") end end @@ -196,11 +207,11 @@ end Sum an array of expressions into a single expression and return the result. We're using errors from add_to_expression!() to check that the types are compatible. """ -function sum_expression(expr::AbstractArray{C, dims}) :: AffExpr where {C,dims} +function sum_expression(expr::AbstractArray{C, dims})::AffExpr where {C, dims} # check_addable_to_expr(C,C) total = AffExpr(0.0) for i in eachindex(expr) add_to_expression!(total, expr[i]) end return total -end \ No newline at end of file +end diff --git a/src/model/generate_model.jl b/src/model/generate_model.jl index 677fc7b03d..ff16f66875 100644 --- a/src/model/generate_model.jl +++ b/src/model/generate_model.jl @@ -67,178 +67,181 @@ The power balance constraint of the model ensures that electricity demand is met # Returns - `Model`: The model object containing the entire optimization problem model to be solved by solve_model.jl """ -function generate_model(setup::Dict,inputs::Dict,OPTIMIZER::MOI.OptimizerWithAttributes) +function generate_model(setup::Dict, inputs::Dict, OPTIMIZER::MOI.OptimizerWithAttributes) + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones - T = inputs["T"] # Number of time steps (hours) - Z = inputs["Z"] # Number of zones + ## Start pre-solve timer + presolver_start_time = time() - ## Start pre-solve timer - presolver_start_time = time() + # Generate Energy Portfolio (EP) Model + EP = Model(OPTIMIZER) + set_string_names_on_creation(EP, Bool(setup["EnableJuMPStringNames"])) + # Introduce dummy variable fixed to zero to ensure that expressions like eTotalCap, + # eTotalCapCharge, eTotalCapEnergy and eAvail_Trans_Cap all have a JuMP variable + @variable(EP, vZERO==0) - # Generate Energy Portfolio (EP) Model - EP = Model(OPTIMIZER) - set_string_names_on_creation(EP, Bool(setup["EnableJuMPStringNames"])) - # Introduce dummy variable fixed to zero to ensure that expressions like eTotalCap, - # eTotalCapCharge, eTotalCapEnergy and eAvail_Trans_Cap all have a JuMP variable - @variable(EP, vZERO == 0); + # Initialize Power Balance Expression + # Expression for "baseline" power balance constraint + create_empty_expression!(EP, :ePowerBalance, (T, Z)) + + # Initialize Objective Function Expression + EP[:eObj] = AffExpr(0.0) + + create_empty_expression!(EP, :eGenerationByZone, (Z, T)) + + # Energy losses related to technologies + create_empty_expression!(EP, :eELOSSByZone, Z) - # Initialize Power Balance Expression - # Expression for "baseline" power balance constraint - create_empty_expression!(EP, :ePowerBalance, (T, Z)) - - # Initialize Objective Function Expression - EP[:eObj] = AffExpr(0.0) - - create_empty_expression!(EP, :eGenerationByZone, (Z, T)) - - # Energy losses related to technologies - create_empty_expression!(EP, :eELOSSByZone, Z) - - # Initialize Capacity Reserve Margin Expression - if setup["CapacityReserveMargin"] > 0 - create_empty_expression!(EP, :eCapResMarBalance, (inputs["NCapacityReserveMargin"], T)) - end - - # Energy Share Requirement - if setup["EnergyShareRequirement"] >= 1 - create_empty_expression!(EP, :eESR, inputs["nESR"]) - end - - if setup["MinCapReq"] == 1 - create_empty_expression!(EP, :eMinCapRes, inputs["NumberOfMinCapReqs"]) - end - - if setup["MaxCapReq"] == 1 - create_empty_expression!(EP, :eMaxCapRes, inputs["NumberOfMaxCapReqs"]) - end - - # Infrastructure - discharge!(EP, inputs, setup) - - non_served_energy!(EP, inputs, setup) - - investment_discharge!(EP, inputs, setup) - - if setup["UCommit"] > 0 - ucommit!(EP, inputs, setup) - end - - fuel!(EP, inputs, setup) - - co2!(EP, inputs) - - if setup["OperationalReserves"] > 0 - operational_reserves!(EP, inputs, setup) - end - - if Z > 1 - investment_transmission!(EP, inputs, setup) - transmission!(EP, inputs, setup) - end - - if Z > 1 && setup["DC_OPF"] != 0 - dcopf_transmission!(EP, inputs, setup) - end - - # Technologies - # Model constraints, variables, expression related to dispatchable renewable resources - - if !isempty(inputs["VRE"]) - curtailable_variable_renewable!(EP, inputs, setup) - end - - # Model constraints, variables, expression related to non-dispatchable renewable resources - if !isempty(inputs["MUST_RUN"]) - must_run!(EP, inputs, setup) - end - - # Model constraints, variables, expression related to energy storage modeling - if !isempty(inputs["STOR_ALL"]) - storage!(EP, inputs, setup) - end - - # Model constraints, variables, expression related to reservoir hydropower resources - if !isempty(inputs["HYDRO_RES"]) - hydro_res!(EP, inputs, setup) - end - - if !isempty(inputs["ELECTROLYZER"]) - electrolyzer!(EP, inputs, setup) - end - - # Model constraints, variables, expression related to reservoir hydropower resources with long duration storage - if inputs["REP_PERIOD"] > 1 && !isempty(inputs["STOR_HYDRO_LONG_DURATION"]) - hydro_inter_period_linkage!(EP, inputs) - end - - # Model constraints, variables, expression related to demand flexibility resources - if !isempty(inputs["FLEX"]) - flexible_demand!(EP, inputs, setup) - end - # Model constraints, variables, expression related to thermal resource technologies - if !isempty(inputs["THERM_ALL"]) - thermal!(EP, inputs, setup) - end - - # Model constraints, variables, expression related to retrofit technologies - if !isempty(inputs["RETROFIT_OPTIONS"]) - EP = retrofit(EP, inputs) - end - - # Model constraints, variables, expressions related to the co-located VRE-storage resources - if !isempty(inputs["VRE_STOR"]) - vre_stor!(EP, inputs, setup) - end - - # Policies - - if setup["OperationalReserves"] > 0 - operational_reserves_constraints!(EP, inputs) - end - - # CO2 emissions limits - if setup["CO2Cap"] > 0 - co2_cap!(EP, inputs, setup) - end - - # Endogenous Retirements - if setup["MultiStage"] > 0 - endogenous_retirement!(EP, inputs, setup) - end - - # Energy Share Requirement - if setup["EnergyShareRequirement"] >= 1 - energy_share_requirement!(EP, inputs, setup) - end - - #Capacity Reserve Margin - if setup["CapacityReserveMargin"] > 0 - cap_reserve_margin!(EP, inputs, setup) - end - - if (setup["MinCapReq"] == 1) - minimum_capacity_requirement!(EP, inputs, setup) - end - - if setup["MaxCapReq"] == 1 - maximum_capacity_requirement!(EP, inputs, setup) - end - - ## Define the objective function - @objective(EP,Min, setup["ObjScale"] * EP[:eObj]) - - ## Power balance constraints - # demand = generation + storage discharge - storage charge - demand deferral + deferred demand satisfaction - demand curtailment (NSE) - # + incoming power flows - outgoing power flows - flow losses - charge of heat storage + generation from NACC - @constraint(EP, cPowerBalance[t=1:T, z=1:Z], EP[:ePowerBalance][t,z] == inputs["pD"][t,z]) - - ## Record pre-solver time - presolver_time = time() - presolver_start_time - if setup["PrintModel"] == 1 - filepath = joinpath(pwd(), "YourModel.lp") - JuMP.write_to_file(EP, filepath) - println("Model Printed") - end + # Initialize Capacity Reserve Margin Expression + if setup["CapacityReserveMargin"] > 0 + create_empty_expression!(EP, + :eCapResMarBalance, + (inputs["NCapacityReserveMargin"], T)) + end + + # Energy Share Requirement + if setup["EnergyShareRequirement"] >= 1 + create_empty_expression!(EP, :eESR, inputs["nESR"]) + end + + if setup["MinCapReq"] == 1 + create_empty_expression!(EP, :eMinCapRes, inputs["NumberOfMinCapReqs"]) + end + + if setup["MaxCapReq"] == 1 + create_empty_expression!(EP, :eMaxCapRes, inputs["NumberOfMaxCapReqs"]) + end + + # Infrastructure + discharge!(EP, inputs, setup) + + non_served_energy!(EP, inputs, setup) + + investment_discharge!(EP, inputs, setup) + + if setup["UCommit"] > 0 + ucommit!(EP, inputs, setup) + end + + fuel!(EP, inputs, setup) + + co2!(EP, inputs) + + if setup["OperationalReserves"] > 0 + operational_reserves!(EP, inputs, setup) + end + + if Z > 1 + investment_transmission!(EP, inputs, setup) + transmission!(EP, inputs, setup) + end + + if Z > 1 && setup["DC_OPF"] != 0 + dcopf_transmission!(EP, inputs, setup) + end + + # Technologies + # Model constraints, variables, expression related to dispatchable renewable resources + + if !isempty(inputs["VRE"]) + curtailable_variable_renewable!(EP, inputs, setup) + end + + # Model constraints, variables, expression related to non-dispatchable renewable resources + if !isempty(inputs["MUST_RUN"]) + must_run!(EP, inputs, setup) + end + + # Model constraints, variables, expression related to energy storage modeling + if !isempty(inputs["STOR_ALL"]) + storage!(EP, inputs, setup) + end + + # Model constraints, variables, expression related to reservoir hydropower resources + if !isempty(inputs["HYDRO_RES"]) + hydro_res!(EP, inputs, setup) + end + + if !isempty(inputs["ELECTROLYZER"]) + electrolyzer!(EP, inputs, setup) + end + + # Model constraints, variables, expression related to reservoir hydropower resources with long duration storage + if inputs["REP_PERIOD"] > 1 && !isempty(inputs["STOR_HYDRO_LONG_DURATION"]) + hydro_inter_period_linkage!(EP, inputs) + end + + # Model constraints, variables, expression related to demand flexibility resources + if !isempty(inputs["FLEX"]) + flexible_demand!(EP, inputs, setup) + end + # Model constraints, variables, expression related to thermal resource technologies + if !isempty(inputs["THERM_ALL"]) + thermal!(EP, inputs, setup) + end + + # Model constraints, variables, expression related to retrofit technologies + if !isempty(inputs["RETROFIT_OPTIONS"]) + EP = retrofit(EP, inputs) + end + + # Model constraints, variables, expressions related to the co-located VRE-storage resources + if !isempty(inputs["VRE_STOR"]) + vre_stor!(EP, inputs, setup) + end + + # Policies + + if setup["OperationalReserves"] > 0 + operational_reserves_constraints!(EP, inputs) + end + + # CO2 emissions limits + if setup["CO2Cap"] > 0 + co2_cap!(EP, inputs, setup) + end + + # Endogenous Retirements + if setup["MultiStage"] > 0 + endogenous_retirement!(EP, inputs, setup) + end + + # Energy Share Requirement + if setup["EnergyShareRequirement"] >= 1 + energy_share_requirement!(EP, inputs, setup) + end + + #Capacity Reserve Margin + if setup["CapacityReserveMargin"] > 0 + cap_reserve_margin!(EP, inputs, setup) + end + + if (setup["MinCapReq"] == 1) + minimum_capacity_requirement!(EP, inputs, setup) + end + + if setup["MaxCapReq"] == 1 + maximum_capacity_requirement!(EP, inputs, setup) + end + + ## Define the objective function + @objective(EP, Min, setup["ObjScale"]*EP[:eObj]) + + ## Power balance constraints + # demand = generation + storage discharge - storage charge - demand deferral + deferred demand satisfaction - demand curtailment (NSE) + # + incoming power flows - outgoing power flows - flow losses - charge of heat storage + generation from NACC + @constraint(EP, + cPowerBalance[t = 1:T, z = 1:Z], + EP[:ePowerBalance][t, z]==inputs["pD"][t, z]) + + ## Record pre-solver time + presolver_time = time() - presolver_start_time + if setup["PrintModel"] == 1 + filepath = joinpath(pwd(), "YourModel.lp") + JuMP.write_to_file(EP, filepath) + println("Model Printed") + end return EP end diff --git a/src/model/policies/cap_reserve_margin.jl b/src/model/policies/cap_reserve_margin.jl index 5a6aa1ba1d..74052fabd4 100755 --- a/src/model/policies/cap_reserve_margin.jl +++ b/src/model/policies/cap_reserve_margin.jl @@ -57,23 +57,30 @@ The expressions establishing the capacity reserve margin contributions of each t class are included in their respective technology modules. """ function cap_reserve_margin!(EP::Model, inputs::Dict, setup::Dict) - # capacity reserve margin constraint - T = inputs["T"] - NCRM = inputs["NCapacityReserveMargin"] - println("Capacity Reserve Margin Policies Module") + # capacity reserve margin constraint + T = inputs["T"] + NCRM = inputs["NCapacityReserveMargin"] + println("Capacity Reserve Margin Policies Module") - # if input files are present, add capacity reserve margin slack variables - if haskey(inputs, "dfCapRes_slack") - @variable(EP,vCapResSlack[res=1:NCRM, t=1:T]>=0) - add_similar_to_expression!(EP[:eCapResMarBalance], vCapResSlack) + # if input files are present, add capacity reserve margin slack variables + if haskey(inputs, "dfCapRes_slack") + @variable(EP, vCapResSlack[res = 1:NCRM, t = 1:T]>=0) + add_similar_to_expression!(EP[:eCapResMarBalance], vCapResSlack) - @expression(EP, eCapResSlack_Year[res=1:NCRM], sum(EP[:vCapResSlack][res,t] * inputs["omega"][t] for t in 1:T)) - @expression(EP, eCCapResSlack[res=1:NCRM], inputs["dfCapRes_slack"][res,:PriceCap] * EP[:eCapResSlack_Year][res]) - @expression(EP, eCTotalCapResSlack, sum(EP[:eCCapResSlack][res] for res = 1:NCRM)) - add_to_expression!(EP[:eObj], eCTotalCapResSlack) - end + @expression(EP, + eCapResSlack_Year[res = 1:NCRM], + sum(EP[:vCapResSlack][res, t] * inputs["omega"][t] for t in 1:T)) + @expression(EP, + eCCapResSlack[res = 1:NCRM], + inputs["dfCapRes_slack"][res, :PriceCap]*EP[:eCapResSlack_Year][res]) + @expression(EP, eCTotalCapResSlack, sum(EP[:eCCapResSlack][res] for res in 1:NCRM)) + add_to_expression!(EP[:eObj], eCTotalCapResSlack) + end - @constraint(EP, cCapacityResMargin[res=1:NCRM, t=1:T], EP[:eCapResMarBalance][res, t] - >= sum(inputs["pD"][t,z] * (1 + inputs["dfCapRes"][z,res]) - for z=findall(x->x!=0,inputs["dfCapRes"][:,res]))) + @constraint(EP, + cCapacityResMargin[res = 1:NCRM, t = 1:T], + EP[:eCapResMarBalance][res, + t] + >=sum(inputs["pD"][t, z] * (1 + inputs["dfCapRes"][z, res]) + for z in findall(x -> x != 0, inputs["dfCapRes"][:, res]))) end diff --git a/src/model/policies/co2_cap.jl b/src/model/policies/co2_cap.jl index 252cb3a7f3..d14b69a265 100644 --- a/src/model/policies/co2_cap.jl +++ b/src/model/policies/co2_cap.jl @@ -66,54 +66,59 @@ Similarly, a generation based emission constraint is defined by setting the emis Note that the generator-side rate-based constraint can be used to represent a fee-rebate (``feebate'') system: the dirty generators that emit above the bar ($\epsilon_{z,p,gen}^{maxCO_2}$) have to buy emission allowances from the emission regulator in the region $z$ where they are located; in the same vein, the clean generators get rebates from the emission regulator at an emission allowance price being the dual variable of the emissions rate constraint. """ function co2_cap!(EP::Model, inputs::Dict, setup::Dict) - - println("CO2 Policies Module") - - SEG = inputs["SEG"] # Number of lines - T = inputs["T"] # Number of time steps (hours) - - ### Variable ### - # if input files are present, add CO2 cap slack variables - if haskey(inputs, "dfCO2Cap_slack") - @variable(EP, vCO2Cap_slack[cap = 1:inputs["NCO2Cap"]]>=0) - - @expression(EP, eCCO2Cap_slack[cap = 1:inputs["NCO2Cap"]], - inputs["dfCO2Cap_slack"][cap,:PriceCap] * EP[:vCO2Cap_slack][cap]) - @expression(EP, eCTotalCO2CapSlack, - sum(EP[:eCCO2Cap_slack][cap] for cap = 1:inputs["NCO2Cap"])) - - add_to_expression!(EP[:eObj], eCTotalCO2CapSlack) - else - @variable(EP, vCO2Cap_slack[cap = 1:inputs["NCO2Cap"]]==0) - end - - ### Constraints ### - - ## Mass-based: Emissions constraint in absolute emissions limit (tons) - if setup["CO2Cap"] == 1 - @constraint(EP, cCO2Emissions_systemwide[cap=1:inputs["NCO2Cap"]], - sum(inputs["omega"][t] * EP[:eEmissionsByZone][z,t] for z=findall(x->x==1, inputs["dfCO2CapZones"][:,cap]), t=1:T) - - vCO2Cap_slack[cap] <= - sum(inputs["dfMaxCO2"][z,cap] for z=findall(x->x==1, inputs["dfCO2CapZones"][:,cap])) - ) - - ## (fulfilled) demand + Rate-based: Emissions constraint in terms of rate (tons/MWh) - elseif setup["CO2Cap"] == 2 ##This part moved to non_served_energy.jl - - @constraint(EP, cCO2Emissions_systemwide[cap=1:inputs["NCO2Cap"]], - sum(inputs["omega"][t] * EP[:eEmissionsByZone][z,t] for z=findall(x->x==1, inputs["dfCO2CapZones"][:,cap]), t=1:T) - - vCO2Cap_slack[cap] <= - sum(inputs["dfMaxCO2Rate"][z,cap] * sum(inputs["omega"][t] * (inputs["pD"][t,z] - sum(EP[:vNSE][s,t,z] for s in 1:SEG)) for t=1:T) for z = findall(x->x==1, inputs["dfCO2CapZones"][:,cap])) + - sum(inputs["dfMaxCO2Rate"][z,cap] * setup["StorageLosses"] * EP[:eELOSSByZone][z] for z=findall(x->x==1, inputs["dfCO2CapZones"][:,cap])) - ) - - ## Generation + Rate-based: Emissions constraint in terms of rate (tons/MWh) - elseif (setup["CO2Cap"]==3) - @constraint(EP, cCO2Emissions_systemwide[cap=1:inputs["NCO2Cap"]], - sum(inputs["omega"][t] * EP[:eEmissionsByZone][z,t] for z=findall(x->x==1, inputs["dfCO2CapZones"][:,cap]), t=1:T) - - vCO2Cap_slack[cap] <= - sum(inputs["dfMaxCO2Rate"][z,cap] * inputs["omega"][t] * EP[:eGenerationByZone][z,t] for t=1:T, z=findall(x->x==1, inputs["dfCO2CapZones"][:,cap])) - ) - end - + println("CO2 Policies Module") + + SEG = inputs["SEG"] # Number of lines + T = inputs["T"] # Number of time steps (hours) + + ### Variable ### + # if input files are present, add CO2 cap slack variables + if haskey(inputs, "dfCO2Cap_slack") + @variable(EP, vCO2Cap_slack[cap = 1:inputs["NCO2Cap"]]>=0) + + @expression(EP, eCCO2Cap_slack[cap = 1:inputs["NCO2Cap"]], + inputs["dfCO2Cap_slack"][cap, :PriceCap]*EP[:vCO2Cap_slack][cap]) + @expression(EP, eCTotalCO2CapSlack, + sum(EP[:eCCO2Cap_slack][cap] for cap in 1:inputs["NCO2Cap"])) + + add_to_expression!(EP[:eObj], eCTotalCO2CapSlack) + else + @variable(EP, vCO2Cap_slack[cap = 1:inputs["NCO2Cap"]]==0) + end + + ### Constraints ### + + ## Mass-based: Emissions constraint in absolute emissions limit (tons) + if setup["CO2Cap"] == 1 + @constraint(EP, cCO2Emissions_systemwide[cap = 1:inputs["NCO2Cap"]], + sum(inputs["omega"][t] * EP[:eEmissionsByZone][z, t] + for z in findall(x -> x == 1, inputs["dfCO2CapZones"][:, cap]), t in 1:T) - + vCO2Cap_slack[cap]<= + sum(inputs["dfMaxCO2"][z, cap] + for z in findall(x -> x == 1, inputs["dfCO2CapZones"][:, cap]))) + + ## (fulfilled) demand + Rate-based: Emissions constraint in terms of rate (tons/MWh) + elseif setup["CO2Cap"] == 2 ##This part moved to non_served_energy.jl + @constraint(EP, cCO2Emissions_systemwide[cap = 1:inputs["NCO2Cap"]], + sum(inputs["omega"][t] * EP[:eEmissionsByZone][z, t] + for z in findall(x -> x == 1, inputs["dfCO2CapZones"][:, cap]), t in 1:T) - + vCO2Cap_slack[cap]<= + sum(inputs["dfMaxCO2Rate"][z, cap] * sum(inputs["omega"][t] * + (inputs["pD"][t, z] - sum(EP[:vNSE][s, t, z] for s in 1:SEG)) + for t in 1:T) + for z in findall(x -> x == 1, inputs["dfCO2CapZones"][:, cap])) + + sum(inputs["dfMaxCO2Rate"][z, cap] * setup["StorageLosses"] * + EP[:eELOSSByZone][z] + for z in findall(x -> x == 1, inputs["dfCO2CapZones"][:, cap]))) + + ## Generation + Rate-based: Emissions constraint in terms of rate (tons/MWh) + elseif (setup["CO2Cap"] == 3) + @constraint(EP, cCO2Emissions_systemwide[cap = 1:inputs["NCO2Cap"]], + sum(inputs["omega"][t] * EP[:eEmissionsByZone][z, t] + for z in findall(x -> x == 1, inputs["dfCO2CapZones"][:, cap]), t in 1:T) - + vCO2Cap_slack[cap]<= + sum(inputs["dfMaxCO2Rate"][z, cap] * inputs["omega"][t] * + EP[:eGenerationByZone][z, t] + for t in 1:T, z in findall(x -> x == 1, inputs["dfCO2CapZones"][:, cap]))) + end end diff --git a/src/model/policies/energy_share_requirement.jl b/src/model/policies/energy_share_requirement.jl index 2c65aa61ee..a4e3225c08 100644 --- a/src/model/policies/energy_share_requirement.jl +++ b/src/model/policies/energy_share_requirement.jl @@ -24,22 +24,23 @@ In practice, most existing renewable portfolio standard policies do not account However, with 100% RPS or CES policies enacted in several jurisdictions, policy makers may wish to include storage losses in the minimum energy share, as otherwise there will be a difference between total generation and total demand that will permit continued use of non-qualifying resources (e.g. emitting generators). """ function energy_share_requirement!(EP::Model, inputs::Dict, setup::Dict) + println("Energy Share Requirement Policies Module") - println("Energy Share Requirement Policies Module") - - # if input files are present, add energy share requirement slack variables - if haskey(inputs, "dfESR_slack") - @variable(EP, vESR_slack[ESR=1:inputs["nESR"]]>=0) - add_similar_to_expression!(EP[:eESR], vESR_slack) + # if input files are present, add energy share requirement slack variables + if haskey(inputs, "dfESR_slack") + @variable(EP, vESR_slack[ESR = 1:inputs["nESR"]]>=0) + add_similar_to_expression!(EP[:eESR], vESR_slack) - @expression(EP, eCESRSlack[ESR=1:inputs["nESR"]], inputs["dfESR_slack"][ESR,:PriceCap] * EP[:vESR_slack][ESR]) - @expression(EP, eCTotalESRSlack, sum(EP[:eCESRSlack][ESR] for ESR = 1:inputs["nESR"])) - - add_to_expression!(EP[:eObj], eCTotalESRSlack) - end - - ## Energy Share Requirements (minimum energy share from qualifying renewable resources) constraint - @constraint(EP, cESRShare[ESR=1:inputs["nESR"]], EP[:eESR][ESR] >= 0) + @expression(EP, + eCESRSlack[ESR = 1:inputs["nESR"]], + inputs["dfESR_slack"][ESR, :PriceCap]*EP[:vESR_slack][ESR]) + @expression(EP, + eCTotalESRSlack, + sum(EP[:eCESRSlack][ESR] for ESR in 1:inputs["nESR"])) + add_to_expression!(EP[:eObj], eCTotalESRSlack) + end + ## Energy Share Requirements (minimum energy share from qualifying renewable resources) constraint + @constraint(EP, cESRShare[ESR = 1:inputs["nESR"]], EP[:eESR][ESR]>=0) end diff --git a/src/model/policies/maximum_capacity_requirement.jl b/src/model/policies/maximum_capacity_requirement.jl index c36d994b99..4f92aa4017 100644 --- a/src/model/policies/maximum_capacity_requirement.jl +++ b/src/model/policies/maximum_capacity_requirement.jl @@ -9,21 +9,25 @@ The maximum capacity requirement constraint allows for modeling maximum deployme Note that $\epsilon_{y,z,p}^{MaxCapReq}$ is the eligiblity of a generator of technology $y$ in zone $z$ of requirement $p$ and will be equal to $1$ for eligible generators and will be zero for ineligible resources. The dual value of each maximum capacity constraint can be interpreted as the required payment (e.g. subsidy) per MW per year required to ensure adequate revenue for the qualifying resources. """ function maximum_capacity_requirement!(EP::Model, inputs::Dict, setup::Dict) + println("Maximum Capacity Requirement Module") + NumberOfMaxCapReqs = inputs["NumberOfMaxCapReqs"] - println("Maximum Capacity Requirement Module") - NumberOfMaxCapReqs = inputs["NumberOfMaxCapReqs"] + # if input files are present, add maximum capacity requirement slack variables + if haskey(inputs, "MaxCapPriceCap") + @variable(EP, vMaxCap_slack[maxcap = 1:NumberOfMaxCapReqs]>=0) + add_similar_to_expression!(EP[:eMaxCapRes], -vMaxCap_slack) - # if input files are present, add maximum capacity requirement slack variables - if haskey(inputs, "MaxCapPriceCap") - @variable(EP, vMaxCap_slack[maxcap = 1:NumberOfMaxCapReqs]>=0) - add_similar_to_expression!(EP[:eMaxCapRes], -vMaxCap_slack) + @expression(EP, + eCMaxCap_slack[maxcap = 1:NumberOfMaxCapReqs], + inputs["MaxCapPriceCap"][maxcap]*EP[:vMaxCap_slack][maxcap]) + @expression(EP, + eTotalCMaxCapSlack, + sum(EP[:eCMaxCap_slack][maxcap] for maxcap in 1:NumberOfMaxCapReqs)) - @expression(EP, eCMaxCap_slack[maxcap = 1:NumberOfMaxCapReqs], inputs["MaxCapPriceCap"][maxcap] * EP[:vMaxCap_slack][maxcap]) - @expression(EP, eTotalCMaxCapSlack, sum(EP[:eCMaxCap_slack][maxcap] for maxcap = 1:NumberOfMaxCapReqs)) - - add_to_expression!(EP[:eObj], eTotalCMaxCapSlack) - end - - @constraint(EP, cZoneMaxCapReq[maxcap = 1:NumberOfMaxCapReqs], EP[:eMaxCapRes][maxcap] <= inputs["MaxCapReq"][maxcap]) + add_to_expression!(EP[:eObj], eTotalCMaxCapSlack) + end + @constraint(EP, + cZoneMaxCapReq[maxcap = 1:NumberOfMaxCapReqs], + EP[:eMaxCapRes][maxcap]<=inputs["MaxCapReq"][maxcap]) end diff --git a/src/model/policies/minimum_capacity_requirement.jl b/src/model/policies/minimum_capacity_requirement.jl index c07b10821e..333c6b551d 100644 --- a/src/model/policies/minimum_capacity_requirement.jl +++ b/src/model/policies/minimum_capacity_requirement.jl @@ -15,22 +15,25 @@ Also note that co-located VRE and storage resources, there are three different c requirements. """ function minimum_capacity_requirement!(EP::Model, inputs::Dict, setup::Dict) + println("Minimum Capacity Requirement Module") + NumberOfMinCapReqs = inputs["NumberOfMinCapReqs"] - println("Minimum Capacity Requirement Module") - NumberOfMinCapReqs = inputs["NumberOfMinCapReqs"] + # if input files are present, add minimum capacity requirement slack variables + if haskey(inputs, "MinCapPriceCap") + @variable(EP, vMinCap_slack[mincap = 1:NumberOfMinCapReqs]>=0) + add_similar_to_expression!(EP[:eMinCapRes], vMinCap_slack) - # if input files are present, add minimum capacity requirement slack variables - if haskey(inputs, "MinCapPriceCap") - @variable(EP, vMinCap_slack[mincap = 1:NumberOfMinCapReqs]>=0) - add_similar_to_expression!(EP[:eMinCapRes], vMinCap_slack) - - @expression(EP, eCMinCap_slack[mincap = 1:NumberOfMinCapReqs], inputs["MinCapPriceCap"][mincap] * EP[:vMinCap_slack][mincap]) - @expression(EP, eTotalCMinCapSlack, sum(EP[:eCMinCap_slack][mincap] for mincap = 1:NumberOfMinCapReqs)) - - add_to_expression!(EP[:eObj], eTotalCMinCapSlack) - end - - @constraint(EP, cZoneMinCapReq[mincap = 1:NumberOfMinCapReqs], EP[:eMinCapRes][mincap] >= inputs["MinCapReq"][mincap]) + @expression(EP, + eCMinCap_slack[mincap = 1:NumberOfMinCapReqs], + inputs["MinCapPriceCap"][mincap]*EP[:vMinCap_slack][mincap]) + @expression(EP, + eTotalCMinCapSlack, + sum(EP[:eCMinCap_slack][mincap] for mincap in 1:NumberOfMinCapReqs)) + add_to_expression!(EP[:eObj], eTotalCMinCapSlack) + end + @constraint(EP, + cZoneMinCapReq[mincap = 1:NumberOfMinCapReqs], + EP[:eMinCapRes][mincap]>=inputs["MinCapReq"][mincap]) end diff --git a/src/model/resources/curtailable_variable_renewable/curtailable_variable_renewable.jl b/src/model/resources/curtailable_variable_renewable/curtailable_variable_renewable.jl index e86179f132..9b790789d6 100644 --- a/src/model/resources/curtailable_variable_renewable/curtailable_variable_renewable.jl +++ b/src/model/resources/curtailable_variable_renewable/curtailable_variable_renewable.jl @@ -14,41 +14,43 @@ The above constraint is defined as an inequality instead of an equality to allow Note that if ```OperationalReserves=1``` indicating that frequency regulation and operating reserves are modeled, then this function calls ```curtailable_variable_renewable_operational_reserves!()```, which replaces the above constraints with a formulation inclusive of reserve provision. """ function curtailable_variable_renewable!(EP::Model, inputs::Dict, setup::Dict) - ## Controllable variable renewable generators - ### Option of modeling VRE generators with multiple availability profiles and capacity limits - Num_VRE_Bins in Vre.csv >1 - ## Default value of Num_VRE_Bins ==1 - println("Dispatchable Resources Module") + ## Controllable variable renewable generators + ### Option of modeling VRE generators with multiple availability profiles and capacity limits - Num_VRE_Bins in Vre.csv >1 + ## Default value of Num_VRE_Bins ==1 + println("Dispatchable Resources Module") - gen = inputs["RESOURCES"] + gen = inputs["RESOURCES"] - OperationalReserves = setup["OperationalReserves"] - CapacityReserveMargin = setup["CapacityReserveMargin"] + OperationalReserves = setup["OperationalReserves"] + CapacityReserveMargin = setup["CapacityReserveMargin"] - T = inputs["T"] # Number of time steps (hours) - Z = inputs["Z"] # Number of zones - G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones + G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) - VRE = inputs["VRE"] + VRE = inputs["VRE"] - VRE_POWER_OUT = intersect(VRE, ids_with_positive(gen, num_vre_bins)) - VRE_NO_POWER_OUT = setdiff(VRE, VRE_POWER_OUT) + VRE_POWER_OUT = intersect(VRE, ids_with_positive(gen, num_vre_bins)) + VRE_NO_POWER_OUT = setdiff(VRE, VRE_POWER_OUT) - ### Expressions ### + ### Expressions ### - ## Power Balance Expressions ## + ## Power Balance Expressions ## - @expression(EP, ePowerBalanceDisp[t=1:T, z=1:Z], - sum(EP[:vP][y,t] for y in intersect(VRE, resources_in_zone_by_rid(gen,z))) - ) - add_similar_to_expression!(EP[:ePowerBalance], EP[:ePowerBalanceDisp]) + @expression(EP, ePowerBalanceDisp[t = 1:T, z = 1:Z], + sum(EP[:vP][y, t] for y in intersect(VRE, resources_in_zone_by_rid(gen, z)))) + add_similar_to_expression!(EP[:ePowerBalance], EP[:ePowerBalanceDisp]) - # Capacity Reserves Margin policy - if CapacityReserveMargin > 0 - @expression(EP, eCapResMarBalanceVRE[res=1:inputs["NCapacityReserveMargin"], t=1:T], sum(derating_factor(gen[y], tag=res) * EP[:eTotalCap][y] * inputs["pP_Max"][y,t] for y in VRE)) - add_similar_to_expression!(EP[:eCapResMarBalance], eCapResMarBalanceVRE) - end + # Capacity Reserves Margin policy + if CapacityReserveMargin > 0 + @expression(EP, + eCapResMarBalanceVRE[res = 1:inputs["NCapacityReserveMargin"], t = 1:T], + sum(derating_factor(gen[y], tag = res) * EP[:eTotalCap][y] * + inputs["pP_Max"][y, t] for y in VRE)) + add_similar_to_expression!(EP[:eCapResMarBalance], eCapResMarBalanceVRE) + end - ### Constraints ### + ### Constraints ### if OperationalReserves == 1 # Constraints on power output and contribution to regulation and reserves curtailable_variable_renewable_operational_reserves!(EP, inputs) @@ -58,25 +60,28 @@ function curtailable_variable_renewable!(EP::Model, inputs::Dict, setup::Dict) for y in VRE_POWER_OUT # Define the set of generator indices corresponding to the different sites (or bins) of a particular VRE technology (E.g. wind or solar) in a particular zone. # For example the wind resource in a particular region could be include three types of bins corresponding to different sites with unique interconnection, hourly capacity factor and maximim available capacity limits. - VRE_BINS = intersect(resource_id.(gen[resource_id.(gen) .>= y]), resource_id.(gen[resource_id.(gen) .<= y+num_vre_bins(gen[y])-1])) + VRE_BINS = intersect(resource_id.(gen[resource_id.(gen) .>= y]), + resource_id.(gen[resource_id.(gen) .<= y + num_vre_bins(gen[y]) - 1])) # Maximum power generated per hour by renewable generators must be less than # sum of product of hourly capacity factor for each bin times its the bin installed capacity # Note: inequality constraint allows curtailment of output below maximum level. - @constraint(EP, [t=1:T], EP[:vP][y,t] <= sum(inputs["pP_Max"][yy,t]*EP[:eTotalCap][yy] for yy in VRE_BINS)) + @constraint(EP, + [t = 1:T], + EP[:vP][y, + t]<=sum(inputs["pP_Max"][yy, t] * EP[:eTotalCap][yy] for yy in VRE_BINS)) end end - # Set power variables for all bins that are not being modeled for hourly output to be zero - for y in VRE_NO_POWER_OUT - fix.(EP[:vP][y,:], 0.0, force=true) - end - ##CO2 Polcy Module VRE Generation by zone - @expression(EP, eGenerationByVRE[z=1:Z, t=1:T], # the unit is GW - sum(EP[:vP][y,t] for y in intersect(inputs["VRE"], resources_in_zone_by_rid(gen,z))) - ) - add_similar_to_expression!(EP[:eGenerationByZone], eGenerationByVRE) - + # Set power variables for all bins that are not being modeled for hourly output to be zero + for y in VRE_NO_POWER_OUT + fix.(EP[:vP][y, :], 0.0, force = true) + end + ##CO2 Polcy Module VRE Generation by zone + @expression(EP, eGenerationByVRE[z = 1:Z, t = 1:T], # the unit is GW + sum(EP[:vP][y, t] + for y in intersect(inputs["VRE"], resources_in_zone_by_rid(gen, z)))) + add_similar_to_expression!(EP[:eGenerationByZone], eGenerationByVRE) end @doc raw""" @@ -103,11 +108,11 @@ The amount of frequency regulation and operating reserves procured in each time ``` """ function curtailable_variable_renewable_operational_reserves!(EP::Model, inputs::Dict) - gen = inputs["RESOURCES"] - T = inputs["T"] + gen = inputs["RESOURCES"] + T = inputs["T"] VRE = inputs["VRE"] - VRE_POWER_OUT = intersect(VRE, ids_with_positive(gen, num_vre_bins)) + VRE_POWER_OUT = intersect(VRE, ids_with_positive(gen, num_vre_bins)) REG = intersect(VRE_POWER_OUT, inputs["REG"]) RSV = intersect(VRE_POWER_OUT, inputs["RSV"]) @@ -121,33 +126,37 @@ function curtailable_variable_renewable_operational_reserves!(EP::Model, inputs: resources_in_bin(y) = UnitRange(y, y + num_vre_bins(gen[y]) - 1) hourly_bin_capacity(y, t) = sum(hourly_capacity(yy, t) for yy in resources_in_bin(y)) - @constraint(EP, [y in REG, t in 1:T], vREG[y, t] <= reg_max(gen[y]) * hourly_bin_capacity(y, t)) - @constraint(EP, [y in RSV, t in 1:T], vRSV[y, t] <= rsv_max(gen[y]) * hourly_bin_capacity(y, t)) + @constraint(EP, + [y in REG, t in 1:T], + vREG[y, t]<=reg_max(gen[y]) * hourly_bin_capacity(y, t)) + @constraint(EP, + [y in RSV, t in 1:T], + vRSV[y, t]<=rsv_max(gen[y]) * hourly_bin_capacity(y, t)) expr = extract_time_series_to_expression(vP, VRE_POWER_OUT) add_similar_to_expression!(expr[REG, :], -vREG[REG, :]) - @constraint(EP, [y in VRE_POWER_OUT, t in 1:T], expr[y, t] >= 0) + @constraint(EP, [y in VRE_POWER_OUT, t in 1:T], expr[y, t]>=0) expr = extract_time_series_to_expression(vP, VRE_POWER_OUT) add_similar_to_expression!(expr[REG, :], +vREG[REG, :]) add_similar_to_expression!(expr[RSV, :], +vRSV[RSV, :]) - @constraint(EP, [y in VRE_POWER_OUT, t in 1:T], expr[y, t] <= hourly_bin_capacity(y, t)) + @constraint(EP, [y in VRE_POWER_OUT, t in 1:T], expr[y, t]<=hourly_bin_capacity(y, t)) end function remove_operational_reserves_for_binned_vre_resources!(EP::Model, inputs::Dict) gen = inputs["RESOURCES"] VRE = inputs["VRE"] - VRE_POWER_OUT = intersect(VRE, ids_with_positive(gen, num_vre_bins)) + VRE_POWER_OUT = intersect(VRE, ids_with_positive(gen, num_vre_bins)) REG = inputs["REG"] RSV = inputs["RSV"] VRE_NO_POWER_OUT = setdiff(VRE, VRE_POWER_OUT) for y in intersect(VRE_NO_POWER_OUT, REG) - fix.(EP[:vREG][y,:], 0.0, force=true) - end + fix.(EP[:vREG][y, :], 0.0, force = true) + end for y in intersect(VRE_NO_POWER_OUT, RSV) - fix.(EP[:vRSV][y,:], 0.0, force=true) - end + fix.(EP[:vRSV][y, :], 0.0, force = true) + end end diff --git a/src/model/resources/flexible_demand/flexible_demand.jl b/src/model/resources/flexible_demand/flexible_demand.jl index 7562d4ac43..18bdbbe681 100644 --- a/src/model/resources/flexible_demand/flexible_demand.jl +++ b/src/model/resources/flexible_demand/flexible_demand.jl @@ -36,89 +36,101 @@ A similar constraints maximum time steps of demand advancement. This is done by If $t$ is first time step of the year (or the first time step of the representative period), then the above two constraints are implemented to look back over the last n time steps, starting with the last time step of the year (or the last time step of the representative period). This time-wrapping implementation is similar to the time-wrapping implementations used for defining the storage balance constraints for hydropower reservoir resources and energy storage resources. """ function flexible_demand!(EP::Model, inputs::Dict, setup::Dict) -## Flexible demand resources available during all hours and can be either delayed or advanced (virtual storage-shiftable demand) - DR ==1 + ## Flexible demand resources available during all hours and can be either delayed or advanced (virtual storage-shiftable demand) - DR ==1 -println("Flexible Demand Resources Module") + println("Flexible Demand Resources Module") -T = inputs["T"] # Number of time steps (hours) -Z = inputs["Z"] # Number of zones -FLEX = inputs["FLEX"] # Set of flexible demand resources + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones + FLEX = inputs["FLEX"] # Set of flexible demand resources -gen = inputs["RESOURCES"] + gen = inputs["RESOURCES"] -hours_per_subperiod = inputs["hours_per_subperiod"] # Total number of hours per subperiod + hours_per_subperiod = inputs["hours_per_subperiod"] # Total number of hours per subperiod -### Variables ### + ### Variables ### -# Variable tracking total advanced (negative) or deferred (positive) demand for demand flex resource y in period t -@variable(EP, vS_FLEX[y in FLEX, t=1:T]); + # Variable tracking total advanced (negative) or deferred (positive) demand for demand flex resource y in period t + @variable(EP, vS_FLEX[y in FLEX, t = 1:T]) -# Variable tracking demand deferred by demand flex resource y in period t -@variable(EP, vCHARGE_FLEX[y in FLEX, t=1:T] >= 0); + # Variable tracking demand deferred by demand flex resource y in period t + @variable(EP, vCHARGE_FLEX[y in FLEX, t = 1:T]>=0) -### Expressions ### + ### Expressions ### -## Power Balance Expressions ## -@expression(EP, ePowerBalanceDemandFlex[t=1:T, z=1:Z], - sum(-EP[:vP][y,t]+EP[:vCHARGE_FLEX][y,t] for y in intersect(FLEX, resources_in_zone_by_rid(gen,z))) -) -add_similar_to_expression!(EP[:ePowerBalance], ePowerBalanceDemandFlex) + ## Power Balance Expressions ## + @expression(EP, ePowerBalanceDemandFlex[t = 1:T, z = 1:Z], + sum(-EP[:vP][y, t] + EP[:vCHARGE_FLEX][y, t] + for y in intersect(FLEX, resources_in_zone_by_rid(gen, z)))) + add_similar_to_expression!(EP[:ePowerBalance], ePowerBalanceDemandFlex) -# Capacity Reserves Margin policy -if setup["CapacityReserveMargin"] > 0 - @expression(EP, eCapResMarBalanceFlex[res=1:inputs["NCapacityReserveMargin"], t=1:T], sum(derating_factor(gen[y], tag=res) * (EP[:vCHARGE_FLEX][y,t] - EP[:vP][y,t]) for y in FLEX)) - add_similar_to_expression!(EP[:eCapResMarBalance], eCapResMarBalanceFlex) -end - -## Objective Function Expressions ## - -# Variable costs of "charging" for technologies "y" during hour "t" in zone "z" -@expression(EP, eCVarFlex_in[y in FLEX,t=1:T], inputs["omega"][t]*var_om_cost_per_mwh_in(gen[y])*vCHARGE_FLEX[y,t]) - -# Sum individual resource contributions to variable charging costs to get total variable charging costs -@expression(EP, eTotalCVarFlexInT[t=1:T], sum(eCVarFlex_in[y,t] for y in FLEX)) -@expression(EP, eTotalCVarFlexIn, sum(eTotalCVarFlexInT[t] for t in 1:T)) -add_to_expression!(EP[:eObj], eTotalCVarFlexIn) - -### Constraints ### - -## Flexible demand is available only during specified hours with time delay or time advance (virtual storage-shiftable demand) -for z in 1:Z - # NOTE: Flexible demand operates by zone since capacity is now related to zone demand - FLEX_Z = intersect(FLEX, resources_in_zone_by_rid(gen,z)) - - @constraints(EP, begin - # State of "charge" constraint (equals previous state + charge - discharge) - # NOTE: no maximum energy "stored" or deferred for later hours - # NOTE: Flexible_Demand_Energy_Eff corresponds to energy loss due to time shifting - [y in FLEX_Z, t in 1:T], EP[:vS_FLEX][y,t] == EP[:vS_FLEX][y, hoursbefore(hours_per_subperiod, t, 1)] - flexible_demand_energy_eff(gen[y]) * EP[:vP][y,t] + EP[:vCHARGE_FLEX][y,t] - - # Maximum charging rate - # NOTE: the maximum amount that can be shifted is given by hourly availability of the resource times the maximum capacity of the resource - [y in FLEX_Z, t=1:T], EP[:vCHARGE_FLEX][y,t] <= inputs["pP_Max"][y,t]*EP[:eTotalCap][y] - # NOTE: no maximum discharge rate unless constrained by other factors like transmission, etc. - end) - - - for y in FLEX_Z - - # Require deferred demands to be satisfied within the specified time delay - max_flex_demand_delay = Int(floor(max_flexible_demand_delay(gen[y]))) - - # Require advanced demands to be satisfied within the specified time period - max_flex_demand_advance = Int(floor(max_flexible_demand_advance(gen[y]))) - - @constraint(EP, [t in 1:T], - # cFlexibleDemandDelay: Constraints looks forward over next n hours, where n = max_flexible_demand_delay - sum(EP[:vP][y,e] for e=hoursafter(hours_per_subperiod, t, 1:max_flex_demand_delay)) >= EP[:vS_FLEX][y,t]) - - @constraint(EP, [t in 1:T], - # cFlexibleDemandAdvance: Constraint looks forward over next n hours, where n = max_flexible_demand_advance - sum(EP[:vCHARGE_FLEX][y,e] for e=hoursafter(hours_per_subperiod, t, 1:max_flex_demand_advance)) >= -EP[:vS_FLEX][y,t]) + # Capacity Reserves Margin policy + if setup["CapacityReserveMargin"] > 0 + @expression(EP, + eCapResMarBalanceFlex[res = 1:inputs["NCapacityReserveMargin"], t = 1:T], + sum(derating_factor(gen[y], tag = res) * + (EP[:vCHARGE_FLEX][y, t] - EP[:vP][y, t]) for y in FLEX)) + add_similar_to_expression!(EP[:eCapResMarBalance], eCapResMarBalanceFlex) + end + ## Objective Function Expressions ## + + # Variable costs of "charging" for technologies "y" during hour "t" in zone "z" + @expression(EP, + eCVarFlex_in[y in FLEX, t = 1:T], + inputs["omega"][t]*var_om_cost_per_mwh_in(gen[y])*vCHARGE_FLEX[y, t]) + + # Sum individual resource contributions to variable charging costs to get total variable charging costs + @expression(EP, eTotalCVarFlexInT[t = 1:T], sum(eCVarFlex_in[y, t] for y in FLEX)) + @expression(EP, eTotalCVarFlexIn, sum(eTotalCVarFlexInT[t] for t in 1:T)) + add_to_expression!(EP[:eObj], eTotalCVarFlexIn) + + ### Constraints ### + + ## Flexible demand is available only during specified hours with time delay or time advance (virtual storage-shiftable demand) + for z in 1:Z + # NOTE: Flexible demand operates by zone since capacity is now related to zone demand + FLEX_Z = intersect(FLEX, resources_in_zone_by_rid(gen, z)) + + @constraints(EP, + begin + # State of "charge" constraint (equals previous state + charge - discharge) + # NOTE: no maximum energy "stored" or deferred for later hours + # NOTE: Flexible_Demand_Energy_Eff corresponds to energy loss due to time shifting + [y in FLEX_Z, t in 1:T], + EP[:vS_FLEX][y, t] == + EP[:vS_FLEX][y, hoursbefore(hours_per_subperiod, t, 1)] - + flexible_demand_energy_eff(gen[y]) * EP[:vP][y, t] + + EP[:vCHARGE_FLEX][y, t] + + # Maximum charging rate + # NOTE: the maximum amount that can be shifted is given by hourly availability of the resource times the maximum capacity of the resource + [y in FLEX_Z, t = 1:T], + EP[:vCHARGE_FLEX][y, t] <= inputs["pP_Max"][y, t] * EP[:eTotalCap][y] + # NOTE: no maximum discharge rate unless constrained by other factors like transmission, etc. + end) + + for y in FLEX_Z + + # Require deferred demands to be satisfied within the specified time delay + max_flex_demand_delay = Int(floor(max_flexible_demand_delay(gen[y]))) + + # Require advanced demands to be satisfied within the specified time period + max_flex_demand_advance = Int(floor(max_flexible_demand_advance(gen[y]))) + + @constraint(EP, [t in 1:T], + # cFlexibleDemandDelay: Constraints looks forward over next n hours, where n = max_flexible_demand_delay + sum(EP[:vP][y, e] + for e in hoursafter(hours_per_subperiod, t, 1:max_flex_demand_delay))>=EP[:vS_FLEX][y, + t]) + + @constraint(EP, [t in 1:T], + # cFlexibleDemandAdvance: Constraint looks forward over next n hours, where n = max_flexible_demand_advance + sum(EP[:vCHARGE_FLEX][y, e] + for e in hoursafter(hours_per_subperiod, t, 1:max_flex_demand_advance))>=-EP[:vS_FLEX][y, + t]) + end end -end -return EP + return EP end - diff --git a/src/model/resources/hydro/hydro_inter_period_linkage.jl b/src/model/resources/hydro/hydro_inter_period_linkage.jl index 8ea3836047..5fdab6287d 100644 --- a/src/model/resources/hydro/hydro_inter_period_linkage.jl +++ b/src/model/resources/hydro/hydro_inter_period_linkage.jl @@ -44,55 +44,67 @@ Finally, the next constraint enforces that the initial storage level for each in ``` """ function hydro_inter_period_linkage!(EP::Model, inputs::Dict) - - println("Long Duration Storage Module for Hydro Reservoir") - - gen = inputs["RESOURCES"] - - REP_PERIOD = inputs["REP_PERIOD"] # Number of representative periods - - STOR_HYDRO_LONG_DURATION = inputs["STOR_HYDRO_LONG_DURATION"] - - hours_per_subperiod = inputs["hours_per_subperiod"] #total number of hours per subperiod - - dfPeriodMap = inputs["Period_Map"] # Dataframe that maps modeled periods to representative periods - NPeriods = size(inputs["Period_Map"])[1] # Number of modeled periods - - MODELED_PERIODS_INDEX = 1:NPeriods - REP_PERIODS_INDEX = MODELED_PERIODS_INDEX[dfPeriodMap[!,:Rep_Period] .== MODELED_PERIODS_INDEX] - - ### Variables ### - - # Variables to define inter-period energy transferred between modeled periods - - # State of charge of storage at beginning of each modeled period n - @variable(EP, vSOC_HYDROw[y in STOR_HYDRO_LONG_DURATION, n in MODELED_PERIODS_INDEX] >= 0) - - # Build up in storage inventory over each representative period w - # Build up inventory can be positive or negative - @variable(EP, vdSOC_HYDRO[y in STOR_HYDRO_LONG_DURATION, w=1:REP_PERIOD]) - - ### Constraints ### - - # Links last time step with first time step, ensuring position in hour 1 is within eligible change from final hour position - # Modified initial state of storage for long-duration storage - initialize wth value carried over from last period - # Alternative to cSoCBalStart constraint which is included when not modeling operations wrapping and long duration storage - # Note: tw_min = hours_per_subperiod*(w-1)+1; tw_max = hours_per_subperiod*w - @constraint(EP, cHydroReservoirLongDurationStorageStart[w=1:REP_PERIOD, y in STOR_HYDRO_LONG_DURATION], - EP[:vS_HYDRO][y,hours_per_subperiod*(w-1)+1] == (EP[:vS_HYDRO][y,hours_per_subperiod*w]-vdSOC_HYDRO[y,w])-(1/efficiency_down(gen[y])*EP[:vP][y,hours_per_subperiod*(w-1)+1])-EP[:vSPILL][y,hours_per_subperiod*(w-1)+1]+inputs["pP_Max"][y,hours_per_subperiod*(w-1)+1]*EP[:eTotalCap][y]) - # Storage at beginning of period w = storage at beginning of period w-1 + storage built up in period w (after n representative periods) - ## Multiply storage build up term from prior period with corresponding weight - @constraint(EP, cHydroReservoirLongDurationStorage[y in STOR_HYDRO_LONG_DURATION, r in MODELED_PERIODS_INDEX], - vSOC_HYDROw[y, mod1(r+1, NPeriods)] == vSOC_HYDROw[y,r] + vdSOC_HYDRO[y,dfPeriodMap[r,:Rep_Period_Index]]) - - # Storage at beginning of each modeled period cannot exceed installed energy capacity - @constraint(EP, cHydroReservoirLongDurationStorageUpper[y in STOR_HYDRO_LONG_DURATION, r in MODELED_PERIODS_INDEX], - vSOC_HYDROw[y,r] <= hydro_energy_to_power_ratio(gen[y])*EP[:eTotalCap][y]) - - # Initial storage level for representative periods must also adhere to sub-period storage inventory balance - # Initial storage = Final storage - change in storage inventory across representative period - @constraint(EP, cHydroReservoirLongDurationStorageSub[y in STOR_HYDRO_LONG_DURATION, r in REP_PERIODS_INDEX], - vSOC_HYDROw[y,r] == EP[:vS_HYDRO][y,hours_per_subperiod*dfPeriodMap[r,:Rep_Period_Index]] - vdSOC_HYDRO[y,dfPeriodMap[r,:Rep_Period_Index]]) - - + println("Long Duration Storage Module for Hydro Reservoir") + + gen = inputs["RESOURCES"] + + REP_PERIOD = inputs["REP_PERIOD"] # Number of representative periods + + STOR_HYDRO_LONG_DURATION = inputs["STOR_HYDRO_LONG_DURATION"] + + hours_per_subperiod = inputs["hours_per_subperiod"] #total number of hours per subperiod + + dfPeriodMap = inputs["Period_Map"] # Dataframe that maps modeled periods to representative periods + NPeriods = size(inputs["Period_Map"])[1] # Number of modeled periods + + MODELED_PERIODS_INDEX = 1:NPeriods + REP_PERIODS_INDEX = MODELED_PERIODS_INDEX[dfPeriodMap[!, :Rep_Period] .== MODELED_PERIODS_INDEX] + + ### Variables ### + + # Variables to define inter-period energy transferred between modeled periods + + # State of charge of storage at beginning of each modeled period n + @variable(EP, vSOC_HYDROw[y in STOR_HYDRO_LONG_DURATION, n in MODELED_PERIODS_INDEX]>=0) + + # Build up in storage inventory over each representative period w + # Build up inventory can be positive or negative + @variable(EP, vdSOC_HYDRO[y in STOR_HYDRO_LONG_DURATION, w = 1:REP_PERIOD]) + + ### Constraints ### + + # Links last time step with first time step, ensuring position in hour 1 is within eligible change from final hour position + # Modified initial state of storage for long-duration storage - initialize wth value carried over from last period + # Alternative to cSoCBalStart constraint which is included when not modeling operations wrapping and long duration storage + # Note: tw_min = hours_per_subperiod*(w-1)+1; tw_max = hours_per_subperiod*w + @constraint(EP, + cHydroReservoirLongDurationStorageStart[w = 1:REP_PERIOD, + y in STOR_HYDRO_LONG_DURATION], + EP[:vS_HYDRO][y, + hours_per_subperiod * (w - 1) + 1]==(EP[:vS_HYDRO][y, hours_per_subperiod * w] - vdSOC_HYDRO[y, w]) - + (1 / efficiency_down(gen[y]) * EP[:vP][y, hours_per_subperiod * (w - 1) + 1]) - + EP[:vSPILL][y, hours_per_subperiod * (w - 1) + 1] + + inputs["pP_Max"][y, hours_per_subperiod * (w - 1) + 1] * EP[:eTotalCap][y]) + # Storage at beginning of period w = storage at beginning of period w-1 + storage built up in period w (after n representative periods) + ## Multiply storage build up term from prior period with corresponding weight + @constraint(EP, + cHydroReservoirLongDurationStorage[y in STOR_HYDRO_LONG_DURATION, + r in MODELED_PERIODS_INDEX], + vSOC_HYDROw[y, + mod1(r + 1, NPeriods)]==vSOC_HYDROw[y, r] + vdSOC_HYDRO[y, dfPeriodMap[r, :Rep_Period_Index]]) + + # Storage at beginning of each modeled period cannot exceed installed energy capacity + @constraint(EP, + cHydroReservoirLongDurationStorageUpper[y in STOR_HYDRO_LONG_DURATION, + r in MODELED_PERIODS_INDEX], + vSOC_HYDROw[y, r]<=hydro_energy_to_power_ratio(gen[y]) * EP[:eTotalCap][y]) + + # Initial storage level for representative periods must also adhere to sub-period storage inventory balance + # Initial storage = Final storage - change in storage inventory across representative period + @constraint(EP, + cHydroReservoirLongDurationStorageSub[y in STOR_HYDRO_LONG_DURATION, + r in REP_PERIODS_INDEX], + vSOC_HYDROw[y, + r]==EP[:vS_HYDRO][y, hours_per_subperiod * dfPeriodMap[r, :Rep_Period_Index]] - + vdSOC_HYDRO[y, dfPeriodMap[r, :Rep_Period_Index]]) end diff --git a/src/model/resources/hydro/hydro_res.jl b/src/model/resources/hydro/hydro_res.jl index e9734ed975..ce9b2c69f5 100644 --- a/src/model/resources/hydro/hydro_res.jl +++ b/src/model/resources/hydro/hydro_res.jl @@ -61,24 +61,23 @@ In case the reservoir capacity is known ($y \in W^{cap}$), then an additional co ``` """ function hydro_res!(EP::Model, inputs::Dict, setup::Dict) + println("Hydro Reservoir Core Resources Module") - println("Hydro Reservoir Core Resources Module") + gen = inputs["RESOURCES"] - gen = inputs["RESOURCES"] + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones - T = inputs["T"] # Number of time steps (hours) - Z = inputs["Z"] # Number of zones + p = inputs["hours_per_subperiod"] # total number of hours per subperiod - p = inputs["hours_per_subperiod"] # total number of hours per subperiod + HYDRO_RES = inputs["HYDRO_RES"]# Set of all reservoir hydro resources, used for common constraints + HYDRO_RES_KNOWN_CAP = inputs["HYDRO_RES_KNOWN_CAP"] # Reservoir hydro resources modeled with unknown reservoir energy capacity - HYDRO_RES = inputs["HYDRO_RES"] # Set of all reservoir hydro resources, used for common constraints - HYDRO_RES_KNOWN_CAP = inputs["HYDRO_RES_KNOWN_CAP"] # Reservoir hydro resources modeled with unknown reservoir energy capacity + STOR_HYDRO_SHORT_DURATION = inputs["STOR_HYDRO_SHORT_DURATION"] + representative_periods = inputs["REP_PERIOD"] - STOR_HYDRO_SHORT_DURATION = inputs["STOR_HYDRO_SHORT_DURATION"] - representative_periods = inputs["REP_PERIOD"] - - START_SUBPERIODS = inputs["START_SUBPERIODS"] - INTERIOR_SUBPERIODS = inputs["INTERIOR_SUBPERIODS"] + START_SUBPERIODS = inputs["START_SUBPERIODS"] + INTERIOR_SUBPERIODS = inputs["INTERIOR_SUBPERIODS"] # These variables are used in the ramp-up and ramp-down expressions reserves_term = @expression(EP, [y in HYDRO_RES, t in 1:T], 0) @@ -88,81 +87,99 @@ function hydro_res!(EP::Model, inputs::Dict, setup::Dict) HYDRO_RES_REG = intersect(HYDRO_RES, inputs["REG"]) # Set of reservoir hydro resources with regulation reserves HYDRO_RES_RSV = intersect(HYDRO_RES, inputs["RSV"]) # Set of reservoir hydro resources with spinning reserves regulation_term = @expression(EP, [y in HYDRO_RES, t in 1:T], - y ∈ HYDRO_RES_REG ? EP[:vREG][y,t] - EP[:vREG][y, hoursbefore(p, t, 1)] : 0) + y ∈ HYDRO_RES_REG ? EP[:vREG][y, t] - EP[:vREG][y, hoursbefore(p, t, 1)] : 0) reserves_term = @expression(EP, [y in HYDRO_RES, t in 1:T], - y ∈ HYDRO_RES_RSV ? EP[:vRSV][y,t] : 0) + y ∈ HYDRO_RES_RSV ? EP[:vRSV][y, t] : 0) + end + + ### Variables ### + + # Reservoir hydro storage level of resource "y" at hour "t" [MWh] on zone "z" - unbounded + @variable(EP, vS_HYDRO[y in HYDRO_RES, t = 1:T]>=0) + + # Hydro reservoir overflow (water spill) variable + @variable(EP, vSPILL[y in HYDRO_RES, t = 1:T]>=0) + + ### Expressions ### + + ## Power Balance Expressions ## + @expression(EP, ePowerBalanceHydroRes[t = 1:T, z = 1:Z], + sum(EP[:vP][y, t] for y in intersect(HYDRO_RES, resources_in_zone_by_rid(gen, z)))) + add_similar_to_expression!(EP[:ePowerBalance], ePowerBalanceHydroRes) + + # Capacity Reserves Margin policy + if setup["CapacityReserveMargin"] > 0 + @expression(EP, + eCapResMarBalanceHydro[res = 1:inputs["NCapacityReserveMargin"], t = 1:T], + sum(derating_factor(gen[y], tag = res) * EP[:vP][y, t] for y in HYDRO_RES)) + add_similar_to_expression!(EP[:eCapResMarBalance], eCapResMarBalanceHydro) end - ### Variables ### - - # Reservoir hydro storage level of resource "y" at hour "t" [MWh] on zone "z" - unbounded - @variable(EP, vS_HYDRO[y in HYDRO_RES, t=1:T] >= 0); - - # Hydro reservoir overflow (water spill) variable - @variable(EP, vSPILL[y in HYDRO_RES, t=1:T] >= 0) - - ### Expressions ### - - ## Power Balance Expressions ## - @expression(EP, ePowerBalanceHydroRes[t=1:T, z=1:Z], - sum(EP[:vP][y,t] for y in intersect(HYDRO_RES, resources_in_zone_by_rid(gen,z))) - ) - add_similar_to_expression!(EP[:ePowerBalance], ePowerBalanceHydroRes) - - # Capacity Reserves Margin policy - if setup["CapacityReserveMargin"] > 0 - @expression(EP, eCapResMarBalanceHydro[res=1:inputs["NCapacityReserveMargin"], t=1:T], sum(derating_factor(gen[y], tag=res) * EP[:vP][y,t] for y in HYDRO_RES)) - add_similar_to_expression!(EP[:eCapResMarBalance], eCapResMarBalanceHydro) - end - - ### Constratints ### - - if representative_periods > 1 && !isempty(inputs["STOR_HYDRO_LONG_DURATION"]) - CONSTRAINTSET = STOR_HYDRO_SHORT_DURATION - else - CONSTRAINTSET = HYDRO_RES - end - - @constraint(EP, cHydroReservoirStart[y in CONSTRAINTSET,t in START_SUBPERIODS], EP[:vS_HYDRO][y,t] == EP[:vS_HYDRO][y, hoursbefore(p,t,1)]- (1/efficiency_down(gen[y])*EP[:vP][y,t]) - vSPILL[y,t] + inputs["pP_Max"][y,t]*EP[:eTotalCap][y]) - - ### Constraints commmon to all reservoir hydro (y in set HYDRO_RES) ### - @constraints(EP, begin - ### NOTE: time coupling constraints in this block do not apply to first hour in each sample period; - # Energy stored in reservoir at end of each other hour is equal to energy at end of prior hour less generation and spill and + inflows in the current hour - # The ["pP_Max"][y,t] term here refers to inflows as a fraction of peak discharge power capacity. - # DEV NOTE: Last inputs["pP_Max"][y,t] term above is inflows; currently part of capacity factors inputs in Generators_variability.csv but should be moved to its own Hydro_inflows.csv input in future. - - # Constraints for reservoir hydro - cHydroReservoirInterior[y in HYDRO_RES, t in INTERIOR_SUBPERIODS], EP[:vS_HYDRO][y,t] == (EP[:vS_HYDRO][y, hoursbefore(p,t,1)] - (1/efficiency_down(gen[y])*EP[:vP][y,t]) - vSPILL[y,t] + inputs["pP_Max"][y,t]*EP[:eTotalCap][y]) - - # Maximum ramp up and down - cRampUp[y in HYDRO_RES, t in 1:T], EP[:vP][y,t] + regulation_term[y,t] + reserves_term[y,t] - EP[:vP][y, hoursbefore(p,t,1)] <= ramp_up_fraction(gen[y])*EP[:eTotalCap][y] - cRampDown[y in HYDRO_RES, t in 1:T], EP[:vP][y, hoursbefore(p,t,1)] - EP[:vP][y,t] - regulation_term[y,t] + reserves_term[y, hoursbefore(p,t,1)] <= ramp_down_fraction(gen[y])*EP[:eTotalCap][y] - # Minimum streamflow running requirements (power generation and spills must be >= min value) in all hours - cHydroMinFlow[y in HYDRO_RES, t in 1:T], EP[:vP][y,t] + EP[:vSPILL][y,t] >= min_power(gen[y])*EP[:eTotalCap][y] - # DEV NOTE: When creating new hydro inputs, should rename Min_Power with Min_flow or similar for clarity since this includes spilled water as well - - # Maximum discharging rate must be less than power rating OR available stored energy at start of hour, whichever is less - # DEV NOTE: We do not currently account for hydro power plant outages - leave it for later to figure out if we should. - # DEV NOTE (CONTD): If we defin pPMax as hourly availability of the plant and define inflows as a separate parameter, then notation will be consistent with its use for other resources - cHydroMaxPower[y in HYDRO_RES, t in 1:T], EP[:vP][y,t] <= EP[:eTotalCap][y] - cHydroMaxOutflow[y in HYDRO_RES, t in 1:T], EP[:vP][y,t] <= EP[:vS_HYDRO][y, hoursbefore(p,t,1)] - end) - - ### Constraints to limit maximum energy in storage based on known limits on reservoir energy capacity (only for HYDRO_RES_KNOWN_CAP) - # Maximum energy stored in reservoir must be less than energy capacity in all hours - only applied to HYDRO_RES_KNOWN_CAP - @constraint(EP, cHydroMaxEnergy[y in HYDRO_RES_KNOWN_CAP, t in 1:T], EP[:vS_HYDRO][y,t] <= hydro_energy_to_power_ratio(gen[y])*EP[:eTotalCap][y]) - - if setup["OperationalReserves"] == 1 - ### Reserve related constraints for reservoir hydro resources (y in HYDRO_RES), if used - hydro_res_operational_reserves!(EP, inputs) - end - ##CO2 Polcy Module Hydro Res Generation by zone - @expression(EP, eGenerationByHydroRes[z=1:Z, t=1:T], # the unit is GW - sum(EP[:vP][y,t] for y in intersect(HYDRO_RES, resources_in_zone_by_rid(gen,z))) - ) - add_similar_to_expression!(EP[:eGenerationByZone], eGenerationByHydroRes) + ### Constratints ### + + if representative_periods > 1 && !isempty(inputs["STOR_HYDRO_LONG_DURATION"]) + CONSTRAINTSET = STOR_HYDRO_SHORT_DURATION + else + CONSTRAINTSET = HYDRO_RES + end + @constraint(EP, + cHydroReservoirStart[y in CONSTRAINTSET, t in START_SUBPERIODS], + EP[:vS_HYDRO][y, + t]==EP[:vS_HYDRO][y, hoursbefore(p, t, 1)] - + (1 / efficiency_down(gen[y]) * EP[:vP][y, t]) - vSPILL[y, t] + + inputs["pP_Max"][y, t] * EP[:eTotalCap][y]) + + ### Constraints commmon to all reservoir hydro (y in set HYDRO_RES) ### + @constraints(EP, + begin + ### NOTE: time coupling constraints in this block do not apply to first hour in each sample period; + # Energy stored in reservoir at end of each other hour is equal to energy at end of prior hour less generation and spill and + inflows in the current hour + # The ["pP_Max"][y,t] term here refers to inflows as a fraction of peak discharge power capacity. + # DEV NOTE: Last inputs["pP_Max"][y,t] term above is inflows; currently part of capacity factors inputs in Generators_variability.csv but should be moved to its own Hydro_inflows.csv input in future. + + # Constraints for reservoir hydro + cHydroReservoirInterior[y in HYDRO_RES, t in INTERIOR_SUBPERIODS], + EP[:vS_HYDRO][y, t] == (EP[:vS_HYDRO][y, hoursbefore(p, t, 1)] - + (1 / efficiency_down(gen[y]) * EP[:vP][y, t]) - vSPILL[y, t] + + inputs["pP_Max"][y, t] * EP[:eTotalCap][y]) + + # Maximum ramp up and down + cRampUp[y in HYDRO_RES, t in 1:T], + EP[:vP][y, t] + regulation_term[y, t] + reserves_term[y, t] - + EP[:vP][y, hoursbefore(p, t, 1)] <= + ramp_up_fraction(gen[y]) * EP[:eTotalCap][y] + cRampDown[y in HYDRO_RES, t in 1:T], + EP[:vP][y, hoursbefore(p, t, 1)] - EP[:vP][y, t] - regulation_term[y, t] + + reserves_term[y, hoursbefore(p, t, 1)] <= + ramp_down_fraction(gen[y]) * EP[:eTotalCap][y] + # Minimum streamflow running requirements (power generation and spills must be >= min value) in all hours + cHydroMinFlow[y in HYDRO_RES, t in 1:T], + EP[:vP][y, t] + EP[:vSPILL][y, t] >= min_power(gen[y]) * EP[:eTotalCap][y] + # DEV NOTE: When creating new hydro inputs, should rename Min_Power with Min_flow or similar for clarity since this includes spilled water as well + + # Maximum discharging rate must be less than power rating OR available stored energy at start of hour, whichever is less + # DEV NOTE: We do not currently account for hydro power plant outages - leave it for later to figure out if we should. + # DEV NOTE (CONTD): If we defin pPMax as hourly availability of the plant and define inflows as a separate parameter, then notation will be consistent with its use for other resources + cHydroMaxPower[y in HYDRO_RES, t in 1:T], EP[:vP][y, t] <= EP[:eTotalCap][y] + cHydroMaxOutflow[y in HYDRO_RES, t in 1:T], + EP[:vP][y, t] <= EP[:vS_HYDRO][y, hoursbefore(p, t, 1)] + end) + + ### Constraints to limit maximum energy in storage based on known limits on reservoir energy capacity (only for HYDRO_RES_KNOWN_CAP) + # Maximum energy stored in reservoir must be less than energy capacity in all hours - only applied to HYDRO_RES_KNOWN_CAP + @constraint(EP, + cHydroMaxEnergy[y in HYDRO_RES_KNOWN_CAP, t in 1:T], + EP[:vS_HYDRO][y, t]<=hydro_energy_to_power_ratio(gen[y]) * EP[:eTotalCap][y]) + + if setup["OperationalReserves"] == 1 + ### Reserve related constraints for reservoir hydro resources (y in HYDRO_RES), if used + hydro_res_operational_reserves!(EP, inputs) + end + ##CO2 Polcy Module Hydro Res Generation by zone + @expression(EP, eGenerationByHydroRes[z = 1:Z, t = 1:T], # the unit is GW + sum(EP[:vP][y, t] for y in intersect(HYDRO_RES, resources_in_zone_by_rid(gen, z)))) + add_similar_to_expression!(EP[:eGenerationByZone], eGenerationByHydroRes) end @doc raw""" @@ -195,19 +212,18 @@ r_{y,z, t} \leq \upsilon^{rsv}_{y,z}\times \Delta^{total}_{y,z} ``` """ function hydro_res_operational_reserves!(EP::Model, inputs::Dict) + println("Hydro Reservoir Operational Reserves Module") - println("Hydro Reservoir Operational Reserves Module") - - gen = inputs["RESOURCES"] + gen = inputs["RESOURCES"] - T = inputs["T"] # Number of time steps (hours) + T = inputs["T"] # Number of time steps (hours) - HYDRO_RES = inputs["HYDRO_RES"] + HYDRO_RES = inputs["HYDRO_RES"] REG = inputs["REG"] RSV = inputs["RSV"] - HYDRO_RES_REG = intersect(HYDRO_RES, REG) # Set of reservoir hydro resources with regulation reserves - HYDRO_RES_RSV = intersect(HYDRO_RES, RSV) # Set of reservoir hydro resources with spinning reserves + HYDRO_RES_REG = intersect(HYDRO_RES, REG) # Set of reservoir hydro resources with regulation reserves + HYDRO_RES_RSV = intersect(HYDRO_RES, RSV) # Set of reservoir hydro resources with spinning reserves vP = EP[:vP] vREG = EP[:vREG] @@ -224,9 +240,13 @@ function hydro_res_operational_reserves!(EP::Model, inputs::Dict) S = HYDRO_RES_RSV add_similar_to_expression!(max_up_reserves_lhs[S, :], vRSV[S, :]) - @constraint(EP, [y in HYDRO_RES, t in 1:T], max_up_reserves_lhs[y, t] <= eTotalCap[y]) - @constraint(EP, [y in HYDRO_RES, t in 1:T], max_dn_reserves_lhs[y, t] >= 0) + @constraint(EP, [y in HYDRO_RES, t in 1:T], max_up_reserves_lhs[y, t]<=eTotalCap[y]) + @constraint(EP, [y in HYDRO_RES, t in 1:T], max_dn_reserves_lhs[y, t]>=0) - @constraint(EP, [y in HYDRO_RES_REG, t in 1:T], vREG[y, t] <= reg_max(gen[y]) * eTotalCap[y]) - @constraint(EP, [y in HYDRO_RES_RSV, t in 1:T], vRSV[y, t] <= rsv_max(gen[y]) * eTotalCap[y]) + @constraint(EP, + [y in HYDRO_RES_REG, t in 1:T], + vREG[y, t]<=reg_max(gen[y]) * eTotalCap[y]) + @constraint(EP, + [y in HYDRO_RES_RSV, t in 1:T], + vRSV[y, t]<=rsv_max(gen[y]) * eTotalCap[y]) end diff --git a/src/model/resources/hydrogen/electrolyzer.jl b/src/model/resources/hydrogen/electrolyzer.jl index f14b9a8c38..bfd21d505d 100644 --- a/src/model/resources/hydrogen/electrolyzer.jl +++ b/src/model/resources/hydrogen/electrolyzer.jl @@ -78,99 +78,122 @@ This optional constraint (enabled by setting `HydrogenHourlyMatching==1` in `gen This constraint permits modeling of the 'three pillars' requirements for clean hydrogen supply of (1) new clean supply (if only new clean resources are designated as eligible), (2) that is deliverable to the electrolyzer (assuming co-location within the same modeled zone = deliverability), and (3) produced within the same hour as the electrolyzer consumes power (otherwise known as 'additionality/new supply', 'deliverability', and 'temporal matching requirements') See Ricks, Xu & Jenkins (2023), ''Minimizing emissions from grid-based hydrogen production in the United States'' *Environ. Res. Lett.* 18 014025 [doi:10.1088/1748-9326/acacb5](https://iopscience.iop.org/article/10.1088/1748-9326/acacb5/meta) for more. """ function electrolyzer!(EP::Model, inputs::Dict, setup::Dict) - println("Electrolyzer Resources Module") + println("Electrolyzer Resources Module") - gen = inputs["RESOURCES"] + gen = inputs["RESOURCES"] - T = inputs["T"] # Number of time steps (hours) - Z = inputs["Z"] # Number of zones + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones - ELECTROLYZERS = inputs["ELECTROLYZER"] - STORAGE = inputs["STOR_ALL"] + ELECTROLYZERS = inputs["ELECTROLYZER"] + STORAGE = inputs["STOR_ALL"] - p = inputs["hours_per_subperiod"] #total number of hours per subperiod + p = inputs["hours_per_subperiod"] #total number of hours per subperiod - ### Variables ### + ### Variables ### - # Electrical energy consumed by electrolyzer resource "y" at hour "t" - @variable(EP, vUSE[y=ELECTROLYZERS, t in 1:T] >=0); + # Electrical energy consumed by electrolyzer resource "y" at hour "t" + @variable(EP, vUSE[y = ELECTROLYZERS, t in 1:T]>=0) - ### Expressions ### + ### Expressions ### - ## Power Balance Expressions ## + ## Power Balance Expressions ## - @expression(EP, ePowerBalanceElectrolyzers[t in 1:T, z in 1:Z], - sum(EP[:vUSE][y,t] for y in intersect(ELECTROLYZERS, resources_in_zone_by_rid(gen,z)))) + @expression(EP, ePowerBalanceElectrolyzers[t in 1:T, z in 1:Z], + sum(EP[:vUSE][y, t] + for y in intersect(ELECTROLYZERS, resources_in_zone_by_rid(gen, z)))) - # Electrolyzers consume electricity so their vUSE is subtracted from power balance - EP[:ePowerBalance] -= ePowerBalanceElectrolyzers + # Electrolyzers consume electricity so their vUSE is subtracted from power balance + EP[:ePowerBalance] -= ePowerBalanceElectrolyzers - # Capacity Reserves Margin policy - ## Electrolyzers currently do not contribute to capacity reserve margin. Could allow them to contribute as a curtailable demand in future. + # Capacity Reserves Margin policy + ## Electrolyzers currently do not contribute to capacity reserve margin. Could allow them to contribute as a curtailable demand in future. - ### Constraints ### + ### Constraints ### - ### Maximum ramp up and down between consecutive hours (Constraints #1-2) - @constraints(EP, begin - ## Maximum ramp up between consecutive hours - [y in ELECTROLYZERS, t in 1:T], EP[:vUSE][y,t] - EP[:vUSE][y, hoursbefore(p,t,1)] <= ramp_up_fraction(gen[y])*EP[:eTotalCap][y] + ### Maximum ramp up and down between consecutive hours (Constraints #1-2) + @constraints(EP, + begin + ## Maximum ramp up between consecutive hours + [y in ELECTROLYZERS, t in 1:T], + EP[:vUSE][y, t] - EP[:vUSE][y, hoursbefore(p, t, 1)] <= + ramp_up_fraction(gen[y]) * EP[:eTotalCap][y] - ## Maximum ramp down between consecutive hours - [y in ELECTROLYZERS, t in 1:T], EP[:vUSE][y, hoursbefore(p,t,1)] - EP[:vUSE][y,t] <= ramp_down_fraction(gen[y])*EP[:eTotalCap][y] - end) + ## Maximum ramp down between consecutive hours + [y in ELECTROLYZERS, t in 1:T], + EP[:vUSE][y, hoursbefore(p, t, 1)] - EP[:vUSE][y, t] <= + ramp_down_fraction(gen[y]) * EP[:eTotalCap][y] + end) - ### Minimum and maximum power output constraints (Constraints #3-4) + ### Minimum and maximum power output constraints (Constraints #3-4) # Electrolyzers currently do not contribute to operating reserves, so there is not # special case (for OperationalReserves == 1) here. # Could allow them to contribute as a curtailable demand in future. + @constraints(EP, + begin + # Minimum stable power generated per technology "y" at hour "t" Min_Power + [y in ELECTROLYZERS, t in 1:T], + EP[:vUSE][y, t] >= min_power(gen[y]) * EP[:eTotalCap][y] + + # Maximum power generated per technology "y" at hour "t" + [y in ELECTROLYZERS, t in 1:T], + EP[:vUSE][y, t] <= inputs["pP_Max"][y, t] * EP[:eTotalCap][y] + end) + + ### Minimum hydrogen production constraint (if any) (Constraint #5) + kt_to_t = 10^3 + @constraint(EP, + cHydrogenMin[y in ELECTROLYZERS], + sum(inputs["omega"][t] * EP[:vUSE][y, t] / hydrogen_mwh_per_tonne(gen[y]) + for t in 1:T)>=electrolyzer_min_kt(gen[y]) * kt_to_t) + + ### Remove vP (electrolyzers do not produce power so vP = 0 for all periods) @constraints(EP, begin - # Minimum stable power generated per technology "y" at hour "t" Min_Power - [y in ELECTROLYZERS, t in 1:T], EP[:vUSE][y,t] >= min_power(gen[y])*EP[:eTotalCap][y] - - # Maximum power generated per technology "y" at hour "t" - [y in ELECTROLYZERS, t in 1:T], EP[:vUSE][y,t] <= inputs["pP_Max"][y,t]*EP[:eTotalCap][y] + [y in ELECTROLYZERS, t in 1:T], EP[:vP][y, t] == 0 end) - ### Minimum hydrogen production constraint (if any) (Constraint #5) - kt_to_t = 10^3 - @constraint(EP, - cHydrogenMin[y in ELECTROLYZERS], - sum(inputs["omega"][t] * EP[:vUSE][y,t] / hydrogen_mwh_per_tonne(gen[y]) for t=1:T) >= electrolyzer_min_kt(gen[y]) * kt_to_t - ) - - ### Remove vP (electrolyzers do not produce power so vP = 0 for all periods) - @constraints(EP, begin - [y in ELECTROLYZERS, t in 1:T], EP[:vP][y,t] == 0 - end) - - ### Hydrogen Hourly Supply Matching Constraint (Constraint #6) ### - # Requires generation from qualified resources (indicated by Qualified_Hydrogen_Supply==1 in the resource .csv files) - # from within the same zone as the electrolyzers are located to be >= hourly consumption from electrolyzers in the zone - # (and any charging by qualified storage within the zone used to help increase electrolyzer utilization). - if setup["HydrogenHourlyMatching"] == 1 - HYDROGEN_ZONES = unique(zone_id.(gen.Electrolyzer)) - QUALIFIED_SUPPLY = ids_with(gen, qualified_hydrogen_supply) - @constraint(EP, cHourlyMatching[z in HYDROGEN_ZONES, t in 1:T], - sum(EP[:vP][y,t] for y=intersect(resources_in_zone_by_rid(gen,z), QUALIFIED_SUPPLY)) >= sum(EP[:vUSE][y,t] for y=intersect(resources_in_zone_by_rid(gen,z), ELECTROLYZERS)) + sum(EP[:vCHARGE][y,t] for y=intersect(resources_in_zone_by_rid(gen,z), QUALIFIED_SUPPLY, STORAGE)) - ) - end - - - ### Energy Share Requirement Policy ### - # Since we're using vUSE to denote electrolyzer consumption, we subtract this from the eESR Energy Share Requirement balance to increase demand for clean resources if desired - # Electrolyzer demand is only accounted for in an ESR that the electrolyzer resources is tagged in in Generates_data.csv (e.g. ESR_N > 0) and - # a share of electrolyzer demand equal to df[y,:ESR_N] must be met by resources qualifying for ESR_N for each electrolyzer resource y. - if setup["EnergyShareRequirement"] >= 1 - @expression(EP, eElectrolyzerESR[ESR in 1:inputs["nESR"]], sum(inputs["omega"][t]*EP[:vUSE][y,t] for y=intersect(ELECTROLYZERS, ids_with_policy(gen,esr,tag=ESR)), t in 1:T)) - EP[:eESR] -= eElectrolyzerESR - end - - ### Objective Function ### - # Subtract hydrogen revenue from objective function - scale_factor = setup["ParameterScale"] == 1 ? 10^6 : 1 # If ParameterScale==1, costs are in millions of $ - @expression(EP, eHydrogenValue[y in ELECTROLYZERS, t in 1:T], (inputs["omega"][t] * EP[:vUSE][y,t] / hydrogen_mwh_per_tonne(gen[y]) * hydrogen_price_per_tonne(gen[y]) / scale_factor)) - @expression(EP, eTotalHydrogenValueT[t in 1:T], sum(eHydrogenValue[y,t] for y in ELECTROLYZERS)) - @expression(EP, eTotalHydrogenValue, sum(eTotalHydrogenValueT[t] for t in 1:T)) - EP[:eObj] -= eTotalHydrogenValue - + ### Hydrogen Hourly Supply Matching Constraint (Constraint #6) ### + # Requires generation from qualified resources (indicated by Qualified_Hydrogen_Supply==1 in the resource .csv files) + # from within the same zone as the electrolyzers are located to be >= hourly consumption from electrolyzers in the zone + # (and any charging by qualified storage within the zone used to help increase electrolyzer utilization). + if setup["HydrogenHourlyMatching"] == 1 + HYDROGEN_ZONES = unique(zone_id.(gen.Electrolyzer)) + QUALIFIED_SUPPLY = ids_with(gen, qualified_hydrogen_supply) + @constraint(EP, cHourlyMatching[z in HYDROGEN_ZONES, t in 1:T], + sum(EP[:vP][y, t] + for y in intersect(resources_in_zone_by_rid(gen, z), QUALIFIED_SUPPLY))>=sum(EP[:vUSE][y, + t] for y in intersect(resources_in_zone_by_rid(gen, + z), + ELECTROLYZERS)) + sum(EP[:vCHARGE][y, + t] for y in intersect(resources_in_zone_by_rid(gen, + z), + QUALIFIED_SUPPLY, + STORAGE))) + end + + ### Energy Share Requirement Policy ### + # Since we're using vUSE to denote electrolyzer consumption, we subtract this from the eESR Energy Share Requirement balance to increase demand for clean resources if desired + # Electrolyzer demand is only accounted for in an ESR that the electrolyzer resources is tagged in in Generates_data.csv (e.g. ESR_N > 0) and + # a share of electrolyzer demand equal to df[y,:ESR_N] must be met by resources qualifying for ESR_N for each electrolyzer resource y. + if setup["EnergyShareRequirement"] >= 1 + @expression(EP, + eElectrolyzerESR[ESR in 1:inputs["nESR"]], + sum(inputs["omega"][t] * EP[:vUSE][y, t] + for y in intersect(ELECTROLYZERS, ids_with_policy(gen, esr, tag = ESR)), + t in 1:T)) + EP[:eESR] -= eElectrolyzerESR + end + + ### Objective Function ### + # Subtract hydrogen revenue from objective function + scale_factor = setup["ParameterScale"] == 1 ? 10^6 : 1 # If ParameterScale==1, costs are in millions of $ + @expression(EP, + eHydrogenValue[y in ELECTROLYZERS, t in 1:T], + (inputs["omega"][t] * EP[:vUSE][y, t] / hydrogen_mwh_per_tonne(gen[y]) * + hydrogen_price_per_tonne(gen[y])/scale_factor)) + @expression(EP, + eTotalHydrogenValueT[t in 1:T], + sum(eHydrogenValue[y, t] for y in ELECTROLYZERS)) + @expression(EP, eTotalHydrogenValue, sum(eTotalHydrogenValueT[t] for t in 1:T)) + EP[:eObj] -= eTotalHydrogenValue end diff --git a/src/model/resources/maintenance.jl b/src/model/resources/maintenance.jl index 1499fa09c8..37d9c0ce82 100644 --- a/src/model/resources/maintenance.jl +++ b/src/model/resources/maintenance.jl @@ -12,7 +12,7 @@ const MAINTENANCE_SHUT_VARS = "MaintenanceShutVariables" """ function resources_with_maintenance(df::DataFrame)::Vector{Int} if "MAINT" in names(df) - df[df.MAINT.>0, :R_ID] + df[df.MAINT .> 0, :R_ID] else Vector{Int}[] end @@ -58,13 +58,11 @@ end maintenance_duration: length of a maintenance period maintenance_begin_hours: collection of hours in which maintenance is allowed to start """ -function controlling_maintenance_start_hours( - p::Int, +function controlling_maintenance_start_hours(p::Int, t::Int, maintenance_duration::Int, - maintenance_begin_hours, -) - controlled_hours = hoursbefore(p, t, 0:(maintenance_duration-1)) + maintenance_begin_hours) + controlled_hours = hoursbefore(p, t, 0:(maintenance_duration - 1)) return intersect(controlled_hours, maintenance_begin_hours) end @@ -103,8 +101,7 @@ end Creates maintenance-tracking variables and adds their Symbols to two Sets in `inputs`. Adds constraints which act on the vCOMMIT-like variable. """ -function maintenance_formulation!( - EP::Model, +function maintenance_formulation!(EP::Model, inputs::Dict, resource_component::AbstractString, r_id::Int, @@ -114,9 +111,7 @@ function maintenance_formulation!( cap::Float64, vcommit::Symbol, ecap::Symbol, - integer_operational_unit_commitment::Bool, -) - + integer_operational_unit_commitment::Bool) T = 1:inputs["T"] hours_per_subperiod = inputs["hours_per_subperiod"] @@ -132,14 +127,11 @@ function maintenance_formulation!( maintenance_begin_hours = 1:maint_begin_cadence:T[end] # create variables - vMDOWN = EP[down] = @variable(EP, [t in T], base_name = down_name, lower_bound = 0) - vMSHUT = - EP[shut] = @variable( - EP, - [t in maintenance_begin_hours], - base_name = shut_name, - lower_bound = 0 - ) + vMDOWN = EP[down] = @variable(EP, [t in T], base_name=down_name, lower_bound=0) + vMSHUT = EP[shut] = @variable(EP, + [t in maintenance_begin_hours], + base_name=shut_name, + lower_bound=0) if integer_operational_unit_commitment set_integer.(vMDOWN) @@ -155,22 +147,20 @@ function maintenance_formulation!( end) # Plant is non-committed during maintenance - @constraint(EP, [t in T], vMDOWN[t] + vcommit[y, t] <= ecap[y] / cap) - - controlling_hours(t) = controlling_maintenance_start_hours( - hours_per_subperiod, - t, - maint_dur, - maintenance_begin_hours, - ) + @constraint(EP, [t in T], vMDOWN[t] + vcommit[y, t]<=ecap[y] / cap) + + function controlling_hours(t) + controlling_maintenance_start_hours(hours_per_subperiod, + t, + maint_dur, + maintenance_begin_hours) + end # Plant is down for the required number of hours - @constraint(EP, [t in T], vMDOWN[t] == sum(vMSHUT[controlling_hours(t)])) + @constraint(EP, [t in T], vMDOWN[t]==sum(vMSHUT[controlling_hours(t)])) # Plant requires maintenance every (certain number of) year(s) - @constraint( - EP, - sum(vMSHUT[t] for t in maintenance_begin_hours) >= ecap[y] / cap / maint_freq_years - ) + @constraint(EP, + sum(vMSHUT[t] for t in maintenance_begin_hours)>=ecap[y] / cap / maint_freq_years) return end diff --git a/src/model/resources/must_run/must_run.jl b/src/model/resources/must_run/must_run.jl index a16efb1141..fddcba6258 100644 --- a/src/model/resources/must_run/must_run.jl +++ b/src/model/resources/must_run/must_run.jl @@ -13,40 +13,41 @@ For must-run resources ($y\in \mathcal{MR}$) output in each time period $t$ must ``` """ function must_run!(EP::Model, inputs::Dict, setup::Dict) + println("Must-Run Resources Module") - println("Must-Run Resources Module") + gen = inputs["RESOURCES"] - gen = inputs["RESOURCES"] + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones + G = inputs["G"] # Number of generators - T = inputs["T"] # Number of time steps (hours) - Z = inputs["Z"] # Number of zones - G = inputs["G"] # Number of generators + MUST_RUN = inputs["MUST_RUN"] + CapacityReserveMargin = setup["CapacityReserveMargin"] - MUST_RUN = inputs["MUST_RUN"] - CapacityReserveMargin = setup["CapacityReserveMargin"] + ### Expressions ### - ### Expressions ### + ## Power Balance Expressions ## - ## Power Balance Expressions ## + @expression(EP, ePowerBalanceNdisp[t = 1:T, z = 1:Z], + sum(EP[:vP][y, t] for y in intersect(MUST_RUN, resources_in_zone_by_rid(gen, z)))) + add_similar_to_expression!(EP[:ePowerBalance], ePowerBalanceNdisp) - @expression(EP, ePowerBalanceNdisp[t=1:T, z=1:Z], - sum(EP[:vP][y,t] for y in intersect(MUST_RUN, resources_in_zone_by_rid(gen,z))) - ) - add_similar_to_expression!(EP[:ePowerBalance], ePowerBalanceNdisp) + # Capacity Reserves Margin policy + if CapacityReserveMargin > 0 + @expression(EP, + eCapResMarBalanceMustRun[res = 1:inputs["NCapacityReserveMargin"], t = 1:T], + sum(derating_factor(gen[y], tag = res) * EP[:eTotalCap][y] * + inputs["pP_Max"][y, t] for y in MUST_RUN)) + add_similar_to_expression!(EP[:eCapResMarBalance], eCapResMarBalanceMustRun) + end - # Capacity Reserves Margin policy - if CapacityReserveMargin > 0 - @expression(EP, eCapResMarBalanceMustRun[res=1:inputs["NCapacityReserveMargin"], t=1:T], sum(derating_factor(gen[y], tag=res) * EP[:eTotalCap][y] * inputs["pP_Max"][y,t] for y in MUST_RUN)) - add_similar_to_expression!(EP[:eCapResMarBalance], eCapResMarBalanceMustRun) - end - - ### Constratints ### - - @constraint(EP, [y in MUST_RUN, t=1:T], EP[:vP][y,t] == inputs["pP_Max"][y,t]*EP[:eTotalCap][y]) - ##CO2 Polcy Module Must Run Generation by zone - @expression(EP, eGenerationByMustRun[z=1:Z, t=1:T], # the unit is GW - sum(EP[:vP][y,t] for y in intersect(MUST_RUN, resources_in_zone_by_rid(gen,z))) - ) - add_similar_to_expression!(EP[:eGenerationByZone], eGenerationByMustRun) + ### Constratints ### + @constraint(EP, + [y in MUST_RUN, t = 1:T], + EP[:vP][y, t]==inputs["pP_Max"][y, t] * EP[:eTotalCap][y]) + ##CO2 Polcy Module Must Run Generation by zone + @expression(EP, eGenerationByMustRun[z = 1:Z, t = 1:T], # the unit is GW + sum(EP[:vP][y, t] for y in intersect(MUST_RUN, resources_in_zone_by_rid(gen, z)))) + add_similar_to_expression!(EP[:eGenerationByZone], eGenerationByMustRun) end diff --git a/src/model/resources/resources.jl b/src/model/resources/resources.jl index 4b86f9ee7d..1e12375064 100644 --- a/src/model/resources/resources.jl +++ b/src/model/resources/resources.jl @@ -14,20 +14,20 @@ Possible values: - :Electrolyzer """ const resource_types = (:Thermal, - :Vre, - :Hydro, - :Storage, - :MustRun, - :FlexDemand, - :VreStorage, - :Electrolyzer) + :Vre, + :Hydro, + :Storage, + :MustRun, + :FlexDemand, + :VreStorage, + :Electrolyzer) # Create composite types (structs) for each resource type in resource_types for r in resource_types let dict = :dict, r = r @eval begin - struct $r{names<:Symbol, T<:Any} <: AbstractResource - $dict::Dict{names,T} + struct $r{names <: Symbol, T <: Any} <: AbstractResource + $dict::Dict{names, T} end Base.parent(r::$r) = getfield(r, $(QuoteNode(dict))) end @@ -66,7 +66,9 @@ Allows to set the attribute `sym` of an `AbstractResource` object using dot synt - `value`: The value to set for the attribute. """ -Base.setproperty!(r::AbstractResource, sym::Symbol, value) = setindex!(parent(r), value, sym) +Base.setproperty!(r::AbstractResource, sym::Symbol, value) = setindex!(parent(r), + value, + sym) """ haskey(r::AbstractResource, sym::Symbol) @@ -97,8 +99,8 @@ Retrieves the value of a specific attribute from an `AbstractResource` object. I - The value of the attribute if it exists in the parent object, `default` otherwise. """ -function Base.get(r::AbstractResource, sym::Symbol, default) - return haskey(r, sym) ? getproperty(r,sym) : default +function Base.get(r::AbstractResource, sym::Symbol, default) + return haskey(r, sym) ? getproperty(r, sym) : default end """ @@ -124,7 +126,7 @@ julia> vre_gen.zone """ function Base.getproperty(rs::Vector{<:AbstractResource}, sym::Symbol) # if sym is Type then return a vector resources of that type - if sym ∈ resource_types + if sym ∈ resource_types res_type = eval(sym) return Vector{res_type}(rs[isa.(rs, res_type)]) end @@ -149,7 +151,7 @@ Set the attributes specified by `sym` to the corresponding values in `value` for function Base.setproperty!(rs::Vector{<:AbstractResource}, sym::Symbol, value::Vector) # if sym is a field of the resource then set that field for all resources @assert length(rs) == length(value) - for (r,v) in zip(rs, value) + for (r, v) in zip(rs, value) setproperty!(r, sym, v) end return rs @@ -172,7 +174,7 @@ Define dot syntax for setting the attributes specified by `sym` to the correspon function Base.setindex!(rs::Vector{<:AbstractResource}, value::Vector, sym::Symbol) # if sym is a field of the resource then set that field for all resources @assert length(rs) == length(value) - for (r,v) in zip(rs, value) + for (r, v) in zip(rs, value) setproperty!(r, sym, v) end return rs @@ -207,8 +209,8 @@ function Base.show(io::IO, r::AbstractResource) value_length = length(resource_name(r)) + 3 println(io, "\nResource: $(r.resource) (id: $(r.id))") println(io, repeat("-", key_length + value_length)) - for (k,v) in pairs(r) - k,v = string(k), string(v) + for (k, v) in pairs(r) + k, v = string(k), string(v) k = k * repeat(" ", key_length - length(k)) println(io, "$k | $v") end @@ -231,7 +233,6 @@ function attributes(r::AbstractResource) return tuple(keys(parent(r))...) end - """ findall(f::Function, rs::Vector{<:AbstractResource}) @@ -254,7 +255,8 @@ julia> findall(r -> max_cap_mwh(r) != 0, gen.Storage) 50 ``` """ -Base.findall(f::Function, rs::Vector{<:AbstractResource}) = resource_id.(filter(r -> f(r), rs)) +Base.findall(f::Function, rs::Vector{<:AbstractResource}) = resource_id.(filter(r -> f(r), + rs)) """ interface(name, default=default_zero, type=AbstractResource) @@ -283,7 +285,7 @@ julia> max_cap_mw.(gen.Vre) # vectorized 9.848441999999999 ``` """ -macro interface(name, default=default_zero, type=AbstractResource) +macro interface(name, default = default_zero, type = AbstractResource) quote function $(esc(name))(r::$(esc(type))) return get(r, $(QuoteNode(name)), $(esc(default))) @@ -314,7 +316,7 @@ julia> max_cap_mw(gen[3]) 4.888236 ``` """ -function ids_with_positive(rs::Vector{T}, f::Function) where T <: AbstractResource +function ids_with_positive(rs::Vector{T}, f::Function) where {T <: AbstractResource} return findall(r -> f(r) > 0, rs) end @@ -341,13 +343,14 @@ julia> max_cap_mw(gen[3]) 4.888236 ``` """ -function ids_with_positive(rs::Vector{T}, name::Symbol) where T <: AbstractResource +function ids_with_positive(rs::Vector{T}, name::Symbol) where {T <: AbstractResource} # if the getter function exists in GenX then use it, otherwise get the attribute directly f = isdefined(GenX, name) ? getfield(GenX, name) : r -> getproperty(r, name) return ids_with_positive(rs, f) end -function ids_with_positive(rs::Vector{T}, name::AbstractString) where T <: AbstractResource +function ids_with_positive(rs::Vector{T}, + name::AbstractString) where {T <: AbstractResource} return ids_with_positive(rs, Symbol(lowercase(name))) end @@ -368,7 +371,7 @@ Function for finding resources in a vector `rs` where the attribute specified by julia> ids_with_nonneg(gen, max_cap_mw) ``` """ -function ids_with_nonneg(rs::Vector{T}, f::Function) where T <: AbstractResource +function ids_with_nonneg(rs::Vector{T}, f::Function) where {T <: AbstractResource} return findall(r -> f(r) >= 0, rs) end @@ -389,13 +392,13 @@ Function for finding resources in a vector `rs` where the attribute specified by julia> ids_with_nonneg(gen, max_cap_mw) ``` """ -function ids_with_nonneg(rs::Vector{T}, name::Symbol) where T <: AbstractResource +function ids_with_nonneg(rs::Vector{T}, name::Symbol) where {T <: AbstractResource} # if the getter function exists in GenX then use it, otherwise get the attribute directly f = isdefined(GenX, name) ? getfield(GenX, name) : r -> getproperty(r, name) return ids_with_nonneg(rs, f) end -function ids_with_nonneg(rs::Vector{T}, name::AbstractString) where T <: AbstractResource +function ids_with_nonneg(rs::Vector{T}, name::AbstractString) where {T <: AbstractResource} return ids_with_nonneg(rs, Symbol(lowercase(name))) end @@ -425,7 +428,9 @@ julia> existing_cap_mw(gen[21]) 7.0773 ``` """ -function ids_with(rs::Vector{T}, f::Function, default=default_zero) where T <: AbstractResource +function ids_with(rs::Vector{T}, + f::Function, + default = default_zero) where {T <: AbstractResource} return findall(r -> f(r) != default, rs) end @@ -454,13 +459,17 @@ julia> existing_cap_mw(gen[21]) 7.0773 ``` """ -function ids_with(rs::Vector{T}, name::Symbol, default=default_zero) where T <: AbstractResource +function ids_with(rs::Vector{T}, + name::Symbol, + default = default_zero) where {T <: AbstractResource} # if the getter function exists in GenX then use it, otherwise get the attribute directly f = isdefined(GenX, name) ? getfield(GenX, name) : r -> getproperty(r, name) return ids_with(rs, f, default) end -function ids_with(rs::Vector{T}, name::AbstractString, default=default_zero) where T <: AbstractResource +function ids_with(rs::Vector{T}, + name::AbstractString, + default = default_zero) where {T <: AbstractResource} return ids_with(rs, Symbol(lowercase(name)), default) end @@ -477,8 +486,10 @@ Function for finding resources in a vector `rs` where the policy specified by `f # Returns - `ids (Vector{Int64})`: The vector of resource ids with a positive value for policy `f` and tag `tag`. """ -function ids_with_policy(rs::Vector{T}, f::Function; tag::Int64) where T <: AbstractResource - return findall(r -> f(r, tag=tag) > 0, rs) +function ids_with_policy(rs::Vector{T}, + f::Function; + tag::Int64) where {T <: AbstractResource} + return findall(r -> f(r, tag = tag) > 0, rs) end """ @@ -494,17 +505,21 @@ Function for finding resources in a vector `rs` where the policy specified by `n # Returns - `ids (Vector{Int64})`: The vector of resource ids with a positive value for policy `name` and tag `tag`. """ -function ids_with_policy(rs::Vector{T}, name::Symbol; tag::Int64) where T <: AbstractResource +function ids_with_policy(rs::Vector{T}, + name::Symbol; + tag::Int64) where {T <: AbstractResource} # if the getter function exists in GenX then use it, otherwise get the attribute directly if isdefined(GenX, name) f = getfield(GenX, name) - return ids_with_policy(rs, f, tag=tag) + return ids_with_policy(rs, f, tag = tag) end return findall(r -> getproperty(r, Symbol(string(name, "_$tag"))) > 0, rs) end -function ids_with_policy(rs::Vector{T}, name::AbstractString; tag::Int64) where T <: AbstractResource - return ids_with_policy(rs, Symbol(lowercase(name)), tag=tag) +function ids_with_policy(rs::Vector{T}, + name::AbstractString; + tag::Int64) where {T <: AbstractResource} + return ids_with_policy(rs, Symbol(lowercase(name)), tag = tag) end """ @@ -512,18 +527,18 @@ end Default value for resource attributes. """ -const default_zero = 0 +const default_zero = 0 # INTERFACE FOR ALL RESOURCES resource_name(r::AbstractResource) = r.resource -resource_name(rs::Vector{T}) where T <: AbstractResource = rs.resource +resource_name(rs::Vector{T}) where {T <: AbstractResource} = rs.resource resource_id(r::AbstractResource)::Int64 = r.id -resource_id(rs::Vector{T}) where T <: AbstractResource = resource_id.(rs) +resource_id(rs::Vector{T}) where {T <: AbstractResource} = resource_id.(rs) resource_type_mga(r::AbstractResource) = r.resource_type zone_id(r::AbstractResource) = r.zone -zone_id(rs::Vector{T}) where T <: AbstractResource = rs.zone +zone_id(rs::Vector{T}) where {T <: AbstractResource} = rs.zone # getter for boolean attributes (true or false) with validation function new_build(r::AbstractResource) @@ -551,7 +566,7 @@ function can_contribute_min_retirement(r::AbstractResource) return Bool(get(r, :contribute_min_retirement, true)) end -const default_minmax_cap = -1. +const default_minmax_cap = -1.0 max_cap_mw(r::AbstractResource) = get(r, :max_cap_mw, default_minmax_cap) min_cap_mw(r::AbstractResource) = get(r, :min_cap_mw, default_minmax_cap) @@ -569,9 +584,13 @@ cap_size(r::AbstractResource) = get(r, :cap_size, default_zero) num_vre_bins(r::AbstractResource) = get(r, :num_vre_bins, default_zero) -hydro_energy_to_power_ratio(r::AbstractResource) = get(r, :hydro_energy_to_power_ratio, default_zero) +function hydro_energy_to_power_ratio(r::AbstractResource) + get(r, :hydro_energy_to_power_ratio, default_zero) +end -qualified_hydrogen_supply(r::AbstractResource) = get(r, :qualified_hydrogen_supply, default_zero) +function qualified_hydrogen_supply(r::AbstractResource) + get(r, :qualified_hydrogen_supply, default_zero) +end retrofit_id(r::AbstractResource)::String = get(r, :retrofit_id, "None") function retrofit_efficiency(r::AbstractResource) @@ -590,32 +609,58 @@ inv_cost_per_mwyr(r::AbstractResource) = get(r, :inv_cost_per_mwyr, default_zero fixed_om_cost_per_mwyr(r::AbstractResource) = get(r, :fixed_om_cost_per_mwyr, default_zero) var_om_cost_per_mwh(r::AbstractResource) = get(r, :var_om_cost_per_mwh, default_zero) inv_cost_per_mwhyr(r::AbstractResource) = get(r, :inv_cost_per_mwhyr, default_zero) -fixed_om_cost_per_mwhyr(r::AbstractResource) = get(r, :fixed_om_cost_per_mwhyr, default_zero) -inv_cost_charge_per_mwyr(r::AbstractResource) = get(r, :inv_cost_charge_per_mwyr, default_zero) -fixed_om_cost_charge_per_mwyr(r::AbstractResource) = get(r, :fixed_om_cost_charge_per_mwyr, default_zero) +function fixed_om_cost_per_mwhyr(r::AbstractResource) + get(r, :fixed_om_cost_per_mwhyr, default_zero) +end +function inv_cost_charge_per_mwyr(r::AbstractResource) + get(r, :inv_cost_charge_per_mwyr, default_zero) +end +function fixed_om_cost_charge_per_mwyr(r::AbstractResource) + get(r, :fixed_om_cost_charge_per_mwyr, default_zero) +end start_cost_per_mw(r::AbstractResource) = get(r, :start_cost_per_mw, default_zero) # fuel fuel(r::AbstractResource) = get(r, :fuel, "None") -start_fuel_mmbtu_per_mw(r::AbstractResource) = get(r, :start_fuel_mmbtu_per_mw, default_zero) -heat_rate_mmbtu_per_mwh(r::AbstractResource) = get(r, :heat_rate_mmbtu_per_mwh, default_zero) +function start_fuel_mmbtu_per_mw(r::AbstractResource) + get(r, :start_fuel_mmbtu_per_mw, default_zero) +end +function heat_rate_mmbtu_per_mwh(r::AbstractResource) + get(r, :heat_rate_mmbtu_per_mwh, default_zero) +end co2_capture_fraction(r::AbstractResource) = get(r, :co2_capture_fraction, default_zero) -co2_capture_fraction_startup(r::AbstractResource) = get(r, :co2_capture_fraction_startup, default_zero) -ccs_disposal_cost_per_metric_ton(r::AbstractResource) = get(r, :ccs_disposal_cost_per_metric_ton, default_zero) +function co2_capture_fraction_startup(r::AbstractResource) + get(r, :co2_capture_fraction_startup, default_zero) +end +function ccs_disposal_cost_per_metric_ton(r::AbstractResource) + get(r, :ccs_disposal_cost_per_metric_ton, default_zero) +end biomass(r::AbstractResource) = get(r, :biomass, default_zero) multi_fuels(r::AbstractResource) = get(r, :multi_fuels, default_zero) -fuel_cols(r::AbstractResource; tag::Int64) = get(r, Symbol(string("fuel",tag)), "None") +fuel_cols(r::AbstractResource; tag::Int64) = get(r, Symbol(string("fuel", tag)), "None") num_fuels(r::AbstractResource) = get(r, :num_fuels, default_zero) -heat_rate_cols(r::AbstractResource; tag::Int64) = get(r, Symbol(string("heat_rate",tag, "_mmbtu_per_mwh")), default_zero) -max_cofire_cols(r::AbstractResource; tag::Int64) = get(r, Symbol(string("fuel",tag, "_max_cofire_level")), 1) -min_cofire_cols(r::AbstractResource; tag::Int64) = get(r, Symbol(string("fuel",tag, "_min_cofire_level")), default_zero) -max_cofire_start_cols(r::AbstractResource; tag::Int64) = get(r, Symbol(string("fuel",tag, "_max_cofire_level_start")), 1) -min_cofire_start_cols(r::AbstractResource; tag::Int64) = get(r, Symbol(string("fuel",tag, "_min_cofire_level_start")), default_zero) +function heat_rate_cols(r::AbstractResource; tag::Int64) + get(r, Symbol(string("heat_rate", tag, "_mmbtu_per_mwh")), default_zero) +end +function max_cofire_cols(r::AbstractResource; tag::Int64) + get(r, Symbol(string("fuel", tag, "_max_cofire_level")), 1) +end +function min_cofire_cols(r::AbstractResource; tag::Int64) + get(r, Symbol(string("fuel", tag, "_min_cofire_level")), default_zero) +end +function max_cofire_start_cols(r::AbstractResource; tag::Int64) + get(r, Symbol(string("fuel", tag, "_max_cofire_level_start")), 1) +end +function min_cofire_start_cols(r::AbstractResource; tag::Int64) + get(r, Symbol(string("fuel", tag, "_min_cofire_level_start")), default_zero) +end # Reservoir hydro and storage const default_percent = 1.0 -efficiency_up(r::T) where T <: Union{Hydro,Storage} = get(r, :eff_up, default_percent) -efficiency_down(r::T) where T <: Union{Hydro,Storage} = get(r, :eff_down, default_percent) +efficiency_up(r::T) where {T <: Union{Hydro, Storage}} = get(r, :eff_up, default_percent) +function efficiency_down(r::T) where {T <: Union{Hydro, Storage}} + get(r, :eff_down, default_percent) +end # Ramp up and down const VarPower = Union{Electrolyzer, Hydro, Thermal} @@ -630,8 +675,12 @@ capital_recovery_period(r::Storage) = get(r, :capital_recovery_period, 15) capital_recovery_period(r::AbstractResource) = get(r, :capital_recovery_period, 30) tech_wacc(r::AbstractResource) = get(r, :wacc, default_zero) min_retired_cap_mw(r::AbstractResource) = get(r, :min_retired_cap_mw, default_zero) -min_retired_energy_cap_mw(r::AbstractResource) = get(r, :min_retired_energy_cap_mw, default_zero) -min_retired_charge_cap_mw(r::AbstractResource) = get(r, :min_retired_charge_cap_mw, default_zero) +function min_retired_energy_cap_mw(r::AbstractResource) + get(r, :min_retired_energy_cap_mw, default_zero) +end +function min_retired_charge_cap_mw(r::AbstractResource) + get(r, :min_retired_charge_cap_mw, default_zero) +end cum_min_retired_cap_mw(r::AbstractResource) = r.cum_min_retired_cap_mw cum_min_retired_energy_cap_mw(r::AbstractResource) = r.cum_min_retired_energy_cap_mw cum_min_retired_charge_cap_mw(r::AbstractResource) = r.cum_min_retired_charge_cap_mw @@ -643,45 +692,85 @@ mga(r::AbstractResource) = get(r, :mga, default_zero) esr(r::AbstractResource; tag::Int64) = get(r, Symbol("esr_$tag"), default_zero) min_cap(r::AbstractResource; tag::Int64) = get(r, Symbol("min_cap_$tag"), default_zero) max_cap(r::AbstractResource; tag::Int64) = get(r, Symbol("max_cap_$tag"), default_zero) -derating_factor(r::AbstractResource; tag::Int64) = get(r, Symbol("derating_factor_$tag"), default_zero) +function derating_factor(r::AbstractResource; tag::Int64) + get(r, Symbol("derating_factor_$tag"), default_zero) +end # write_outputs region(r::AbstractResource) = r.region cluster(r::AbstractResource) = r.cluster # UTILITY FUNCTIONS for working with resources -is_LDS(rs::Vector{T}) where T <: AbstractResource = findall(r -> get(r, :lds, default_zero) == 1, rs) -is_SDS(rs::Vector{T}) where T <: AbstractResource = findall(r -> get(r, :lds, default_zero) == 0, rs) +function is_LDS(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> get(r, :lds, default_zero) == 1, rs) +end +function is_SDS(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> get(r, :lds, default_zero) == 0, rs) +end -ids_with_mga(rs::Vector{T}) where T <: AbstractResource = findall(r -> mga(r) == 1, rs) +ids_with_mga(rs::Vector{T}) where {T <: AbstractResource} = findall(r -> mga(r) == 1, rs) -ids_with_fuel(rs::Vector{T}) where T <: AbstractResource = findall(r -> fuel(r) != "None", rs) +function ids_with_fuel(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> fuel(r) != "None", rs) +end -ids_with_singlefuel(rs::Vector{T}) where T <: AbstractResource = findall(r -> multi_fuels(r) == 0, rs) -ids_with_multifuels(rs::Vector{T}) where T <: AbstractResource = findall(r -> multi_fuels(r) == 1, rs) +function ids_with_singlefuel(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> multi_fuels(r) == 0, rs) +end +function ids_with_multifuels(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> multi_fuels(r) == 1, rs) +end -is_buildable(rs::Vector{T}) where T <: AbstractResource = findall(r -> new_build(r) == true, rs) -is_retirable(rs::Vector{T}) where T <: AbstractResource = findall(r -> can_retire(r) == true, rs) -ids_can_retrofit(rs::Vector{T}) where T <: AbstractResource = findall(r -> can_retrofit(r) == true, rs) -ids_retrofit_options(rs::Vector{T}) where T <: AbstractResource = findall(r -> is_retrofit_option(r) == true, rs) +function is_buildable(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> new_build(r) == true, rs) +end +function is_retirable(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> can_retire(r) == true, rs) +end +function ids_can_retrofit(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> can_retrofit(r) == true, rs) +end +function ids_retrofit_options(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> is_retrofit_option(r) == true, rs) +end # Unit commitment -ids_with_unit_commitment(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,Thermal) && r.model == 1, rs) +function ids_with_unit_commitment(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> isa(r, Thermal) && r.model == 1, rs) +end # Without unit commitment -no_unit_commitment(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,Thermal) && r.model == 2, rs) +function no_unit_commitment(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> isa(r, Thermal) && r.model == 2, rs) +end # Operational Reserves -ids_with_regulation_reserve_requirements(rs::Vector{T}) where T <: AbstractResource = findall(r -> reg_max(r) > 0, rs) -ids_with_spinning_reserve_requirements(rs::Vector{T}) where T <: AbstractResource = findall(r -> rsv_max(r) > 0, rs) +function ids_with_regulation_reserve_requirements(rs::Vector{ + T, +}) where {T <: AbstractResource} + findall(r -> reg_max(r) > 0, rs) +end +function ids_with_spinning_reserve_requirements(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> rsv_max(r) > 0, rs) +end # Maintenance -ids_with_maintenance(rs::Vector{T}) where T <: AbstractResource = findall(r -> get(r, :maint, default_zero) == 1, rs) +function ids_with_maintenance(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> get(r, :maint, default_zero) == 1, rs) +end maintenance_duration(r::AbstractResource) = get(r, :maintenance_duration, default_zero) -maintenance_cycle_length_years(r::AbstractResource) = get(r, :maintenance_cycle_length_years, default_zero) -maintenance_begin_cadence(r::AbstractResource) = get(r, :maintenance_begin_cadence, default_zero) +function maintenance_cycle_length_years(r::AbstractResource) + get(r, :maintenance_cycle_length_years, default_zero) +end +function maintenance_begin_cadence(r::AbstractResource) + get(r, :maintenance_begin_cadence, default_zero) +end -ids_contribute_min_retirement(rs::Vector{T}) where T <: AbstractResource = findall(r -> can_contribute_min_retirement(r) == true, rs) -ids_not_contribute_min_retirement(rs::Vector{T}) where T <: AbstractResource = findall(r -> can_contribute_min_retirement(r) == false, rs) +function ids_contribute_min_retirement(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> can_contribute_min_retirement(r) == true, rs) +end +function ids_not_contribute_min_retirement(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> can_contribute_min_retirement(r) == false, rs) +end # STORAGE interface """ @@ -689,14 +778,18 @@ ids_not_contribute_min_retirement(rs::Vector{T}) where T <: AbstractResource = f Returns the indices of all storage resources in the vector `rs`. """ -storage(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,Storage), rs) +storage(rs::Vector{T}) where {T <: AbstractResource} = findall(r -> isa(r, Storage), rs) self_discharge(r::Storage) = r.self_disch min_duration(r::Storage) = r.min_duration max_duration(r::Storage) = r.max_duration var_om_cost_per_mwh_in(r::Storage) = get(r, :var_om_cost_per_mwh_in, default_zero) -symmetric_storage(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,Storage) && r.model == 1, rs) -asymmetric_storage(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,Storage) && r.model == 2, rs) +function symmetric_storage(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> isa(r, Storage) && r.model == 1, rs) +end +function asymmetric_storage(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> isa(r, Storage) && r.model == 2, rs) +end # HYDRO interface """ @@ -704,7 +797,7 @@ asymmetric_storage(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa Returns the indices of all hydro resources in the vector `rs`. """ -hydro(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,Hydro), rs) +hydro(rs::Vector{T}) where {T <: AbstractResource} = findall(r -> isa(r, Hydro), rs) # THERMAL interface """ @@ -712,10 +805,12 @@ hydro(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,Hydro), rs Returns the indices of all thermal resources in the vector `rs`. """ -thermal(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,Thermal), rs) +thermal(rs::Vector{T}) where {T <: AbstractResource} = findall(r -> isa(r, Thermal), rs) up_time(r::Thermal) = get(r, :up_time, default_zero) down_time(r::Thermal) = get(r, :down_time, default_zero) -pwfu_fuel_usage_zero_load_mmbtu_per_h(r::Thermal) = get(r, :pwfu_fuel_usage_zero_load_mmbtu_per_h, default_zero) +function pwfu_fuel_usage_zero_load_mmbtu_per_h(r::Thermal) + get(r, :pwfu_fuel_usage_zero_load_mmbtu_per_h, default_zero) +end # VRE interface """ @@ -723,7 +818,7 @@ pwfu_fuel_usage_zero_load_mmbtu_per_h(r::Thermal) = get(r, :pwfu_fuel_usage_zero Returns the indices of all Vre resources in the vector `rs`. """ -vre(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,Vre), rs) +vre(rs::Vector{T}) where {T <: AbstractResource} = findall(r -> isa(r, Vre), rs) # ELECTROLYZER interface """ @@ -731,7 +826,9 @@ vre(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,Vre), rs) Returns the indices of all electrolyzer resources in the vector `rs`. """ -electrolyzer(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,Electrolyzer), rs) +electrolyzer(rs::Vector{T}) where {T <: AbstractResource} = findall(r -> isa(r, + Electrolyzer), + rs) electrolyzer_min_kt(r::Electrolyzer) = r.electrolyzer_min_kt hydrogen_mwh_per_tonne(r::Electrolyzer) = r.hydrogen_mwh_per_tonne hydrogen_price_per_tonne(r::Electrolyzer) = r.hydrogen_price_per_tonne @@ -742,7 +839,8 @@ hydrogen_price_per_tonne(r::Electrolyzer) = r.hydrogen_price_per_tonne Returns the indices of all flexible demand resources in the vector `rs`. """ -flex_demand(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,FlexDemand), rs) +flex_demand(rs::Vector{T}) where {T <: AbstractResource} = findall(r -> isa(r, FlexDemand), + rs) flexible_demand_energy_eff(r::FlexDemand) = r.flexible_demand_energy_eff max_flexible_demand_delay(r::FlexDemand) = r.max_flexible_demand_delay max_flexible_demand_advance(r::FlexDemand) = r.max_flexible_demand_advance @@ -754,7 +852,7 @@ var_om_cost_per_mwh_in(r::FlexDemand) = get(r, :var_om_cost_per_mwh_in, default_ Returns the indices of all must-run resources in the vector `rs`. """ -must_run(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,MustRun), rs) +must_run(rs::Vector{T}) where {T <: AbstractResource} = findall(r -> isa(r, MustRun), rs) # VRE_STOR interface """ @@ -762,7 +860,7 @@ must_run(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,MustRun Returns the indices of all VRE_STOR resources in the vector `rs`. """ -vre_stor(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,VreStorage), rs) +vre_stor(rs::Vector{T}) where {T <: AbstractResource} = findall(r -> isa(r, VreStorage), rs) technology(r::VreStorage) = r.technology self_discharge(r::VreStorage) = r.self_disch @@ -771,154 +869,200 @@ self_discharge(r::VreStorage) = r.self_disch Returns the indices of all co-located solar resources in the vector `rs`. """ -solar(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,VreStorage) && r.solar != 0, rs) +solar(rs::Vector{T}) where {T <: AbstractResource} = findall(r -> isa(r, VreStorage) && + r.solar != 0, + rs) """ wind(rs::Vector{T}) where T <: AbstractResource Returns the indices of all co-located wind resources in the vector `rs`. """ -wind(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,VreStorage) && r.wind != 0, rs) +wind(rs::Vector{T}) where {T <: AbstractResource} = findall(r -> isa(r, VreStorage) && + r.wind != 0, + rs) """ storage_dc_discharge(rs::Vector{T}) where T <: AbstractResource Returns the indices of all co-located storage resources in the vector `rs` that discharge DC. """ -storage_dc_discharge(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,VreStorage) && r.stor_dc_discharge >= 1, rs) -storage_sym_dc_discharge(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,VreStorage) && r.stor_dc_discharge == 1, rs) -storage_asym_dc_discharge(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,VreStorage) && r.stor_dc_discharge == 2, rs) +storage_dc_discharge(rs::Vector{T}) where {T <: AbstractResource} = findall(r -> isa(r, + VreStorage) && r.stor_dc_discharge >= 1, + rs) +function storage_sym_dc_discharge(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> isa(r, VreStorage) && r.stor_dc_discharge == 1, rs) +end +function storage_asym_dc_discharge(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> isa(r, VreStorage) && r.stor_dc_discharge == 2, rs) +end """ storage_dc_charge(rs::Vector{T}) where T <: AbstractResource Returns the indices of all co-located storage resources in the vector `rs` that charge DC. """ -storage_dc_charge(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,VreStorage) && r.stor_dc_charge >= 1, rs) -storage_sym_dc_charge(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,VreStorage) && r.stor_dc_charge == 1, rs) -storage_asym_dc_charge(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,VreStorage) && r.stor_dc_charge == 2, rs) +storage_dc_charge(rs::Vector{T}) where {T <: AbstractResource} = findall(r -> isa(r, + VreStorage) && r.stor_dc_charge >= 1, + rs) +function storage_sym_dc_charge(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> isa(r, VreStorage) && r.stor_dc_charge == 1, rs) +end +function storage_asym_dc_charge(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> isa(r, VreStorage) && r.stor_dc_charge == 2, rs) +end """ storage_ac_discharge(rs::Vector{T}) where T <: AbstractResource Returns the indices of all co-located storage resources in the vector `rs` that discharge AC. """ -storage_ac_discharge(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,VreStorage) && r.stor_ac_discharge >= 1, rs) -storage_sym_ac_discharge(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,VreStorage) && r.stor_ac_discharge == 1, rs) -storage_asym_ac_discharge(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,VreStorage) && r.stor_ac_discharge == 2, rs) +storage_ac_discharge(rs::Vector{T}) where {T <: AbstractResource} = findall(r -> isa(r, + VreStorage) && r.stor_ac_discharge >= 1, + rs) +function storage_sym_ac_discharge(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> isa(r, VreStorage) && r.stor_ac_discharge == 1, rs) +end +function storage_asym_ac_discharge(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> isa(r, VreStorage) && r.stor_ac_discharge == 2, rs) +end """ storage_ac_charge(rs::Vector{T}) where T <: AbstractResource Returns the indices of all co-located storage resources in the vector `rs` that charge AC. """ -storage_ac_charge(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,VreStorage) && r.stor_ac_charge >= 1, rs) -storage_sym_ac_charge(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,VreStorage) && r.stor_ac_charge == 1, rs) -storage_asym_ac_charge(rs::Vector{T}) where T <: AbstractResource = findall(r -> isa(r,VreStorage) && r.stor_ac_charge == 2, rs) +storage_ac_charge(rs::Vector{T}) where {T <: AbstractResource} = findall(r -> isa(r, + VreStorage) && r.stor_ac_charge >= 1, + rs) +function storage_sym_ac_charge(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> isa(r, VreStorage) && r.stor_ac_charge == 1, rs) +end +function storage_asym_ac_charge(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> isa(r, VreStorage) && r.stor_ac_charge == 2, rs) +end -is_LDS_VRE_STOR(rs::Vector{T}) where T <: AbstractResource = findall(r -> get(r, :lds_vre_stor, default_zero) != 0, rs) +function is_LDS_VRE_STOR(rs::Vector{T}) where {T <: AbstractResource} + findall(r -> get(r, :lds_vre_stor, default_zero) != 0, rs) +end # loop over the above attributes and define function interfaces for each one -for attr in (:existing_cap_solar_mw, - :existing_cap_wind_mw, - :existing_cap_inverter_mw, - :existing_cap_charge_dc_mw, - :existing_cap_charge_ac_mw, - :existing_cap_discharge_dc_mw, - :existing_cap_discharge_ac_mw) +for attr in (:existing_cap_solar_mw, + :existing_cap_wind_mw, + :existing_cap_inverter_mw, + :existing_cap_charge_dc_mw, + :existing_cap_charge_ac_mw, + :existing_cap_discharge_dc_mw, + :existing_cap_discharge_ac_mw) @eval @interface $attr end -for attr in (:max_cap_solar_mw, - :max_cap_wind_mw, - :max_cap_inverter_mw, - :max_cap_charge_dc_mw, - :max_cap_charge_ac_mw, - :max_cap_discharge_dc_mw, - :max_cap_discharge_ac_mw, - :min_cap_solar_mw, - :min_cap_wind_mw, - :min_cap_inverter_mw, - :min_cap_charge_dc_mw, - :min_cap_charge_ac_mw, - :min_cap_discharge_dc_mw, - :min_cap_discharge_ac_mw, - :inverter_ratio_solar, - :inverter_ratio_wind,) +for attr in (:max_cap_solar_mw, + :max_cap_wind_mw, + :max_cap_inverter_mw, + :max_cap_charge_dc_mw, + :max_cap_charge_ac_mw, + :max_cap_discharge_dc_mw, + :max_cap_discharge_ac_mw, + :min_cap_solar_mw, + :min_cap_wind_mw, + :min_cap_inverter_mw, + :min_cap_charge_dc_mw, + :min_cap_charge_ac_mw, + :min_cap_discharge_dc_mw, + :min_cap_discharge_ac_mw, + :inverter_ratio_solar, + :inverter_ratio_wind) @eval @interface $attr default_minmax_cap end for attr in (:etainverter, - :inv_cost_inverter_per_mwyr, - :inv_cost_solar_per_mwyr, - :inv_cost_wind_per_mwyr, - :inv_cost_discharge_dc_per_mwyr, - :inv_cost_charge_dc_per_mwyr, - :inv_cost_discharge_ac_per_mwyr, - :inv_cost_charge_ac_per_mwyr, - :fixed_om_inverter_cost_per_mwyr, - :fixed_om_solar_cost_per_mwyr, - :fixed_om_wind_cost_per_mwyr, - :fixed_om_cost_discharge_dc_per_mwyr, - :fixed_om_cost_charge_dc_per_mwyr, - :fixed_om_cost_discharge_ac_per_mwyr, - :fixed_om_cost_charge_ac_per_mwyr, - :var_om_cost_per_mwh_solar, - :var_om_cost_per_mwh_wind, - :var_om_cost_per_mwh_charge_dc, - :var_om_cost_per_mwh_discharge_dc, - :var_om_cost_per_mwh_charge_ac, - :var_om_cost_per_mwh_discharge_ac, - :eff_up_ac, - :eff_down_ac, - :eff_up_dc, - :eff_down_dc, - :power_to_energy_ac, - :power_to_energy_dc) + :inv_cost_inverter_per_mwyr, + :inv_cost_solar_per_mwyr, + :inv_cost_wind_per_mwyr, + :inv_cost_discharge_dc_per_mwyr, + :inv_cost_charge_dc_per_mwyr, + :inv_cost_discharge_ac_per_mwyr, + :inv_cost_charge_ac_per_mwyr, + :fixed_om_inverter_cost_per_mwyr, + :fixed_om_solar_cost_per_mwyr, + :fixed_om_wind_cost_per_mwyr, + :fixed_om_cost_discharge_dc_per_mwyr, + :fixed_om_cost_charge_dc_per_mwyr, + :fixed_om_cost_discharge_ac_per_mwyr, + :fixed_om_cost_charge_ac_per_mwyr, + :var_om_cost_per_mwh_solar, + :var_om_cost_per_mwh_wind, + :var_om_cost_per_mwh_charge_dc, + :var_om_cost_per_mwh_discharge_dc, + :var_om_cost_per_mwh_charge_ac, + :var_om_cost_per_mwh_discharge_ac, + :eff_up_ac, + :eff_down_ac, + :eff_up_dc, + :eff_down_dc, + :power_to_energy_ac, + :power_to_energy_dc) @eval @interface $attr default_zero VreStorage end # Multistage for attr in (:capital_recovery_period_dc, - :capital_recovery_period_solar, - :capital_recovery_period_wind, - :capital_recovery_period_charge_dc, - :capital_recovery_period_discharge_dc, - :capital_recovery_period_charge_ac, - :capital_recovery_period_discharge_ac, - :tech_wacc_dc, - :tech_wacc_solar, - :tech_wacc_wind, - :tech_wacc_charge_dc, - :tech_wacc_discharge_dc, - :tech_wacc_charge_ac, - :tech_wacc_discharge_ac) + :capital_recovery_period_solar, + :capital_recovery_period_wind, + :capital_recovery_period_charge_dc, + :capital_recovery_period_discharge_dc, + :capital_recovery_period_charge_ac, + :capital_recovery_period_discharge_ac, + :tech_wacc_dc, + :tech_wacc_solar, + :tech_wacc_wind, + :tech_wacc_charge_dc, + :tech_wacc_discharge_dc, + :tech_wacc_charge_ac, + :tech_wacc_discharge_ac) @eval @interface $attr default_zero VreStorage end # Endogenous retirement -for attr in (:min_retired_cap_inverter_mw, - :min_retired_cap_solar_mw, - :min_retired_cap_wind_mw, - :min_retired_cap_discharge_dc_mw, - :min_retired_cap_charge_dc_mw, - :min_retired_cap_discharge_ac_mw, - :min_retired_cap_charge_ac_mw,) - @eval @interface $attr default_zero - cum_attr = Symbol("cum_"*String(attr)) - @eval @interface $cum_attr default_zero +for attr in (:min_retired_cap_inverter_mw, + :min_retired_cap_solar_mw, + :min_retired_cap_wind_mw, + :min_retired_cap_discharge_dc_mw, + :min_retired_cap_charge_dc_mw, + :min_retired_cap_discharge_ac_mw, + :min_retired_cap_charge_ac_mw) + @eval @interface $attr default_zero + cum_attr = Symbol("cum_" * String(attr)) + @eval @interface $cum_attr default_zero end ## policies # co-located storage -esr_vrestor(r::AbstractResource; tag::Int64) = get(r, Symbol("esr_vrestor_$tag"), default_zero) -min_cap_stor(r::AbstractResource; tag::Int64) = get(r, Symbol("min_cap_stor_$tag"), default_zero) -max_cap_stor(r::AbstractResource; tag::Int64) = get(r, Symbol("max_cap_stor_$tag"), default_zero) +function esr_vrestor(r::AbstractResource; tag::Int64) + get(r, Symbol("esr_vrestor_$tag"), default_zero) +end +function min_cap_stor(r::AbstractResource; tag::Int64) + get(r, Symbol("min_cap_stor_$tag"), default_zero) +end +function max_cap_stor(r::AbstractResource; tag::Int64) + get(r, Symbol("max_cap_stor_$tag"), default_zero) +end # vre part -min_cap_solar(r::AbstractResource; tag::Int64) = get(r, Symbol("min_cap_solar_$tag"), default_zero) -max_cap_solar(r::AbstractResource; tag::Int64) = get(r, Symbol("max_cap_solar_$tag"), default_zero) -min_cap_wind(r::AbstractResource; tag::Int64) = get(r, Symbol("min_cap_wind_$tag"), default_zero) -max_cap_wind(r::AbstractResource; tag::Int64) = get(r, Symbol("max_cap_wind_$tag"), default_zero) +function min_cap_solar(r::AbstractResource; tag::Int64) + get(r, Symbol("min_cap_solar_$tag"), default_zero) +end +function max_cap_solar(r::AbstractResource; tag::Int64) + get(r, Symbol("max_cap_solar_$tag"), default_zero) +end +function min_cap_wind(r::AbstractResource; tag::Int64) + get(r, Symbol("min_cap_wind_$tag"), default_zero) +end +function max_cap_wind(r::AbstractResource; tag::Int64) + get(r, Symbol("max_cap_wind_$tag"), default_zero) +end ## Utility functions for working with resources in_zone(r::AbstractResource, zone::Int) = zone_id(r) == zone -resources_in_zone(rs::Vector{<:AbstractResource}, zone::Int) = filter(r -> in_zone(r, zone), rs) +function resources_in_zone(rs::Vector{<:AbstractResource}, zone::Int) + filter(r -> in_zone(r, zone), rs) +end @doc raw""" resources_in_zone_by_rid(rs::Vector{<:AbstractResource}, zone::Int) @@ -940,7 +1084,8 @@ Find R_ID's of resources with retrofit cluster id `cluster_id`. # Returns - `Vector{Int64}`: The vector of resource ids in the retrofit cluster. """ -function resources_in_retrofit_cluster_by_rid(rs::Vector{<:AbstractResource}, cluster_id::String) +function resources_in_retrofit_cluster_by_rid(rs::Vector{<:AbstractResource}, + cluster_id::String) return resource_id.(rs[retrofit_id.(rs) .== cluster_id]) end @@ -959,7 +1104,8 @@ Find the resource with `name` in the vector `rs`. function resource_by_name(rs::Vector{<:AbstractResource}, name::AbstractString) r_id = findfirst(r -> resource_name(r) == name, rs) # check that the resource exists - isnothing(r_id) && error("Resource $name not found in resource data. \nHint: Make sure that the resource names in input files match the ones in the \"resource\" folder.\n") + isnothing(r_id) && + error("Resource $name not found in resource data. \nHint: Make sure that the resource names in input files match the ones in the \"resource\" folder.\n") return rs[r_id] end @@ -976,7 +1122,7 @@ function validate_boolean_attribute(r::AbstractResource, attr::Symbol) attr_value = get(r, attr, 0) if attr_value != 0 && attr_value != 1 error("Attribute $attr in resource $(resource_name(r)) must be boolean." * - "The only valid values are {0,1}, not $attr_value.") + "The only valid values are {0,1}, not $attr_value.") end end @@ -991,7 +1137,7 @@ Find the resource ids of the retrofit units in the vector `rs` where all retrofi # Returns - `Vector{Int64}`: The vector of resource ids. """ -function ids_with_all_options_contributing(rs::Vector{T}) where T <: AbstractResource +function ids_with_all_options_contributing(rs::Vector{T}) where {T <: AbstractResource} # select resources that can retrofit units_can_retrofit = ids_can_retrofit(rs) # check if all retrofit options in the retrofit cluster of each retrofit resource contribute to min retirement @@ -1011,10 +1157,13 @@ Check if all retrofit options in the retrofit cluster of the retrofit resource ` # Returns - `Bool`: True if all retrofit options contribute to min retirement, otherwise false. """ -function has_all_options_contributing(retrofit_res::AbstractResource, rs::Vector{T}) where T <: AbstractResource +function has_all_options_contributing(retrofit_res::AbstractResource, + rs::Vector{T}) where {T <: AbstractResource} retro_id = retrofit_id(retrofit_res) - return isempty(intersect(resources_in_retrofit_cluster_by_rid(rs, retro_id), ids_retrofit_options(rs), ids_not_contribute_min_retirement(rs))) -end + return isempty(intersect(resources_in_retrofit_cluster_by_rid(rs, retro_id), + ids_retrofit_options(rs), + ids_not_contribute_min_retirement(rs))) +end """ ids_with_all_options_not_contributing(rs::Vector{T}) where T <: AbstractResource @@ -1027,11 +1176,12 @@ Find the resource ids of the retrofit units in the vector `rs` where all retrofi # Returns - `Vector{Int64}`: The vector of resource ids. """ -function ids_with_all_options_not_contributing(rs::Vector{T}) where T <: AbstractResource +function ids_with_all_options_not_contributing(rs::Vector{T}) where {T <: AbstractResource} # select resources that can retrofit units_can_retrofit = ids_can_retrofit(rs) # check if all retrofit options in the retrofit cluster of each retrofit resource contribute to min retirement - condition::Vector{Bool} = has_all_options_not_contributing.(rs[units_can_retrofit], Ref(rs)) + condition::Vector{Bool} = has_all_options_not_contributing.(rs[units_can_retrofit], + Ref(rs)) return units_can_retrofit[condition] end @@ -1047,7 +1197,10 @@ Check if all retrofit options in the retrofit cluster of the retrofit resource ` # Returns - `Bool`: True if all retrofit options do not contribute to min retirement, otherwise false. """ -function has_all_options_not_contributing(retrofit_res::AbstractResource, rs::Vector{T}) where T <: AbstractResource +function has_all_options_not_contributing(retrofit_res::AbstractResource, + rs::Vector{T}) where {T <: AbstractResource} retro_id = retrofit_id(retrofit_res) - return isempty(intersect(resources_in_retrofit_cluster_by_rid(rs, retro_id), ids_retrofit_options(rs), ids_contribute_min_retirement(rs))) -end \ No newline at end of file + return isempty(intersect(resources_in_retrofit_cluster_by_rid(rs, retro_id), + ids_retrofit_options(rs), + ids_contribute_min_retirement(rs))) +end diff --git a/src/model/resources/retrofits/retrofits.jl b/src/model/resources/retrofits/retrofits.jl index d29d13fe33..6920dffd79 100644 --- a/src/model/resources/retrofits/retrofits.jl +++ b/src/model/resources/retrofits/retrofits.jl @@ -17,25 +17,40 @@ where ${RETROFIT}$ represents the set of all retrofit IDs (clusters) in the mode """ function retrofit(EP::Model, inputs::Dict) - - println("Retrofit Resources Module") - - gen = inputs["RESOURCES"] - - COMMIT = inputs["COMMIT"] # Set of all resources subject to unit commitment - RETROFIT_CAP = inputs["RETROFIT_CAP"] # Set of all resources being retrofitted - RETROFIT_OPTIONS = inputs["RETROFIT_OPTIONS"] # Set of all resources being created - RETROFIT_IDS = inputs["RETROFIT_IDS"] # Set of unique IDs for retrofit resources - - @expression(EP,eRetrofittedCapByRetroId[id in RETROFIT_IDS], - sum(cap_size(gen[y]) * EP[:vRETROFITCAP][y] for y in intersect(RETROFIT_CAP, COMMIT, resources_in_retrofit_cluster_by_rid(gen,id)); init=0) - + sum(EP[:vRETROFITCAP][y] for y in setdiff(intersect(RETROFIT_CAP, resources_in_retrofit_cluster_by_rid(gen,id)), COMMIT); init=0)) - - @expression(EP,eRetrofitCapByRetroId[id in RETROFIT_IDS], - sum(cap_size(gen[y]) * EP[:vCAP][y] * (1/retrofit_efficiency(gen[y])) for y in intersect(RETROFIT_OPTIONS, COMMIT, resources_in_retrofit_cluster_by_rid(gen,id)); init=0) - + sum(EP[:vCAP][y] * (1/retrofit_efficiency(gen[y])) for y in setdiff(intersect(RETROFIT_OPTIONS, resources_in_retrofit_cluster_by_rid(gen,id)), COMMIT); init=0)) - - @constraint(EP, cRetrofitCapacity[id in RETROFIT_IDS], eRetrofittedCapByRetroId[id] == eRetrofitCapByRetroId[id]) - - return EP + println("Retrofit Resources Module") + + gen = inputs["RESOURCES"] + + COMMIT = inputs["COMMIT"] # Set of all resources subject to unit commitment + RETROFIT_CAP = inputs["RETROFIT_CAP"] # Set of all resources being retrofitted + RETROFIT_OPTIONS = inputs["RETROFIT_OPTIONS"] # Set of all resources being created + RETROFIT_IDS = inputs["RETROFIT_IDS"] # Set of unique IDs for retrofit resources + + @expression(EP, eRetrofittedCapByRetroId[id in RETROFIT_IDS], + sum(cap_size(gen[y]) * EP[:vRETROFITCAP][y] for y in intersect(RETROFIT_CAP, + COMMIT, + resources_in_retrofit_cluster_by_rid(gen, id)); + init = 0) + +sum(EP[:vRETROFITCAP][y] for y in setdiff(intersect(RETROFIT_CAP, + resources_in_retrofit_cluster_by_rid(gen, id)), + COMMIT); + init = 0)) + + @expression(EP, eRetrofitCapByRetroId[id in RETROFIT_IDS], + sum(cap_size(gen[y]) * EP[:vCAP][y] * (1 / retrofit_efficiency(gen[y])) + for y in intersect(RETROFIT_OPTIONS, + COMMIT, + resources_in_retrofit_cluster_by_rid(gen, id)); + init = 0) + +sum(EP[:vCAP][y] * (1 / retrofit_efficiency(gen[y])) + for y in setdiff(intersect(RETROFIT_OPTIONS, + resources_in_retrofit_cluster_by_rid(gen, id)), + COMMIT); + init = 0)) + + @constraint(EP, + cRetrofitCapacity[id in RETROFIT_IDS], + eRetrofittedCapByRetroId[id]==eRetrofitCapByRetroId[id]) + + return EP end diff --git a/src/model/resources/storage/investment_charge.jl b/src/model/resources/storage/investment_charge.jl index 77f67f76bc..5f92ec684c 100644 --- a/src/model/resources/storage/investment_charge.jl +++ b/src/model/resources/storage/investment_charge.jl @@ -39,97 +39,105 @@ In addition, this function adds investment and fixed O&M related costs related t ``` """ function investment_charge!(EP::Model, inputs::Dict, setup::Dict) - - println("Charge Investment Module") - - gen = inputs["RESOURCES"] - - MultiStage = setup["MultiStage"] - - STOR_ASYMMETRIC = inputs["STOR_ASYMMETRIC"] # Set of storage resources with asymmetric (separte) charge/discharge capacity components - - NEW_CAP_CHARGE = inputs["NEW_CAP_CHARGE"] # Set of asymmetric charge/discharge storage resources eligible for new charge capacity - RET_CAP_CHARGE = inputs["RET_CAP_CHARGE"] # Set of asymmetric charge/discharge storage resources eligible for charge capacity retirements - - ### Variables ### - - ## Storage capacity built and retired for storage resources with independent charge and discharge power capacities (STOR=2) - - # New installed charge capacity of resource "y" - @variable(EP, vCAPCHARGE[y in NEW_CAP_CHARGE] >= 0) - - # Retired charge capacity of resource "y" from existing capacity - @variable(EP, vRETCAPCHARGE[y in RET_CAP_CHARGE] >= 0) - - if MultiStage == 1 - @variable(EP, vEXISTINGCAPCHARGE[y in STOR_ASYMMETRIC] >= 0); - end - - ### Expressions ### - - if MultiStage == 1 - @expression(EP, eExistingCapCharge[y in STOR_ASYMMETRIC], vEXISTINGCAPCHARGE[y]) - else - @expression(EP, eExistingCapCharge[y in STOR_ASYMMETRIC], existing_charge_cap_mw(gen[y])) - end - - @expression(EP, eTotalCapCharge[y in STOR_ASYMMETRIC], - if (y in intersect(NEW_CAP_CHARGE, RET_CAP_CHARGE)) - eExistingCapCharge[y] + EP[:vCAPCHARGE][y] - EP[:vRETCAPCHARGE][y] - elseif (y in setdiff(NEW_CAP_CHARGE, RET_CAP_CHARGE)) - eExistingCapCharge[y] + EP[:vCAPCHARGE][y] - elseif (y in setdiff(RET_CAP_CHARGE, NEW_CAP_CHARGE)) - eExistingCapCharge[y] - EP[:vRETCAPCHARGE][y] - else - eExistingCapCharge[y] + EP[:vZERO] - end - ) - - ## Objective Function Expressions ## - - # Fixed costs for resource "y" = annuitized investment cost plus fixed O&M costs - # If resource is not eligible for new charge capacity, fixed costs are only O&M costs - @expression(EP, eCFixCharge[y in STOR_ASYMMETRIC], - if y in NEW_CAP_CHARGE # Resources eligible for new charge capacity - inv_cost_charge_per_mwyr(gen[y])*vCAPCHARGE[y] + fixed_om_cost_charge_per_mwyr(gen[y])*eTotalCapCharge[y] - else - fixed_om_cost_charge_per_mwyr(gen[y])*eTotalCapCharge[y] - end - ) - - # Sum individual resource contributions to fixed costs to get total fixed costs - @expression(EP, eTotalCFixCharge, sum(EP[:eCFixCharge][y] for y in STOR_ASYMMETRIC)) - - # Add term to objective function expression - if MultiStage == 1 - # OPEX multiplier scales fixed costs to account for multiple years between two model stages - # We divide by OPEXMULT since we are going to multiply the entire objective function by this term later, - # and we have already accounted for multiple years between stages for fixed costs. - add_to_expression!(EP[:eObj], (1/inputs["OPEXMULT"]), eTotalCFixCharge) - else - add_to_expression!(EP[:eObj], eTotalCFixCharge) - end - - ### Constratints ### - - if MultiStage == 1 - # Existing capacity variable is equal to existing capacity specified in the input file - @constraint(EP, cExistingCapCharge[y in STOR_ASYMMETRIC], EP[:vEXISTINGCAPCHARGE][y] == existing_charge_cap_mw(gen[y])) - end - - ## Constraints on retirements and capacity additions - #Cannot retire more charge capacity than existing charge capacity - @constraint(EP, cMaxRetCharge[y in RET_CAP_CHARGE], vRETCAPCHARGE[y] <= eExistingCapCharge[y]) - - #Constraints on new built capacity - - # Constraint on maximum charge capacity (if applicable) [set input to -1 if no constraint on maximum charge capacity] - # DEV NOTE: This constraint may be violated in some cases where Existing_Charge_Cap_MW is >= Max_Charge_Cap_MWh and lead to infeasabilty - @constraint(EP, cMaxCapCharge[y in intersect(ids_with_positive(gen, max_charge_cap_mw), STOR_ASYMMETRIC)], eTotalCapCharge[y] <= max_charge_cap_mw(gen[y])) - - # Constraint on minimum charge capacity (if applicable) [set input to -1 if no constraint on minimum charge capacity] - # DEV NOTE: This constraint may be violated in some cases where Existing_Charge_Cap_MW is <= Min_Charge_Cap_MWh and lead to infeasabilty - @constraint(EP, cMinCapCharge[y in intersect(ids_with_positive(gen, min_charge_cap_mw), STOR_ASYMMETRIC)], eTotalCapCharge[y] >= min_charge_cap_mw(gen[y])) - - + println("Charge Investment Module") + + gen = inputs["RESOURCES"] + + MultiStage = setup["MultiStage"] + + STOR_ASYMMETRIC = inputs["STOR_ASYMMETRIC"] # Set of storage resources with asymmetric (separte) charge/discharge capacity components + + NEW_CAP_CHARGE = inputs["NEW_CAP_CHARGE"] # Set of asymmetric charge/discharge storage resources eligible for new charge capacity + RET_CAP_CHARGE = inputs["RET_CAP_CHARGE"] # Set of asymmetric charge/discharge storage resources eligible for charge capacity retirements + + ### Variables ### + + ## Storage capacity built and retired for storage resources with independent charge and discharge power capacities (STOR=2) + + # New installed charge capacity of resource "y" + @variable(EP, vCAPCHARGE[y in NEW_CAP_CHARGE]>=0) + + # Retired charge capacity of resource "y" from existing capacity + @variable(EP, vRETCAPCHARGE[y in RET_CAP_CHARGE]>=0) + + if MultiStage == 1 + @variable(EP, vEXISTINGCAPCHARGE[y in STOR_ASYMMETRIC]>=0) + end + + ### Expressions ### + + if MultiStage == 1 + @expression(EP, eExistingCapCharge[y in STOR_ASYMMETRIC], vEXISTINGCAPCHARGE[y]) + else + @expression(EP, + eExistingCapCharge[y in STOR_ASYMMETRIC], + existing_charge_cap_mw(gen[y])) + end + + @expression(EP, eTotalCapCharge[y in STOR_ASYMMETRIC], + if (y in intersect(NEW_CAP_CHARGE, RET_CAP_CHARGE)) + eExistingCapCharge[y] + EP[:vCAPCHARGE][y] - EP[:vRETCAPCHARGE][y] + elseif (y in setdiff(NEW_CAP_CHARGE, RET_CAP_CHARGE)) + eExistingCapCharge[y] + EP[:vCAPCHARGE][y] + elseif (y in setdiff(RET_CAP_CHARGE, NEW_CAP_CHARGE)) + eExistingCapCharge[y] - EP[:vRETCAPCHARGE][y] + else + eExistingCapCharge[y] + EP[:vZERO] + end) + + ## Objective Function Expressions ## + + # Fixed costs for resource "y" = annuitized investment cost plus fixed O&M costs + # If resource is not eligible for new charge capacity, fixed costs are only O&M costs + @expression(EP, eCFixCharge[y in STOR_ASYMMETRIC], + if y in NEW_CAP_CHARGE # Resources eligible for new charge capacity + inv_cost_charge_per_mwyr(gen[y]) * vCAPCHARGE[y] + + fixed_om_cost_charge_per_mwyr(gen[y]) * eTotalCapCharge[y] + else + fixed_om_cost_charge_per_mwyr(gen[y]) * eTotalCapCharge[y] + end) + + # Sum individual resource contributions to fixed costs to get total fixed costs + @expression(EP, eTotalCFixCharge, sum(EP[:eCFixCharge][y] for y in STOR_ASYMMETRIC)) + + # Add term to objective function expression + if MultiStage == 1 + # OPEX multiplier scales fixed costs to account for multiple years between two model stages + # We divide by OPEXMULT since we are going to multiply the entire objective function by this term later, + # and we have already accounted for multiple years between stages for fixed costs. + add_to_expression!(EP[:eObj], (1 / inputs["OPEXMULT"]), eTotalCFixCharge) + else + add_to_expression!(EP[:eObj], eTotalCFixCharge) + end + + ### Constratints ### + + if MultiStage == 1 + # Existing capacity variable is equal to existing capacity specified in the input file + @constraint(EP, + cExistingCapCharge[y in STOR_ASYMMETRIC], + EP[:vEXISTINGCAPCHARGE][y]==existing_charge_cap_mw(gen[y])) + end + + ## Constraints on retirements and capacity additions + #Cannot retire more charge capacity than existing charge capacity + @constraint(EP, + cMaxRetCharge[y in RET_CAP_CHARGE], + vRETCAPCHARGE[y]<=eExistingCapCharge[y]) + + #Constraints on new built capacity + + # Constraint on maximum charge capacity (if applicable) [set input to -1 if no constraint on maximum charge capacity] + # DEV NOTE: This constraint may be violated in some cases where Existing_Charge_Cap_MW is >= Max_Charge_Cap_MWh and lead to infeasabilty + @constraint(EP, + cMaxCapCharge[y in intersect(ids_with_positive(gen, max_charge_cap_mw), + STOR_ASYMMETRIC)], + eTotalCapCharge[y]<=max_charge_cap_mw(gen[y])) + + # Constraint on minimum charge capacity (if applicable) [set input to -1 if no constraint on minimum charge capacity] + # DEV NOTE: This constraint may be violated in some cases where Existing_Charge_Cap_MW is <= Min_Charge_Cap_MWh and lead to infeasabilty + @constraint(EP, + cMinCapCharge[y in intersect(ids_with_positive(gen, min_charge_cap_mw), + STOR_ASYMMETRIC)], + eTotalCapCharge[y]>=min_charge_cap_mw(gen[y])) end diff --git a/src/model/resources/storage/investment_energy.jl b/src/model/resources/storage/investment_energy.jl index 35757fca6b..af28ba15c2 100644 --- a/src/model/resources/storage/investment_energy.jl +++ b/src/model/resources/storage/investment_energy.jl @@ -42,97 +42,106 @@ In addition, this function adds investment and fixed O\&M related costs related ``` """ function investment_energy!(EP::Model, inputs::Dict, setup::Dict) - - println("Storage Investment Module") - - gen = inputs["RESOURCES"] - - MultiStage = setup["MultiStage"] - - STOR_ALL = inputs["STOR_ALL"] # Set of all storage resources - NEW_CAP_ENERGY = inputs["NEW_CAP_ENERGY"] # Set of all storage resources eligible for new energy capacity - RET_CAP_ENERGY = inputs["RET_CAP_ENERGY"] # Set of all storage resources eligible for energy capacity retirements - - ### Variables ### - - ## Energy storage reservoir capacity (MWh capacity) built/retired for storage with variable power to energy ratio (STOR=1 or STOR=2) - - # New installed energy capacity of resource "y" - @variable(EP, vCAPENERGY[y in NEW_CAP_ENERGY] >= 0) - - # Retired energy capacity of resource "y" from existing capacity - @variable(EP, vRETCAPENERGY[y in RET_CAP_ENERGY] >= 0) - - if MultiStage == 1 - @variable(EP, vEXISTINGCAPENERGY[y in STOR_ALL] >= 0); - end - - ### Expressions ### - - if MultiStage == 1 - @expression(EP, eExistingCapEnergy[y in STOR_ALL], vEXISTINGCAPENERGY[y]) - else - @expression(EP, eExistingCapEnergy[y in STOR_ALL], existing_cap_mwh(gen[y])) - end - - @expression(EP, eTotalCapEnergy[y in STOR_ALL], - if (y in intersect(NEW_CAP_ENERGY, RET_CAP_ENERGY)) - eExistingCapEnergy[y] + EP[:vCAPENERGY][y] - EP[:vRETCAPENERGY][y] - elseif (y in setdiff(NEW_CAP_ENERGY, RET_CAP_ENERGY)) - eExistingCapEnergy[y] + EP[:vCAPENERGY][y] - elseif (y in setdiff(RET_CAP_ENERGY, NEW_CAP_ENERGY)) - eExistingCapEnergy[y] - EP[:vRETCAPENERGY][y] - else - eExistingCapEnergy[y] + EP[:vZERO] - end - ) - - ## Objective Function Expressions ## - - # Fixed costs for resource "y" = annuitized investment cost plus fixed O&M costs - # If resource is not eligible for new energy capacity, fixed costs are only O&M costs - @expression(EP, eCFixEnergy[y in STOR_ALL], - if y in NEW_CAP_ENERGY # Resources eligible for new capacity - inv_cost_per_mwhyr(gen[y])*vCAPENERGY[y] + fixed_om_cost_per_mwhyr(gen[y])*eTotalCapEnergy[y] - else - fixed_om_cost_per_mwhyr(gen[y])*eTotalCapEnergy[y] - end - ) - - # Sum individual resource contributions to fixed costs to get total fixed costs - @expression(EP, eTotalCFixEnergy, sum(EP[:eCFixEnergy][y] for y in STOR_ALL)) - - # Add term to objective function expression - if MultiStage == 1 - # OPEX multiplier scales fixed costs to account for multiple years between two model stages - # We divide by OPEXMULT since we are going to multiply the entire objective function by this term later, - # and we have already accounted for multiple years between stages for fixed costs. - add_to_expression!(EP[:eObj], (1/inputs["OPEXMULT"]), eTotalCFixEnergy) - else - add_to_expression!(EP[:eObj], eTotalCFixEnergy) - end - - ### Constraints ### - - if MultiStage == 1 - @constraint(EP, cExistingCapEnergy[y in STOR_ALL], EP[:vEXISTINGCAPENERGY][y] == existing_cap_mwh(gen[y])) - end - - ## Constraints on retirements and capacity additions - # Cannot retire more energy capacity than existing energy capacity - @constraint(EP, cMaxRetEnergy[y in RET_CAP_ENERGY], vRETCAPENERGY[y] <= eExistingCapEnergy[y]) - - ## Constraints on new built energy capacity - # Constraint on maximum energy capacity (if applicable) [set input to -1 if no constraint on maximum energy capacity] - # DEV NOTE: This constraint may be violated in some cases where Existing_Cap_MWh is >= Max_Cap_MWh and lead to infeasabilty - @constraint(EP, cMaxCapEnergy[y in intersect(ids_with_positive(gen, max_cap_mwh), STOR_ALL)], eTotalCapEnergy[y] <= max_cap_mwh(gen[y])) - - # Constraint on minimum energy capacity (if applicable) [set input to -1 if no constraint on minimum energy apacity] - # DEV NOTE: This constraint may be violated in some cases where Existing_Cap_MWh is <= Min_Cap_MWh and lead to infeasabilty - @constraint(EP, cMinCapEnergy[y in intersect(ids_with_positive(gen, min_cap_mwh), STOR_ALL)], eTotalCapEnergy[y] >= min_cap_mwh(gen[y])) - - # Max and min constraints on energy storage capacity built (as proportion to discharge power capacity) - @constraint(EP, cMinCapEnergyDuration[y in STOR_ALL], EP[:eTotalCapEnergy][y] >= min_duration(gen[y]) * EP[:eTotalCap][y]) - @constraint(EP, cMaxCapEnergyDuration[y in STOR_ALL], EP[:eTotalCapEnergy][y] <= max_duration(gen[y]) * EP[:eTotalCap][y]) - + println("Storage Investment Module") + + gen = inputs["RESOURCES"] + + MultiStage = setup["MultiStage"] + + STOR_ALL = inputs["STOR_ALL"] # Set of all storage resources + NEW_CAP_ENERGY = inputs["NEW_CAP_ENERGY"] # Set of all storage resources eligible for new energy capacity + RET_CAP_ENERGY = inputs["RET_CAP_ENERGY"] # Set of all storage resources eligible for energy capacity retirements + + ### Variables ### + + ## Energy storage reservoir capacity (MWh capacity) built/retired for storage with variable power to energy ratio (STOR=1 or STOR=2) + + # New installed energy capacity of resource "y" + @variable(EP, vCAPENERGY[y in NEW_CAP_ENERGY]>=0) + + # Retired energy capacity of resource "y" from existing capacity + @variable(EP, vRETCAPENERGY[y in RET_CAP_ENERGY]>=0) + + if MultiStage == 1 + @variable(EP, vEXISTINGCAPENERGY[y in STOR_ALL]>=0) + end + + ### Expressions ### + + if MultiStage == 1 + @expression(EP, eExistingCapEnergy[y in STOR_ALL], vEXISTINGCAPENERGY[y]) + else + @expression(EP, eExistingCapEnergy[y in STOR_ALL], existing_cap_mwh(gen[y])) + end + + @expression(EP, eTotalCapEnergy[y in STOR_ALL], + if (y in intersect(NEW_CAP_ENERGY, RET_CAP_ENERGY)) + eExistingCapEnergy[y] + EP[:vCAPENERGY][y] - EP[:vRETCAPENERGY][y] + elseif (y in setdiff(NEW_CAP_ENERGY, RET_CAP_ENERGY)) + eExistingCapEnergy[y] + EP[:vCAPENERGY][y] + elseif (y in setdiff(RET_CAP_ENERGY, NEW_CAP_ENERGY)) + eExistingCapEnergy[y] - EP[:vRETCAPENERGY][y] + else + eExistingCapEnergy[y] + EP[:vZERO] + end) + + ## Objective Function Expressions ## + + # Fixed costs for resource "y" = annuitized investment cost plus fixed O&M costs + # If resource is not eligible for new energy capacity, fixed costs are only O&M costs + @expression(EP, eCFixEnergy[y in STOR_ALL], + if y in NEW_CAP_ENERGY # Resources eligible for new capacity + inv_cost_per_mwhyr(gen[y]) * vCAPENERGY[y] + + fixed_om_cost_per_mwhyr(gen[y]) * eTotalCapEnergy[y] + else + fixed_om_cost_per_mwhyr(gen[y]) * eTotalCapEnergy[y] + end) + + # Sum individual resource contributions to fixed costs to get total fixed costs + @expression(EP, eTotalCFixEnergy, sum(EP[:eCFixEnergy][y] for y in STOR_ALL)) + + # Add term to objective function expression + if MultiStage == 1 + # OPEX multiplier scales fixed costs to account for multiple years between two model stages + # We divide by OPEXMULT since we are going to multiply the entire objective function by this term later, + # and we have already accounted for multiple years between stages for fixed costs. + add_to_expression!(EP[:eObj], (1 / inputs["OPEXMULT"]), eTotalCFixEnergy) + else + add_to_expression!(EP[:eObj], eTotalCFixEnergy) + end + + ### Constraints ### + + if MultiStage == 1 + @constraint(EP, + cExistingCapEnergy[y in STOR_ALL], + EP[:vEXISTINGCAPENERGY][y]==existing_cap_mwh(gen[y])) + end + + ## Constraints on retirements and capacity additions + # Cannot retire more energy capacity than existing energy capacity + @constraint(EP, + cMaxRetEnergy[y in RET_CAP_ENERGY], + vRETCAPENERGY[y]<=eExistingCapEnergy[y]) + + ## Constraints on new built energy capacity + # Constraint on maximum energy capacity (if applicable) [set input to -1 if no constraint on maximum energy capacity] + # DEV NOTE: This constraint may be violated in some cases where Existing_Cap_MWh is >= Max_Cap_MWh and lead to infeasabilty + @constraint(EP, + cMaxCapEnergy[y in intersect(ids_with_positive(gen, max_cap_mwh), STOR_ALL)], + eTotalCapEnergy[y]<=max_cap_mwh(gen[y])) + + # Constraint on minimum energy capacity (if applicable) [set input to -1 if no constraint on minimum energy apacity] + # DEV NOTE: This constraint may be violated in some cases where Existing_Cap_MWh is <= Min_Cap_MWh and lead to infeasabilty + @constraint(EP, + cMinCapEnergy[y in intersect(ids_with_positive(gen, min_cap_mwh), STOR_ALL)], + eTotalCapEnergy[y]>=min_cap_mwh(gen[y])) + + # Max and min constraints on energy storage capacity built (as proportion to discharge power capacity) + @constraint(EP, + cMinCapEnergyDuration[y in STOR_ALL], + EP[:eTotalCapEnergy][y]>=min_duration(gen[y]) * EP[:eTotalCap][y]) + @constraint(EP, + cMaxCapEnergyDuration[y in STOR_ALL], + EP[:eTotalCapEnergy][y]<=max_duration(gen[y]) * EP[:eTotalCap][y]) end diff --git a/src/model/resources/storage/long_duration_storage.jl b/src/model/resources/storage/long_duration_storage.jl index f88b22b0f6..1708332784 100644 --- a/src/model/resources/storage/long_duration_storage.jl +++ b/src/model/resources/storage/long_duration_storage.jl @@ -58,92 +58,119 @@ If the capacity reserve margin constraint is enabled, a similar set of constrain All other constraints are identical to those used to track the actual state of charge, except with the new variables $Q^{CRM}_{o,z,n}$ and $\Delta Q^{CRM}_{o,z,n}$ used in place of $Q_{o,z,n}$ and $\Delta Q_{o,z,n}$, respectively. """ function long_duration_storage!(EP::Model, inputs::Dict, setup::Dict) - - println("Long Duration Storage Module") - - gen = inputs["RESOURCES"] - - CapacityReserveMargin = setup["CapacityReserveMargin"] - - REP_PERIOD = inputs["REP_PERIOD"] # Number of representative periods - - STOR_LONG_DURATION = inputs["STOR_LONG_DURATION"] - - hours_per_subperiod = inputs["hours_per_subperiod"] #total number of hours per subperiod - - dfPeriodMap = inputs["Period_Map"] # Dataframe that maps modeled periods to representative periods - NPeriods = size(inputs["Period_Map"])[1] # Number of modeled periods - - MODELED_PERIODS_INDEX = 1:NPeriods - REP_PERIODS_INDEX = MODELED_PERIODS_INDEX[dfPeriodMap[!,:Rep_Period] .== MODELED_PERIODS_INDEX] - - ### Variables ### - - # Variables to define inter-period energy transferred between modeled periods - - # State of charge of storage at beginning of each modeled period n - @variable(EP, vSOCw[y in STOR_LONG_DURATION, n in MODELED_PERIODS_INDEX] >= 0) - - # Build up in storage inventory over each representative period w - # Build up inventory can be positive or negative - @variable(EP, vdSOC[y in STOR_LONG_DURATION, w=1:REP_PERIOD]) - - if CapacityReserveMargin > 0 - # State of charge held in reserve for storage at beginning of each modeled period n - @variable(EP, vCAPRES_socw[y in STOR_LONG_DURATION, n in MODELED_PERIODS_INDEX] >= 0) - - # Build up in storage inventory held in reserve over each representative period w - # Build up inventory can be positive or negative - @variable(EP, vCAPRES_dsoc[y in STOR_LONG_DURATION, w=1:REP_PERIOD]) - end - - ### Constraints ### - - # Links last time step with first time step, ensuring position in hour 1 is within eligible change from final hour position - # Modified initial state of storage for long-duration storage - initialize wth value carried over from last period - # Alternative to cSoCBalStart constraint which is included when not modeling operations wrapping and long duration storage - # Note: tw_min = hours_per_subperiod*(w-1)+1; tw_max = hours_per_subperiod*w - @constraint(EP, cSoCBalLongDurationStorageStart[w=1:REP_PERIOD, y in STOR_LONG_DURATION], - EP[:vS][y,hours_per_subperiod*(w-1)+1] == (1-self_discharge(gen[y]))*(EP[:vS][y,hours_per_subperiod*w]-vdSOC[y,w]) - -(1/efficiency_down(gen[y])*EP[:vP][y,hours_per_subperiod*(w-1)+1])+(efficiency_up(gen[y])*EP[:vCHARGE][y,hours_per_subperiod*(w-1)+1])) - - # Storage at beginning of period w = storage at beginning of period w-1 + storage built up in period w (after n representative periods) - ## Multiply storage build up term from prior period with corresponding weight - @constraint(EP, cSoCBalLongDurationStorage[y in STOR_LONG_DURATION, r in MODELED_PERIODS_INDEX], - vSOCw[y, mod1(r+1, NPeriods)] == vSOCw[y,r] + vdSOC[y,dfPeriodMap[r,:Rep_Period_Index]]) - - # Storage at beginning of each modeled period cannot exceed installed energy capacity - @constraint(EP, cSoCBalLongDurationStorageUpper[y in STOR_LONG_DURATION, r in MODELED_PERIODS_INDEX], - vSOCw[y,r] <= EP[:eTotalCapEnergy][y]) - - # Initial storage level for representative periods must also adhere to sub-period storage inventory balance - # Initial storage = Final storage - change in storage inventory across representative period - @constraint(EP, cSoCBalLongDurationStorageSub[y in STOR_LONG_DURATION, r in REP_PERIODS_INDEX], - vSOCw[y,r] == EP[:vS][y,hours_per_subperiod*dfPeriodMap[r,:Rep_Period_Index]] - vdSOC[y,dfPeriodMap[r,:Rep_Period_Index]]) - - # Capacity Reserve Margin policy - if CapacityReserveMargin > 0 - # LDES Constraints for storage held in reserve - - # Links last time step with first time step, ensuring position in hour 1 is within eligible change from final hour position - # Modified initial virtual state of storage for long-duration storage - initialize wth value carried over from last period - # Alternative to cVSoCBalStart constraint which is included when not modeling operations wrapping and long duration storage - # Note: tw_min = hours_per_subperiod*(w-1)+1; tw_max = hours_per_subperiod*w - @constraint(EP, cVSoCBalLongDurationStorageStart[w=1:REP_PERIOD, y in STOR_LONG_DURATION], - EP[:vCAPRES_socinreserve][y,hours_per_subperiod*(w-1)+1] == (1-self_discharge(gen[y]))*(EP[:vCAPRES_socinreserve][y,hours_per_subperiod*w]-vCAPRES_dsoc[y,w]) - +(1/efficiency_down(gen[y])*EP[:vCAPRES_discharge][y,hours_per_subperiod*(w-1)+1])-(efficiency_up(gen[y])*EP[:vCAPRES_charge][y,hours_per_subperiod*(w-1)+1])) - - # Storage held in reserve at beginning of period w = storage at beginning of period w-1 + storage built up in period w (after n representative periods) - ## Multiply storage build up term from prior period with corresponding weight - @constraint(EP, cVSoCBalLongDurationStorage[y in STOR_LONG_DURATION, r in MODELED_PERIODS_INDEX], - vCAPRES_socw[y,mod1(r+1, NPeriods)] == vCAPRES_socw[y,r] + vCAPRES_dsoc[y,dfPeriodMap[r,:Rep_Period_Index]]) - - # Initial reserve storage level for representative periods must also adhere to sub-period storage inventory balance - # Initial storage = Final storage - change in storage inventory across representative period - @constraint(EP, cVSoCBalLongDurationStorageSub[y in STOR_LONG_DURATION, r in REP_PERIODS_INDEX], - vCAPRES_socw[y,r] == EP[:vCAPRES_socinreserve][y,hours_per_subperiod*dfPeriodMap[r,:Rep_Period_Index]] - vCAPRES_dsoc[y,dfPeriodMap[r,:Rep_Period_Index]]) - - # energy held in reserve at the beginning of each modeled period acts as a lower bound on the total energy held in storage - @constraint(EP, cSOCMinCapResLongDurationStorage[y in STOR_LONG_DURATION, r in MODELED_PERIODS_INDEX], vSOCw[y,r] >= vCAPRES_socw[y,r]) - end + println("Long Duration Storage Module") + + gen = inputs["RESOURCES"] + + CapacityReserveMargin = setup["CapacityReserveMargin"] + + REP_PERIOD = inputs["REP_PERIOD"] # Number of representative periods + + STOR_LONG_DURATION = inputs["STOR_LONG_DURATION"] + + hours_per_subperiod = inputs["hours_per_subperiod"] #total number of hours per subperiod + + dfPeriodMap = inputs["Period_Map"] # Dataframe that maps modeled periods to representative periods + NPeriods = size(inputs["Period_Map"])[1] # Number of modeled periods + + MODELED_PERIODS_INDEX = 1:NPeriods + REP_PERIODS_INDEX = MODELED_PERIODS_INDEX[dfPeriodMap[!, :Rep_Period] .== MODELED_PERIODS_INDEX] + + ### Variables ### + + # Variables to define inter-period energy transferred between modeled periods + + # State of charge of storage at beginning of each modeled period n + @variable(EP, vSOCw[y in STOR_LONG_DURATION, n in MODELED_PERIODS_INDEX]>=0) + + # Build up in storage inventory over each representative period w + # Build up inventory can be positive or negative + @variable(EP, vdSOC[y in STOR_LONG_DURATION, w = 1:REP_PERIOD]) + + if CapacityReserveMargin > 0 + # State of charge held in reserve for storage at beginning of each modeled period n + @variable(EP, vCAPRES_socw[y in STOR_LONG_DURATION, n in MODELED_PERIODS_INDEX]>=0) + + # Build up in storage inventory held in reserve over each representative period w + # Build up inventory can be positive or negative + @variable(EP, vCAPRES_dsoc[y in STOR_LONG_DURATION, w = 1:REP_PERIOD]) + end + + ### Constraints ### + + # Links last time step with first time step, ensuring position in hour 1 is within eligible change from final hour position + # Modified initial state of storage for long-duration storage - initialize wth value carried over from last period + # Alternative to cSoCBalStart constraint which is included when not modeling operations wrapping and long duration storage + # Note: tw_min = hours_per_subperiod*(w-1)+1; tw_max = hours_per_subperiod*w + @constraint(EP, + cSoCBalLongDurationStorageStart[w = 1:REP_PERIOD, y in STOR_LONG_DURATION], + EP[:vS][y, + hours_per_subperiod * (w - 1) + 1]==(1 - self_discharge(gen[y])) * + (EP[:vS][y, hours_per_subperiod * w] - vdSOC[y, w]) + - + (1 / efficiency_down(gen[y]) * EP[:vP][y, hours_per_subperiod * (w - 1) + 1]) + + (efficiency_up(gen[y]) * EP[:vCHARGE][y, hours_per_subperiod * (w - 1) + 1])) + + # Storage at beginning of period w = storage at beginning of period w-1 + storage built up in period w (after n representative periods) + ## Multiply storage build up term from prior period with corresponding weight + @constraint(EP, + cSoCBalLongDurationStorage[y in STOR_LONG_DURATION, r in MODELED_PERIODS_INDEX], + vSOCw[y, + mod1(r + 1, NPeriods)]==vSOCw[y, r] + vdSOC[y, dfPeriodMap[r, :Rep_Period_Index]]) + + # Storage at beginning of each modeled period cannot exceed installed energy capacity + @constraint(EP, + cSoCBalLongDurationStorageUpper[y in STOR_LONG_DURATION, + r in MODELED_PERIODS_INDEX], + vSOCw[y, r]<=EP[:eTotalCapEnergy][y]) + + # Initial storage level for representative periods must also adhere to sub-period storage inventory balance + # Initial storage = Final storage - change in storage inventory across representative period + @constraint(EP, + cSoCBalLongDurationStorageSub[y in STOR_LONG_DURATION, r in REP_PERIODS_INDEX], + vSOCw[y, + r]==EP[:vS][y, hours_per_subperiod * dfPeriodMap[r, :Rep_Period_Index]] - + vdSOC[y, dfPeriodMap[r, :Rep_Period_Index]]) + + # Capacity Reserve Margin policy + if CapacityReserveMargin > 0 + # LDES Constraints for storage held in reserve + + # Links last time step with first time step, ensuring position in hour 1 is within eligible change from final hour position + # Modified initial virtual state of storage for long-duration storage - initialize wth value carried over from last period + # Alternative to cVSoCBalStart constraint which is included when not modeling operations wrapping and long duration storage + # Note: tw_min = hours_per_subperiod*(w-1)+1; tw_max = hours_per_subperiod*w + @constraint(EP, + cVSoCBalLongDurationStorageStart[w = 1:REP_PERIOD, y in STOR_LONG_DURATION], + EP[:vCAPRES_socinreserve][y, + hours_per_subperiod * (w - 1) + 1]==(1 - self_discharge(gen[y])) * + (EP[:vCAPRES_socinreserve][y, hours_per_subperiod * w] - vCAPRES_dsoc[y, w]) + + + (1 / efficiency_down(gen[y]) * + EP[:vCAPRES_discharge][y, hours_per_subperiod * (w - 1) + 1]) - + (efficiency_up(gen[y]) * + EP[:vCAPRES_charge][y, hours_per_subperiod * (w - 1) + 1])) + + # Storage held in reserve at beginning of period w = storage at beginning of period w-1 + storage built up in period w (after n representative periods) + ## Multiply storage build up term from prior period with corresponding weight + @constraint(EP, + cVSoCBalLongDurationStorage[y in STOR_LONG_DURATION, + r in MODELED_PERIODS_INDEX], + vCAPRES_socw[y, + mod1(r + 1, NPeriods)]==vCAPRES_socw[y, r] + vCAPRES_dsoc[y, dfPeriodMap[r, :Rep_Period_Index]]) + + # Initial reserve storage level for representative periods must also adhere to sub-period storage inventory balance + # Initial storage = Final storage - change in storage inventory across representative period + @constraint(EP, + cVSoCBalLongDurationStorageSub[y in STOR_LONG_DURATION, r in REP_PERIODS_INDEX], + vCAPRES_socw[y, + r]==EP[:vCAPRES_socinreserve][y, + hours_per_subperiod * dfPeriodMap[r, :Rep_Period_Index]] - vCAPRES_dsoc[y, dfPeriodMap[r, :Rep_Period_Index]]) + + # energy held in reserve at the beginning of each modeled period acts as a lower bound on the total energy held in storage + @constraint(EP, + cSOCMinCapResLongDurationStorage[y in STOR_LONG_DURATION, + r in MODELED_PERIODS_INDEX], + vSOCw[y, r]>=vCAPRES_socw[y, r]) + end end diff --git a/src/model/resources/storage/storage.jl b/src/model/resources/storage/storage.jl index baa03217aa..ed6c0630ce 100644 --- a/src/model/resources/storage/storage.jl +++ b/src/model/resources/storage/storage.jl @@ -129,55 +129,65 @@ Finally, the constraints on maximum discharge rate are replaced by the following The above reserve related constraints are established by ```storage_all_operational_reserves!()``` in ```storage_all.jl``` """ function storage!(EP::Model, inputs::Dict, setup::Dict) + println("Storage Resources Module") + gen = inputs["RESOURCES"] + T = inputs["T"] + STOR_ALL = inputs["STOR_ALL"] - println("Storage Resources Module") - gen = inputs["RESOURCES"] - T = inputs["T"] - STOR_ALL = inputs["STOR_ALL"] - - p = inputs["hours_per_subperiod"] + p = inputs["hours_per_subperiod"] rep_periods = inputs["REP_PERIOD"] - EnergyShareRequirement = setup["EnergyShareRequirement"] - CapacityReserveMargin = setup["CapacityReserveMargin"] - IncludeLossesInESR = setup["IncludeLossesInESR"] - StorageVirtualDischarge = setup["StorageVirtualDischarge"] - - if !isempty(STOR_ALL) - investment_energy!(EP, inputs, setup) - storage_all!(EP, inputs, setup) + EnergyShareRequirement = setup["EnergyShareRequirement"] + CapacityReserveMargin = setup["CapacityReserveMargin"] + IncludeLossesInESR = setup["IncludeLossesInESR"] + StorageVirtualDischarge = setup["StorageVirtualDischarge"] - # Include Long Duration Storage only when modeling representative periods and long-duration storage - if rep_periods > 1 && !isempty(inputs["STOR_LONG_DURATION"]) - long_duration_storage!(EP, inputs, setup) - end - end + if !isempty(STOR_ALL) + investment_energy!(EP, inputs, setup) + storage_all!(EP, inputs, setup) - if !isempty(inputs["STOR_ASYMMETRIC"]) - investment_charge!(EP, inputs, setup) - storage_asymmetric!(EP, inputs, setup) - end + # Include Long Duration Storage only when modeling representative periods and long-duration storage + if rep_periods > 1 && !isempty(inputs["STOR_LONG_DURATION"]) + long_duration_storage!(EP, inputs, setup) + end + end - if !isempty(inputs["STOR_SYMMETRIC"]) - storage_symmetric!(EP, inputs, setup) - end + if !isempty(inputs["STOR_ASYMMETRIC"]) + investment_charge!(EP, inputs, setup) + storage_asymmetric!(EP, inputs, setup) + end - # ESR Lossses - if EnergyShareRequirement >= 1 - if IncludeLossesInESR == 1 - @expression(EP, eESRStor[ESR=1:inputs["nESR"]], sum(inputs["dfESR"][z,ESR]*sum(EP[:eELOSS][y] for y in intersect(resources_in_zone_by_rid(gen,z),STOR_ALL)) for z=findall(x->x>0,inputs["dfESR"][:,ESR]))) - add_similar_to_expression!(EP[:eESR], -eESRStor) - end - end + if !isempty(inputs["STOR_SYMMETRIC"]) + storage_symmetric!(EP, inputs, setup) + end - # Capacity Reserves Margin policy - if CapacityReserveMargin > 0 - @expression(EP, eCapResMarBalanceStor[res=1:inputs["NCapacityReserveMargin"], t=1:T], sum(derating_factor(gen[y], tag=res) * (EP[:vP][y,t] - EP[:vCHARGE][y,t]) for y in STOR_ALL)) - if StorageVirtualDischarge > 0 - @expression(EP, eCapResMarBalanceStorVirtual[res=1:inputs["NCapacityReserveMargin"], t=1:T], sum(derating_factor(gen[y], tag=res) * (EP[:vCAPRES_discharge][y,t] - EP[:vCAPRES_charge][y,t]) for y in STOR_ALL)) - add_similar_to_expression!(eCapResMarBalanceStor,eCapResMarBalanceStorVirtual) - end - add_similar_to_expression!(EP[:eCapResMarBalance], eCapResMarBalanceStor) - end + # ESR Lossses + if EnergyShareRequirement >= 1 + if IncludeLossesInESR == 1 + @expression(EP, + eESRStor[ESR = 1:inputs["nESR"]], + sum(inputs["dfESR"][z, ESR] * sum(EP[:eELOSS][y] + for y in intersect(resources_in_zone_by_rid(gen, z), STOR_ALL)) + for z in findall(x -> x > 0, inputs["dfESR"][:, ESR]))) + add_similar_to_expression!(EP[:eESR], -eESRStor) + end + end + # Capacity Reserves Margin policy + if CapacityReserveMargin > 0 + @expression(EP, + eCapResMarBalanceStor[res = 1:inputs["NCapacityReserveMargin"], t = 1:T], + sum(derating_factor(gen[y], tag = res) * (EP[:vP][y, t] - EP[:vCHARGE][y, t]) + for y in STOR_ALL)) + if StorageVirtualDischarge > 0 + @expression(EP, + eCapResMarBalanceStorVirtual[res = 1:inputs["NCapacityReserveMargin"], + t = 1:T], + sum(derating_factor(gen[y], tag = res) * + (EP[:vCAPRES_discharge][y, t] - EP[:vCAPRES_charge][y, t]) + for y in STOR_ALL)) + add_similar_to_expression!(eCapResMarBalanceStor, eCapResMarBalanceStorVirtual) + end + add_similar_to_expression!(EP[:eCapResMarBalance], eCapResMarBalanceStor) + end end diff --git a/src/model/resources/storage/storage_all.jl b/src/model/resources/storage/storage_all.jl index 13c433235c..cc002c78f5 100644 --- a/src/model/resources/storage/storage_all.jl +++ b/src/model/resources/storage/storage_all.jl @@ -4,155 +4,203 @@ Sets up variables and constraints common to all storage resources. See ```storage()``` in ```storage.jl``` for description of constraints. """ function storage_all!(EP::Model, inputs::Dict, setup::Dict) - # Setup variables, constraints, and expressions common to all storage resources - println("Storage Core Resources Module") + # Setup variables, constraints, and expressions common to all storage resources + println("Storage Core Resources Module") - gen = inputs["RESOURCES"] - OperationalReserves = setup["OperationalReserves"] - CapacityReserveMargin = setup["CapacityReserveMargin"] + gen = inputs["RESOURCES"] + OperationalReserves = setup["OperationalReserves"] + CapacityReserveMargin = setup["CapacityReserveMargin"] + + virtual_discharge_cost = inputs["VirtualChargeDischargeCost"] + + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones + + STOR_ALL = inputs["STOR_ALL"] + STOR_SHORT_DURATION = inputs["STOR_SHORT_DURATION"] + representative_periods = inputs["REP_PERIOD"] + + START_SUBPERIODS = inputs["START_SUBPERIODS"] + INTERIOR_SUBPERIODS = inputs["INTERIOR_SUBPERIODS"] - virtual_discharge_cost = inputs["VirtualChargeDischargeCost"] + hours_per_subperiod = inputs["hours_per_subperiod"] #total number of hours per subperiod - T = inputs["T"] # Number of time steps (hours) - Z = inputs["Z"] # Number of zones + ### Variables ### - STOR_ALL = inputs["STOR_ALL"] - STOR_SHORT_DURATION = inputs["STOR_SHORT_DURATION"] - representative_periods = inputs["REP_PERIOD"] + # Storage level of resource "y" at hour "t" [MWh] on zone "z" - unbounded + @variable(EP, vS[y in STOR_ALL, t = 1:T]>=0) - START_SUBPERIODS = inputs["START_SUBPERIODS"] - INTERIOR_SUBPERIODS = inputs["INTERIOR_SUBPERIODS"] + # Energy withdrawn from grid by resource "y" at hour "t" [MWh] on zone "z" + @variable(EP, vCHARGE[y in STOR_ALL, t = 1:T]>=0) - hours_per_subperiod = inputs["hours_per_subperiod"] #total number of hours per subperiod - - ### Variables ### - - # Storage level of resource "y" at hour "t" [MWh] on zone "z" - unbounded - @variable(EP, vS[y in STOR_ALL, t=1:T] >= 0); - - # Energy withdrawn from grid by resource "y" at hour "t" [MWh] on zone "z" - @variable(EP, vCHARGE[y in STOR_ALL, t=1:T] >= 0); - - if CapacityReserveMargin > 0 - # Virtual discharge contributing to capacity reserves at timestep t for storage cluster y - @variable(EP, vCAPRES_discharge[y in STOR_ALL, t=1:T] >= 0) - - # Virtual charge contributing to capacity reserves at timestep t for storage cluster y - @variable(EP, vCAPRES_charge[y in STOR_ALL, t=1:T] >= 0) - - # Total state of charge being held in reserve at timestep t for storage cluster y - @variable(EP, vCAPRES_socinreserve[y in STOR_ALL, t=1:T] >= 0) - end - - ### Expressions ### - - # Energy losses related to technologies (increase in effective demand) - @expression(EP, eELOSS[y in STOR_ALL], sum(inputs["omega"][t]*EP[:vCHARGE][y,t] for t in 1:T) - sum(inputs["omega"][t]*EP[:vP][y,t] for t in 1:T)) - - ## Objective Function Expressions ## - - #Variable costs of "charging" for technologies "y" during hour "t" in zone "z" - @expression(EP, eCVar_in[y in STOR_ALL,t=1:T], inputs["omega"][t]*var_om_cost_per_mwh_in(gen[y])*vCHARGE[y,t]) - - # Sum individual resource contributions to variable charging costs to get total variable charging costs - @expression(EP, eTotalCVarInT[t=1:T], sum(eCVar_in[y,t] for y in STOR_ALL)) - @expression(EP, eTotalCVarIn, sum(eTotalCVarInT[t] for t in 1:T)) - add_to_expression!(EP[:eObj], eTotalCVarIn) - - if CapacityReserveMargin > 0 - #Variable costs of "virtual charging" for technologies "y" during hour "t" in zone "z" - @expression(EP, eCVar_in_virtual[y in STOR_ALL,t=1:T], inputs["omega"][t]*virtual_discharge_cost*vCAPRES_charge[y,t]) - @expression(EP, eTotalCVarInT_virtual[t=1:T], sum(eCVar_in_virtual[y,t] for y in STOR_ALL)) - @expression(EP, eTotalCVarIn_virtual, sum(eTotalCVarInT_virtual[t] for t in 1:T)) - EP[:eObj] += eTotalCVarIn_virtual - - #Variable costs of "virtual discharging" for technologies "y" during hour "t" in zone "z" - @expression(EP, eCVar_out_virtual[y in STOR_ALL,t=1:T], inputs["omega"][t]*virtual_discharge_cost*vCAPRES_discharge[y,t]) - @expression(EP, eTotalCVarOutT_virtual[t=1:T], sum(eCVar_out_virtual[y,t] for y in STOR_ALL)) - @expression(EP, eTotalCVarOut_virtual, sum(eTotalCVarOutT_virtual[t] for t in 1:T)) - EP[:eObj] += eTotalCVarOut_virtual - end - - ## Power Balance Expressions ## - - # Term to represent net dispatch from storage in any period - @expression(EP, ePowerBalanceStor[t=1:T, z=1:Z], - sum(EP[:vP][y,t]-EP[:vCHARGE][y,t] for y in intersect(resources_in_zone_by_rid(gen,z),STOR_ALL)) - ) - add_similar_to_expression!(EP[:ePowerBalance], ePowerBalanceStor) - - ### Constraints ### - - ## Storage energy capacity and state of charge related constraints: - - # Links state of charge in first time step with decisions in last time step of each subperiod - # We use a modified formulation of this constraint (cSoCBalLongDurationStorageStart) when operations wrapping and long duration storage are being modeled - if representative_periods > 1 && !isempty(inputs["STOR_LONG_DURATION"]) - CONSTRAINTSET = STOR_SHORT_DURATION - else - CONSTRAINTSET = STOR_ALL - end - @constraint(EP, cSoCBalStart[t in START_SUBPERIODS, y in CONSTRAINTSET], EP[:vS][y,t] == - EP[:vS][y,t+hours_per_subperiod-1] - (1/efficiency_down(gen[y]) * EP[:vP][y,t]) - + (efficiency_up(gen[y])*EP[:vCHARGE][y,t]) - (self_discharge(gen[y]) * EP[:vS][y,t+hours_per_subperiod-1])) - - @constraints(EP, begin - - # Maximum energy stored must be less than energy capacity - [y in STOR_ALL, t in 1:T], EP[:vS][y,t] <= EP[:eTotalCapEnergy][y] - - # energy stored for the next hour - cSoCBalInterior[t in INTERIOR_SUBPERIODS, y in STOR_ALL], EP[:vS][y,t] == - EP[:vS][y,t-1]-(1/efficiency_down(gen[y])*EP[:vP][y,t])+(efficiency_up(gen[y])*EP[:vCHARGE][y,t])-(self_discharge(gen[y])*EP[:vS][y,t-1]) - end) - - # Storage discharge and charge power (and reserve contribution) related constraints: - if OperationalReserves == 1 - storage_all_operational_reserves!(EP, inputs, setup) - else - if CapacityReserveMargin > 0 - # Note: maximum charge rate is also constrained by maximum charge power capacity, but as this differs by storage type, - # this constraint is set in functions below for each storage type - - # Maximum discharging rate must be less than power rating OR available stored energy in the prior period, whichever is less - # wrapping from end of sample period to start of sample period for energy capacity constraint - @constraints(EP, begin - [y in STOR_ALL, t=1:T], EP[:vP][y,t] + EP[:vCAPRES_discharge][y,t] <= EP[:eTotalCap][y] - [y in STOR_ALL, t=1:T], EP[:vP][y,t] + EP[:vCAPRES_discharge][y,t] <= EP[:vS][y, hoursbefore(hours_per_subperiod,t,1)]*efficiency_down(gen[y]) - end) - else - @constraints(EP, begin - [y in STOR_ALL, t=1:T], EP[:vP][y,t] <= EP[:eTotalCap][y] - [y in STOR_ALL, t=1:T], EP[:vP][y,t] <= EP[:vS][y, hoursbefore(hours_per_subperiod,t,1)]*efficiency_down(gen[y]) - end) - end - end - - # From CO2 Policy module - expr = @expression(EP, [z=1:Z], sum(EP[:eELOSS][y] for y in intersect(STOR_ALL, resources_in_zone_by_rid(gen,z)))) - add_similar_to_expression!(EP[:eELOSSByZone], expr) - - # Capacity Reserve Margin policy - if CapacityReserveMargin > 0 - # Constraints governing energy held in reserve when storage makes virtual capacity reserve margin contributions: - - # Links energy held in reserve in first time step with decisions in last time step of each subperiod - # We use a modified formulation of this constraint (cVSoCBalLongDurationStorageStart) when operations wrapping and long duration storage are being modeled - @constraint(EP, cVSoCBalStart[t in START_SUBPERIODS, y in CONSTRAINTSET], EP[:vCAPRES_socinreserve][y,t] == - EP[:vCAPRES_socinreserve][y,t+hours_per_subperiod-1] + (1/efficiency_down(gen[y]) * EP[:vCAPRES_discharge][y,t]) - - (efficiency_up(gen[y])*EP[:vCAPRES_charge][y,t]) - (self_discharge(gen[y]) * EP[:vCAPRES_socinreserve][y,t+hours_per_subperiod-1])) - - # energy held in reserve for the next hour - @constraint(EP, cVSoCBalInterior[t in INTERIOR_SUBPERIODS, y in STOR_ALL], EP[:vCAPRES_socinreserve][y,t] == - EP[:vCAPRES_socinreserve][y,t-1]+(1/efficiency_down(gen[y])*EP[:vCAPRES_discharge][y,t])-(efficiency_up(gen[y])*EP[:vCAPRES_charge][y,t])-(self_discharge(gen[y])*EP[:vCAPRES_socinreserve][y,t-1])) - - # energy held in reserve acts as a lower bound on the total energy held in storage - @constraint(EP, cSOCMinCapRes[t in 1:T, y in STOR_ALL], EP[:vS][y,t] >= EP[:vCAPRES_socinreserve][y,t]) - end + if CapacityReserveMargin > 0 + # Virtual discharge contributing to capacity reserves at timestep t for storage cluster y + @variable(EP, vCAPRES_discharge[y in STOR_ALL, t = 1:T]>=0) + + # Virtual charge contributing to capacity reserves at timestep t for storage cluster y + @variable(EP, vCAPRES_charge[y in STOR_ALL, t = 1:T]>=0) + + # Total state of charge being held in reserve at timestep t for storage cluster y + @variable(EP, vCAPRES_socinreserve[y in STOR_ALL, t = 1:T]>=0) + end + + ### Expressions ### + + # Energy losses related to technologies (increase in effective demand) + @expression(EP, + eELOSS[y in STOR_ALL], + sum(inputs["omega"][t] * EP[:vCHARGE][y, t] for t in 1:T)-sum(inputs["omega"][t] * + EP[:vP][y, t] + for t in 1:T)) + + ## Objective Function Expressions ## + + #Variable costs of "charging" for technologies "y" during hour "t" in zone "z" + @expression(EP, + eCVar_in[y in STOR_ALL, t = 1:T], + inputs["omega"][t]*var_om_cost_per_mwh_in(gen[y])*vCHARGE[y, t]) + + # Sum individual resource contributions to variable charging costs to get total variable charging costs + @expression(EP, eTotalCVarInT[t = 1:T], sum(eCVar_in[y, t] for y in STOR_ALL)) + @expression(EP, eTotalCVarIn, sum(eTotalCVarInT[t] for t in 1:T)) + add_to_expression!(EP[:eObj], eTotalCVarIn) + + if CapacityReserveMargin > 0 + #Variable costs of "virtual charging" for technologies "y" during hour "t" in zone "z" + @expression(EP, + eCVar_in_virtual[y in STOR_ALL, t = 1:T], + inputs["omega"][t]*virtual_discharge_cost*vCAPRES_charge[y, t]) + @expression(EP, + eTotalCVarInT_virtual[t = 1:T], + sum(eCVar_in_virtual[y, t] for y in STOR_ALL)) + @expression(EP, eTotalCVarIn_virtual, sum(eTotalCVarInT_virtual[t] for t in 1:T)) + EP[:eObj] += eTotalCVarIn_virtual + + #Variable costs of "virtual discharging" for technologies "y" during hour "t" in zone "z" + @expression(EP, + eCVar_out_virtual[y in STOR_ALL, t = 1:T], + inputs["omega"][t]*virtual_discharge_cost*vCAPRES_discharge[y, t]) + @expression(EP, + eTotalCVarOutT_virtual[t = 1:T], + sum(eCVar_out_virtual[y, t] for y in STOR_ALL)) + @expression(EP, eTotalCVarOut_virtual, sum(eTotalCVarOutT_virtual[t] for t in 1:T)) + EP[:eObj] += eTotalCVarOut_virtual + end + + ## Power Balance Expressions ## + + # Term to represent net dispatch from storage in any period + @expression(EP, ePowerBalanceStor[t = 1:T, z = 1:Z], + sum(EP[:vP][y, t] - EP[:vCHARGE][y, t] + for y in intersect(resources_in_zone_by_rid(gen, z), STOR_ALL))) + add_similar_to_expression!(EP[:ePowerBalance], ePowerBalanceStor) + + ### Constraints ### + + ## Storage energy capacity and state of charge related constraints: + + # Links state of charge in first time step with decisions in last time step of each subperiod + # We use a modified formulation of this constraint (cSoCBalLongDurationStorageStart) when operations wrapping and long duration storage are being modeled + if representative_periods > 1 && !isempty(inputs["STOR_LONG_DURATION"]) + CONSTRAINTSET = STOR_SHORT_DURATION + else + CONSTRAINTSET = STOR_ALL + end + @constraint(EP, + cSoCBalStart[t in START_SUBPERIODS, y in CONSTRAINTSET], + EP[:vS][y, + t]== + EP[:vS][y, t + hours_per_subperiod - 1] - + (1 / efficiency_down(gen[y]) * EP[:vP][y, t]) + + + (efficiency_up(gen[y]) * EP[:vCHARGE][y, t]) - + (self_discharge(gen[y]) * EP[:vS][y, t + hours_per_subperiod - 1])) + + @constraints(EP, + begin + + # Maximum energy stored must be less than energy capacity + [y in STOR_ALL, t in 1:T], EP[:vS][y, t] <= EP[:eTotalCapEnergy][y] + + # energy stored for the next hour + cSoCBalInterior[t in INTERIOR_SUBPERIODS, y in STOR_ALL], + EP[:vS][y, t] == + EP[:vS][y, t - 1] - (1 / efficiency_down(gen[y]) * EP[:vP][y, t]) + + (efficiency_up(gen[y]) * EP[:vCHARGE][y, t]) - + (self_discharge(gen[y]) * EP[:vS][y, t - 1]) + end) + + # Storage discharge and charge power (and reserve contribution) related constraints: + if OperationalReserves == 1 + storage_all_operational_reserves!(EP, inputs, setup) + else + if CapacityReserveMargin > 0 + # Note: maximum charge rate is also constrained by maximum charge power capacity, but as this differs by storage type, + # this constraint is set in functions below for each storage type + + # Maximum discharging rate must be less than power rating OR available stored energy in the prior period, whichever is less + # wrapping from end of sample period to start of sample period for energy capacity constraint + @constraints(EP, + begin + [y in STOR_ALL, t = 1:T], + EP[:vP][y, t] + EP[:vCAPRES_discharge][y, t] <= EP[:eTotalCap][y] + [y in STOR_ALL, t = 1:T], + EP[:vP][y, t] + EP[:vCAPRES_discharge][y, t] <= + EP[:vS][y, hoursbefore(hours_per_subperiod, t, 1)] * + efficiency_down(gen[y]) + end) + else + @constraints(EP, + begin + [y in STOR_ALL, t = 1:T], EP[:vP][y, t] <= EP[:eTotalCap][y] + [y in STOR_ALL, t = 1:T], + EP[:vP][y, t] <= + EP[:vS][y, hoursbefore(hours_per_subperiod, t, 1)] * + efficiency_down(gen[y]) + end) + end + end + + # From CO2 Policy module + expr = @expression(EP, + [z = 1:Z], + sum(EP[:eELOSS][y] for y in intersect(STOR_ALL, resources_in_zone_by_rid(gen, z)))) + add_similar_to_expression!(EP[:eELOSSByZone], expr) + + # Capacity Reserve Margin policy + if CapacityReserveMargin > 0 + # Constraints governing energy held in reserve when storage makes virtual capacity reserve margin contributions: + + # Links energy held in reserve in first time step with decisions in last time step of each subperiod + # We use a modified formulation of this constraint (cVSoCBalLongDurationStorageStart) when operations wrapping and long duration storage are being modeled + @constraint(EP, + cVSoCBalStart[t in START_SUBPERIODS, y in CONSTRAINTSET], + EP[:vCAPRES_socinreserve][y, + t]== + EP[:vCAPRES_socinreserve][y, t + hours_per_subperiod - 1] + + (1 / efficiency_down(gen[y]) * EP[:vCAPRES_discharge][y, t]) + - + (efficiency_up(gen[y]) * EP[:vCAPRES_charge][y, t]) - (self_discharge(gen[y]) * + EP[:vCAPRES_socinreserve][y, t + hours_per_subperiod - 1])) + + # energy held in reserve for the next hour + @constraint(EP, + cVSoCBalInterior[t in INTERIOR_SUBPERIODS, y in STOR_ALL], + EP[:vCAPRES_socinreserve][y, + t]== + EP[:vCAPRES_socinreserve][y, t - 1] + + (1 / efficiency_down(gen[y]) * EP[:vCAPRES_discharge][y, t]) - + (efficiency_up(gen[y]) * EP[:vCAPRES_charge][y, t]) - + (self_discharge(gen[y]) * EP[:vCAPRES_socinreserve][y, t - 1])) + + # energy held in reserve acts as a lower bound on the total energy held in storage + @constraint(EP, + cSOCMinCapRes[t in 1:T, y in STOR_ALL], + EP[:vS][y, t]>=EP[:vCAPRES_socinreserve][y, t]) + end end function storage_all_operational_reserves!(EP::Model, inputs::Dict, setup::Dict) - gen = inputs["RESOURCES"] T = inputs["T"] p = inputs["hours_per_subperiod"] @@ -176,27 +224,35 @@ function storage_all_operational_reserves!(EP::Model, inputs::Dict, setup::Dict) eTotalCap = EP[:eTotalCap] eTotalCapEnergy = EP[:eTotalCapEnergy] - # Maximum storage contribution to reserves is a specified fraction of installed capacity - @constraint(EP, [y in STOR_REG, t in 1:T], vREG[y, t] <= reg_max(gen[y]) * eTotalCap[y]) - @constraint(EP, [y in STOR_RSV, t in 1:T], vRSV[y, t] <= rsv_max(gen[y]) * eTotalCap[y]) + # Maximum storage contribution to reserves is a specified fraction of installed capacity + @constraint(EP, [y in STOR_REG, t in 1:T], vREG[y, t]<=reg_max(gen[y]) * eTotalCap[y]) + @constraint(EP, [y in STOR_RSV, t in 1:T], vRSV[y, t]<=rsv_max(gen[y]) * eTotalCap[y]) - # Actual contribution to regulation and reserves is sum of auxilary variables for portions contributed during charging and discharging - @constraint(EP, [y in STOR_REG, t in 1:T], vREG[y, t] == vREG_charge[y, t] + vREG_discharge[y, t]) - @constraint(EP, [y in STOR_RSV, t in 1:T], vRSV[y, t] == vRSV_charge[y, t] + vRSV_discharge[y, t]) + # Actual contribution to regulation and reserves is sum of auxilary variables for portions contributed during charging and discharging + @constraint(EP, + [y in STOR_REG, t in 1:T], + vREG[y, t]==vREG_charge[y, t] + vREG_discharge[y, t]) + @constraint(EP, + [y in STOR_RSV, t in 1:T], + vRSV[y, t]==vRSV_charge[y, t] + vRSV_discharge[y, t]) # Maximum charging rate plus contribution to reserves up must be greater than zero # Note: when charging, reducing charge rate is contributing to upwards reserve & regulation as it drops net demand expr = extract_time_series_to_expression(vCHARGE, STOR_ALL) add_similar_to_expression!(expr[STOR_REG, :], -vREG_charge[STOR_REG, :]) add_similar_to_expression!(expr[STOR_RSV, :], -vRSV_charge[STOR_RSV, :]) - @constraint(EP, [y in STOR_ALL, t in 1:T], expr[y, t] >= 0) + @constraint(EP, [y in STOR_ALL, t in 1:T], expr[y, t]>=0) # Maximum discharging rate and contribution to reserves down must be greater than zero # Note: when discharging, reducing discharge rate is contributing to downwards regulation as it drops net supply - @constraint(EP, [y in STOR_REG, t in 1:T], vP[y, t] - vREG_discharge[y, t] >= 0) + @constraint(EP, [y in STOR_REG, t in 1:T], vP[y, t] - vREG_discharge[y, t]>=0) # Maximum charging rate plus contribution to regulation down must be less than available storage capacity - @constraint(EP, [y in STOR_REG, t in 1:T], efficiency_up(gen[y])*(vCHARGE[y, t]+vREG_charge[y, t]) <= eTotalCapEnergy[y]-vS[y, hoursbefore(p,t,1)]) + @constraint(EP, + [y in STOR_REG, t in 1:T], + efficiency_up(gen[y]) * + (vCHARGE[y, t] + + vREG_charge[y, t])<=eTotalCapEnergy[y] - vS[y, hoursbefore(p, t, 1)]) # Note: maximum charge rate is also constrained by maximum charge power capacity, but as this differs by storage type, # this constraint is set in functions below for each storage type @@ -208,7 +264,9 @@ function storage_all_operational_reserves!(EP::Model, inputs::Dict, setup::Dict) add_similar_to_expression!(expr[STOR_ALL, :], vCAPRES_discharge[STOR_ALL, :]) end # Maximum discharging rate and contribution to reserves up must be less than power rating - @constraint(EP, [y in STOR_ALL, t in 1:T], expr[y, t] <= eTotalCap[y]) + @constraint(EP, [y in STOR_ALL, t in 1:T], expr[y, t]<=eTotalCap[y]) # Maximum discharging rate and contribution to reserves up must be less than available stored energy in prior period - @constraint(EP, [y in STOR_ALL, t in 1:T], expr[y, t] <= vS[y, hoursbefore(p,t,1)] * efficiency_down(gen[y])) + @constraint(EP, + [y in STOR_ALL, t in 1:T], + expr[y, t]<=vS[y, hoursbefore(p, t, 1)] * efficiency_down(gen[y])) end diff --git a/src/model/resources/storage/storage_asymmetric.jl b/src/model/resources/storage/storage_asymmetric.jl index f77fe0fa23..8554d129e8 100644 --- a/src/model/resources/storage/storage_asymmetric.jl +++ b/src/model/resources/storage/storage_asymmetric.jl @@ -4,34 +4,37 @@ Sets up variables and constraints specific to storage resources with asymmetric charge and discharge capacities. See ```storage()``` in ```storage.jl``` for description of constraints. """ function storage_asymmetric!(EP::Model, inputs::Dict, setup::Dict) - # Set up additional variables, constraints, and expressions associated with storage resources with asymmetric charge & discharge capacity - # (e.g. most chemical, thermal, and mechanical storage options with distinct charge & discharge components/processes) - # STOR = 2 corresponds to storage with distinct power and energy capacity decisions and distinct charge and discharge power capacity decisions/ratings + # Set up additional variables, constraints, and expressions associated with storage resources with asymmetric charge & discharge capacity + # (e.g. most chemical, thermal, and mechanical storage options with distinct charge & discharge components/processes) + # STOR = 2 corresponds to storage with distinct power and energy capacity decisions and distinct charge and discharge power capacity decisions/ratings - println("Storage Resources with Asmymetric Charge/Discharge Capacity Module") + println("Storage Resources with Asmymetric Charge/Discharge Capacity Module") - OperationalReserves = setup["OperationalReserves"] - CapacityReserveMargin = setup["CapacityReserveMargin"] + OperationalReserves = setup["OperationalReserves"] + CapacityReserveMargin = setup["CapacityReserveMargin"] - T = inputs["T"] # Number of time steps (hours) + T = inputs["T"] # Number of time steps (hours) - STOR_ASYMMETRIC = inputs["STOR_ASYMMETRIC"] + STOR_ASYMMETRIC = inputs["STOR_ASYMMETRIC"] - ### Constraints ### - - # Storage discharge and charge power (and reserve contribution) related constraints for symmetric storage resources: - if OperationalReserves == 1 - storage_asymmetric_operational_reserves!(EP, inputs, setup) - else - if CapacityReserveMargin > 0 - # Maximum charging rate (including virtual charging to move energy held in reserve back to available storage) must be less than charge power rating - @constraint(EP, [y in STOR_ASYMMETRIC, t in 1:T], EP[:vCHARGE][y,t] + EP[:vCAPRES_charge][y,t] <= EP[:eTotalCapCharge][y]) - else - # Maximum charging rate (including virtual charging to move energy held in reserve back to available storage) must be less than charge power rating - @constraint(EP, [y in STOR_ASYMMETRIC, t in 1:T], EP[:vCHARGE][y,t] <= EP[:eTotalCapCharge][y]) - end - end + ### Constraints ### + # Storage discharge and charge power (and reserve contribution) related constraints for symmetric storage resources: + if OperationalReserves == 1 + storage_asymmetric_operational_reserves!(EP, inputs, setup) + else + if CapacityReserveMargin > 0 + # Maximum charging rate (including virtual charging to move energy held in reserve back to available storage) must be less than charge power rating + @constraint(EP, + [y in STOR_ASYMMETRIC, t in 1:T], + EP[:vCHARGE][y, t] + EP[:vCAPRES_charge][y, t]<=EP[:eTotalCapCharge][y]) + else + # Maximum charging rate (including virtual charging to move energy held in reserve back to available storage) must be less than charge power rating + @constraint(EP, + [y in STOR_ASYMMETRIC, t in 1:T], + EP[:vCHARGE][y, t]<=EP[:eTotalCapCharge][y]) + end + end end @doc raw""" @@ -40,12 +43,11 @@ end Sets up variables and constraints specific to storage resources with asymmetric charge and discharge capacities when reserves are modeled. See ```storage()``` in ```storage.jl``` for description of constraints. """ function storage_asymmetric_operational_reserves!(EP::Model, inputs::Dict, setup::Dict) + T = inputs["T"] + CapacityReserveMargin = setup["CapacityReserveMargin"] > 0 - T = inputs["T"] - CapacityReserveMargin = setup["CapacityReserveMargin"] > 0 - - STOR_ASYMMETRIC = inputs["STOR_ASYMMETRIC"] - STOR_ASYM_REG = intersect(STOR_ASYMMETRIC, inputs["REG"]) # Set of asymmetric storage resources with REG reserves + STOR_ASYMMETRIC = inputs["STOR_ASYMMETRIC"] + STOR_ASYM_REG = intersect(STOR_ASYMMETRIC, inputs["REG"]) # Set of asymmetric storage resources with REG reserves vCHARGE = EP[:vCHARGE] vREG_charge = EP[:vREG_charge] @@ -55,7 +57,8 @@ function storage_asymmetric_operational_reserves!(EP::Model, inputs::Dict, setup add_similar_to_expression!(expr[STOR_ASYM_REG, :], vREG_charge[STOR_ASYM_REG, :]) if CapacityReserveMargin vCAPRES_charge = EP[:vCAPRES_charge] - add_similar_to_expression!(expr[STOR_ASYMMETRIC, :], vCAPRES_charge[STOR_ASYMMETRIC, :]) + add_similar_to_expression!(expr[STOR_ASYMMETRIC, :], + vCAPRES_charge[STOR_ASYMMETRIC, :]) end - @constraint(EP, [y in STOR_ASYMMETRIC, t in 1:T], expr[y, t] <= eTotalCapCharge[y]) + @constraint(EP, [y in STOR_ASYMMETRIC, t in 1:T], expr[y, t]<=eTotalCapCharge[y]) end diff --git a/src/model/resources/storage/storage_symmetric.jl b/src/model/resources/storage/storage_symmetric.jl index 3ac73f2ed2..3c20d2368b 100644 --- a/src/model/resources/storage/storage_symmetric.jl +++ b/src/model/resources/storage/storage_symmetric.jl @@ -4,40 +4,44 @@ Sets up variables and constraints specific to storage resources with symmetric charge and discharge capacities. See ```storage()``` in ```storage.jl``` for description of constraints. """ function storage_symmetric!(EP::Model, inputs::Dict, setup::Dict) - # Set up additional variables, constraints, and expressions associated with storage resources with symmetric charge & discharge capacity - # (e.g. most electrochemical batteries that use same components for charge & discharge) - # STOR = 1 corresponds to storage with distinct power and energy capacity decisions but symmetric charge/discharge power ratings + # Set up additional variables, constraints, and expressions associated with storage resources with symmetric charge & discharge capacity + # (e.g. most electrochemical batteries that use same components for charge & discharge) + # STOR = 1 corresponds to storage with distinct power and energy capacity decisions but symmetric charge/discharge power ratings - println("Storage Resources with Symmetric Charge/Discharge Capacity Module") + println("Storage Resources with Symmetric Charge/Discharge Capacity Module") - OperationalReserves = setup["OperationalReserves"] - CapacityReserveMargin = setup["CapacityReserveMargin"] + OperationalReserves = setup["OperationalReserves"] + CapacityReserveMargin = setup["CapacityReserveMargin"] - T = inputs["T"] # Number of time steps (hours) + T = inputs["T"] # Number of time steps (hours) - STOR_SYMMETRIC = inputs["STOR_SYMMETRIC"] + STOR_SYMMETRIC = inputs["STOR_SYMMETRIC"] - ### Constraints ### - - # Storage discharge and charge power (and reserve contribution) related constraints for symmetric storage resources: - if OperationalReserves == 1 - storage_symmetric_operational_reserves!(EP, inputs, setup) - else - if CapacityReserveMargin > 0 - @constraints(EP, begin - # Maximum charging rate (including virtual charging to move energy held in reserve back to available storage) must be less than symmetric power rating - # Max simultaneous charge and discharge cannot be greater than capacity - [y in STOR_SYMMETRIC, t in 1:T], EP[:vP][y,t]+EP[:vCHARGE][y,t]+EP[:vCAPRES_discharge][y,t]+EP[:vCAPRES_charge][y,t] <= EP[:eTotalCap][y] - end) - else - @constraints(EP, begin - # Maximum charging rate (including virtual charging to move energy held in reserve back to available storage) must be less than symmetric power rating - # Max simultaneous charge and discharge cannot be greater than capacity - [y in STOR_SYMMETRIC, t in 1:T], EP[:vP][y,t]+EP[:vCHARGE][y,t] <= EP[:eTotalCap][y] - end) - end - end + ### Constraints ### + # Storage discharge and charge power (and reserve contribution) related constraints for symmetric storage resources: + if OperationalReserves == 1 + storage_symmetric_operational_reserves!(EP, inputs, setup) + else + if CapacityReserveMargin > 0 + @constraints(EP, + begin + # Maximum charging rate (including virtual charging to move energy held in reserve back to available storage) must be less than symmetric power rating + # Max simultaneous charge and discharge cannot be greater than capacity + [y in STOR_SYMMETRIC, t in 1:T], + EP[:vP][y, t] + EP[:vCHARGE][y, t] + EP[:vCAPRES_discharge][y, t] + + EP[:vCAPRES_charge][y, t] <= EP[:eTotalCap][y] + end) + else + @constraints(EP, + begin + # Maximum charging rate (including virtual charging to move energy held in reserve back to available storage) must be less than symmetric power rating + # Max simultaneous charge and discharge cannot be greater than capacity + [y in STOR_SYMMETRIC, t in 1:T], + EP[:vP][y, t] + EP[:vCHARGE][y, t] <= EP[:eTotalCap][y] + end) + end + end end @doc raw""" @@ -46,14 +50,13 @@ end Sets up variables and constraints specific to storage resources with symmetric charge and discharge capacities when reserves are modeled. See ```storage()``` in ```storage.jl``` for description of constraints. """ function storage_symmetric_operational_reserves!(EP::Model, inputs::Dict, setup::Dict) + T = inputs["T"] + CapacityReserveMargin = setup["CapacityReserveMargin"] > 0 - T = inputs["T"] - CapacityReserveMargin = setup["CapacityReserveMargin"] > 0 - - SYMMETRIC = inputs["STOR_SYMMETRIC"] + SYMMETRIC = inputs["STOR_SYMMETRIC"] - REG = intersect(SYMMETRIC, inputs["REG"]) - RSV = intersect(SYMMETRIC, inputs["RSV"]) + REG = intersect(SYMMETRIC, inputs["REG"]) + RSV = intersect(SYMMETRIC, inputs["RSV"]) vP = EP[:vP] vCHARGE = EP[:vCHARGE] @@ -65,7 +68,7 @@ function storage_symmetric_operational_reserves!(EP::Model, inputs::Dict, setup: # Maximum charging rate plus contribution to regulation down must be less than symmetric power rating # Max simultaneous charge and discharge rates cannot be greater than symmetric charge/discharge capacity - expr = @expression(EP, [y in SYMMETRIC, t in 1:T], vP[y, t] + vCHARGE[y, t]) + expr = @expression(EP, [y in SYMMETRIC, t in 1:T], vP[y, t]+vCHARGE[y, t]) add_similar_to_expression!(expr[REG, :], vREG_charge[REG, :]) add_similar_to_expression!(expr[REG, :], vREG_discharge[REG, :]) add_similar_to_expression!(expr[RSV, :], vRSV_discharge[RSV, :]) @@ -75,5 +78,5 @@ function storage_symmetric_operational_reserves!(EP::Model, inputs::Dict, setup: add_similar_to_expression!(expr[SYMMETRIC, :], vCAPRES_charge[SYMMETRIC, :]) add_similar_to_expression!(expr[SYMMETRIC, :], vCAPRES_discharge[SYMMETRIC, :]) end - @constraint(EP, [y in SYMMETRIC, t in 1:T], expr[y, t] <= eTotalCap[y]) + @constraint(EP, [y in SYMMETRIC, t in 1:T], expr[y, t]<=eTotalCap[y]) end diff --git a/src/model/resources/thermal/thermal.jl b/src/model/resources/thermal/thermal.jl index 894c2da2c0..ef5f9df385 100644 --- a/src/model/resources/thermal/thermal.jl +++ b/src/model/resources/thermal/thermal.jl @@ -4,46 +4,38 @@ The thermal module creates decision variables, expressions, and constraints rela This module uses the following 'helper' functions in separate files: ```thermal_commit()``` for thermal resources subject to unit commitment decisions and constraints (if any) and ```thermal_no_commit()``` for thermal resources not subject to unit commitment (if any). """ function thermal!(EP::Model, inputs::Dict, setup::Dict) - gen = inputs["RESOURCES"] + gen = inputs["RESOURCES"] - T = inputs["T"] # Number of time steps (hours) - Z = inputs["Z"] # Number of zones + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones - THERM_COMMIT = inputs["THERM_COMMIT"] - THERM_NO_COMMIT = inputs["THERM_NO_COMMIT"] - THERM_ALL = inputs["THERM_ALL"] + THERM_COMMIT = inputs["THERM_COMMIT"] + THERM_NO_COMMIT = inputs["THERM_NO_COMMIT"] + THERM_ALL = inputs["THERM_ALL"] - if !isempty(THERM_COMMIT) - thermal_commit!(EP, inputs, setup) - end + if !isempty(THERM_COMMIT) + thermal_commit!(EP, inputs, setup) + end - if !isempty(THERM_NO_COMMIT) - thermal_no_commit!(EP, inputs, setup) - end - ##CO2 Polcy Module Thermal Generation by zone - @expression(EP, eGenerationByThermAll[z=1:Z, t=1:T], # the unit is GW - sum(EP[:vP][y,t] for y in intersect(inputs["THERM_ALL"], resources_in_zone_by_rid(gen,z))) - ) - add_similar_to_expression!(EP[:eGenerationByZone], eGenerationByThermAll) + if !isempty(THERM_NO_COMMIT) + thermal_no_commit!(EP, inputs, setup) + end + ##CO2 Polcy Module Thermal Generation by zone + @expression(EP, eGenerationByThermAll[z = 1:Z, t = 1:T], # the unit is GW + sum(EP[:vP][y, t] + for y in intersect(inputs["THERM_ALL"], resources_in_zone_by_rid(gen, z)))) + add_similar_to_expression!(EP[:eGenerationByZone], eGenerationByThermAll) - # Capacity Reserves Margin policy - if setup["CapacityReserveMargin"] > 0 + # Capacity Reserves Margin policy + if setup["CapacityReserveMargin"] > 0 ncapres = inputs["NCapacityReserveMargin"] @expression(EP, eCapResMarBalanceThermal[capres in 1:ncapres, t in 1:T], - sum(derating_factor(gen[y], tag=capres) * EP[:eTotalCap][y] for y in THERM_ALL)) - add_similar_to_expression!(EP[:eCapResMarBalance], eCapResMarBalanceThermal) + sum(derating_factor(gen[y], tag = capres) * EP[:eTotalCap][y] for y in THERM_ALL)) + add_similar_to_expression!(EP[:eCapResMarBalance], eCapResMarBalanceThermal) MAINT = ids_with_maintenance(gen) if !isempty(intersect(MAINT, THERM_COMMIT)) thermal_maintenance_capacity_reserve_margin_adjustment!(EP, inputs) end - end -#= - ##CO2 Polcy Module Thermal Generation by zone - @expression(EP, eGenerationByThermAll[z=1:Z, t=1:T], # the unit is GW - sum(EP[:vP][y,t] for y in intersect(inputs["THERM_ALL"], resources_in_zone_by_rid(gen,z))) - ) - EP[:eGenerationByZone] += eGenerationByThermAll - =# ##From main + end end - diff --git a/src/model/resources/thermal/thermal_commit.jl b/src/model/resources/thermal/thermal_commit.jl index 84e3e7020d..347bdfbed5 100644 --- a/src/model/resources/thermal/thermal_commit.jl +++ b/src/model/resources/thermal/thermal_commit.jl @@ -125,20 +125,19 @@ Like with the ramping constraints, the minimum up and down constraint time also It is recommended that users of GenX must use longer subperiods than the longest min up/down time if modeling UC. Otherwise, the model will report error. """ function thermal_commit!(EP::Model, inputs::Dict, setup::Dict) + println("Thermal (Unit Commitment) Resources Module") - println("Thermal (Unit Commitment) Resources Module") - - gen = inputs["RESOURCES"] + gen = inputs["RESOURCES"] - T = inputs["T"] # Number of time steps (hours) - Z = inputs["Z"] # Number of zones - G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones + G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) - p = inputs["hours_per_subperiod"] #total number of hours per subperiod + p = inputs["hours_per_subperiod"] #total number of hours per subperiod - THERM_COMMIT = inputs["THERM_COMMIT"] + THERM_COMMIT = inputs["THERM_COMMIT"] - ### Expressions ### + ### Expressions ### # These variables are used in the ramp-up and ramp-down expressions reserves_term = @expression(EP, [y in THERM_COMMIT, t in 1:T], 0) @@ -148,76 +147,97 @@ function thermal_commit!(EP::Model, inputs::Dict, setup::Dict) THERM_COMMIT_REG = intersect(THERM_COMMIT, inputs["REG"]) # Set of thermal resources with regulation reserves THERM_COMMIT_RSV = intersect(THERM_COMMIT, inputs["RSV"]) # Set of thermal resources with spinning reserves regulation_term = @expression(EP, [y in THERM_COMMIT, t in 1:T], - y ∈ THERM_COMMIT_REG ? EP[:vREG][y,t] - EP[:vREG][y, hoursbefore(p, t, 1)] : 0) + y ∈ THERM_COMMIT_REG ? EP[:vREG][y, t] - EP[:vREG][y, hoursbefore(p, t, 1)] : 0) reserves_term = @expression(EP, [y in THERM_COMMIT, t in 1:T], - y ∈ THERM_COMMIT_RSV ? EP[:vRSV][y,t] : 0) + y ∈ THERM_COMMIT_RSV ? EP[:vRSV][y, t] : 0) + end + + ## Power Balance Expressions ## + @expression(EP, ePowerBalanceThermCommit[t = 1:T, z = 1:Z], + sum(EP[:vP][y, t] for y in intersect(THERM_COMMIT, resources_in_zone_by_rid(gen, z)))) + add_similar_to_expression!(EP[:ePowerBalance], ePowerBalanceThermCommit) + + ### Constraints ### + + ### Capacitated limits on unit commitment decision variables (Constraints #1-3) + @constraints(EP, + begin + [y in THERM_COMMIT, t = 1:T], + EP[:vCOMMIT][y, t] <= EP[:eTotalCap][y] / cap_size(gen[y]) + [y in THERM_COMMIT, t = 1:T], + EP[:vSTART][y, t] <= EP[:eTotalCap][y] / cap_size(gen[y]) + [y in THERM_COMMIT, t = 1:T], + EP[:vSHUT][y, t] <= EP[:eTotalCap][y] / cap_size(gen[y]) + end) + + # Commitment state constraint linking startup and shutdown decisions (Constraint #4) + @constraints(EP, + begin + [y in THERM_COMMIT, t in 1:T], + EP[:vCOMMIT][y, t] == + EP[:vCOMMIT][y, hoursbefore(p, t, 1)] + EP[:vSTART][y, t] - EP[:vSHUT][y, t] + end) + + ### Maximum ramp up and down between consecutive hours (Constraints #5-6) + + ## For Start Hours + # Links last time step with first time step, ensuring position in hour 1 is within eligible ramp of final hour position + # rampup constraints + @constraint(EP, [y in THERM_COMMIT, t in 1:T], + EP[:vP][y, t] - EP[:vP][y, hoursbefore(p, t, 1)] + regulation_term[y, t] + + reserves_term[y, t]<=ramp_up_fraction(gen[y]) * cap_size(gen[y]) * + (EP[:vCOMMIT][y, t] - EP[:vSTART][y, t]) + + + min(inputs["pP_Max"][y, t], + max(min_power(gen[y]), ramp_up_fraction(gen[y]))) * cap_size(gen[y]) * EP[:vSTART][y, t] + - + min_power(gen[y]) * cap_size(gen[y]) * EP[:vSHUT][y, t]) + + # rampdown constraints + @constraint(EP, [y in THERM_COMMIT, t in 1:T], + EP[:vP][y, hoursbefore(p, t, 1)] - EP[:vP][y, t] - regulation_term[y, t] + + reserves_term[y, + hoursbefore(p, t, 1)]<=ramp_down_fraction(gen[y]) * cap_size(gen[y]) * + (EP[:vCOMMIT][y, t] - EP[:vSTART][y, t]) + - + min_power(gen[y]) * cap_size(gen[y]) * EP[:vSTART][y, t] + + + min(inputs["pP_Max"][y, t], + max(min_power(gen[y]), ramp_down_fraction(gen[y]))) * cap_size(gen[y]) * EP[:vSHUT][y, t]) + + ### Minimum and maximum power output constraints (Constraints #7-8) + if setup["OperationalReserves"] == 1 + # If modeling with regulation and reserves, constraints are established by thermal_commit_operational_reserves() function below + thermal_commit_operational_reserves!(EP, inputs) + else + @constraints(EP, + begin + # Minimum stable power generated per technology "y" at hour "t" > Min power + [y in THERM_COMMIT, t = 1:T], + EP[:vP][y, t] >= min_power(gen[y]) * cap_size(gen[y]) * EP[:vCOMMIT][y, t] + + # Maximum power generated per technology "y" at hour "t" < Max power + [y in THERM_COMMIT, t = 1:T], + EP[:vP][y, t] <= + inputs["pP_Max"][y, t] * cap_size(gen[y]) * EP[:vCOMMIT][y, t] + end) end - ## Power Balance Expressions ## - @expression(EP, ePowerBalanceThermCommit[t=1:T, z=1:Z], - sum(EP[:vP][y,t] for y in intersect(THERM_COMMIT, resources_in_zone_by_rid(gen,z))) - ) - add_similar_to_expression!(EP[:ePowerBalance], ePowerBalanceThermCommit) - - ### Constraints ### - - ### Capacitated limits on unit commitment decision variables (Constraints #1-3) - @constraints(EP, begin - [y in THERM_COMMIT, t=1:T], EP[:vCOMMIT][y,t] <= EP[:eTotalCap][y]/cap_size(gen[y]) - [y in THERM_COMMIT, t=1:T], EP[:vSTART][y,t] <= EP[:eTotalCap][y]/cap_size(gen[y]) - [y in THERM_COMMIT, t=1:T], EP[:vSHUT][y,t] <= EP[:eTotalCap][y]/cap_size(gen[y]) - end) - - # Commitment state constraint linking startup and shutdown decisions (Constraint #4) - @constraints(EP, begin - [y in THERM_COMMIT, t in 1:T], EP[:vCOMMIT][y,t] == EP[:vCOMMIT][y, hoursbefore(p, t, 1)] + EP[:vSTART][y,t] - EP[:vSHUT][y,t] - end) - - ### Maximum ramp up and down between consecutive hours (Constraints #5-6) - - ## For Start Hours - # Links last time step with first time step, ensuring position in hour 1 is within eligible ramp of final hour position - # rampup constraints - @constraint(EP,[y in THERM_COMMIT, t in 1:T], - EP[:vP][y,t] - EP[:vP][y, hoursbefore(p, t, 1)] + regulation_term[y,t] + reserves_term[y,t] <= ramp_up_fraction(gen[y])*cap_size(gen[y])*(EP[:vCOMMIT][y,t]-EP[:vSTART][y,t]) - + min(inputs["pP_Max"][y,t],max(min_power(gen[y]),ramp_up_fraction(gen[y])))*cap_size(gen[y])*EP[:vSTART][y,t] - - min_power(gen[y])*cap_size(gen[y])*EP[:vSHUT][y,t]) - - # rampdown constraints - @constraint(EP,[y in THERM_COMMIT, t in 1:T], - EP[:vP][y, hoursbefore(p,t,1)] - EP[:vP][y,t] - regulation_term[y,t] + reserves_term[y, hoursbefore(p,t,1)] <= ramp_down_fraction(gen[y])*cap_size(gen[y])*(EP[:vCOMMIT][y,t]-EP[:vSTART][y,t]) - - min_power(gen[y])*cap_size(gen[y])*EP[:vSTART][y,t] - + min(inputs["pP_Max"][y,t],max(min_power(gen[y]),ramp_down_fraction(gen[y])))*cap_size(gen[y])*EP[:vSHUT][y,t]) - - - ### Minimum and maximum power output constraints (Constraints #7-8) - if setup["OperationalReserves"] == 1 - # If modeling with regulation and reserves, constraints are established by thermal_commit_operational_reserves() function below - thermal_commit_operational_reserves!(EP, inputs) - else - @constraints(EP, begin - # Minimum stable power generated per technology "y" at hour "t" > Min power - [y in THERM_COMMIT, t=1:T], EP[:vP][y,t] >= min_power(gen[y])*cap_size(gen[y])*EP[:vCOMMIT][y,t] - - # Maximum power generated per technology "y" at hour "t" < Max power - [y in THERM_COMMIT, t=1:T], EP[:vP][y,t] <= inputs["pP_Max"][y,t]*cap_size(gen[y])*EP[:vCOMMIT][y,t] - end) - end - - ### Minimum up and down times (Constraints #9-10) - Up_Time = zeros(Int, G) - Up_Time[THERM_COMMIT] .= Int.(floor.(up_time.(gen[THERM_COMMIT]))) - @constraint(EP, [y in THERM_COMMIT, t in 1:T], - EP[:vCOMMIT][y,t] >= sum(EP[:vSTART][y, u] for u in hoursbefore(p, t, 0:(Up_Time[y] - 1))) - ) - - Down_Time = zeros(Int, G) - Down_Time[THERM_COMMIT] .= Int.(floor.(down_time.(gen[THERM_COMMIT]))) - @constraint(EP, [y in THERM_COMMIT, t in 1:T], - EP[:eTotalCap][y]/cap_size(gen[y])-EP[:vCOMMIT][y,t] >= sum(EP[:vSHUT][y, u] for u in hoursbefore(p, t, 0:(Down_Time[y] - 1))) - ) - - ## END Constraints for thermal units subject to integer (discrete) unit commitment decisions + ### Minimum up and down times (Constraints #9-10) + Up_Time = zeros(Int, G) + Up_Time[THERM_COMMIT] .= Int.(floor.(up_time.(gen[THERM_COMMIT]))) + @constraint(EP, [y in THERM_COMMIT, t in 1:T], + EP[:vCOMMIT][y, + t]>=sum(EP[:vSTART][y, u] for u in hoursbefore(p, t, 0:(Up_Time[y] - 1)))) + + Down_Time = zeros(Int, G) + Down_Time[THERM_COMMIT] .= Int.(floor.(down_time.(gen[THERM_COMMIT]))) + @constraint(EP, [y in THERM_COMMIT, t in 1:T], + EP[:eTotalCap][y] / cap_size(gen[y]) - + EP[:vCOMMIT][y, + t]>=sum(EP[:vSHUT][y, u] for u in hoursbefore(p, t, 0:(Down_Time[y] - 1)))) + + ## END Constraints for thermal units subject to integer (discrete) unit commitment decisions if !isempty(ids_with_maintenance(gen)) maintenance_formulation_thermal_commit!(EP, inputs, setup) end @@ -266,39 +286,46 @@ When modeling frequency regulation and spinning reserves contributions, thermal """ function thermal_commit_operational_reserves!(EP::Model, inputs::Dict) + println("Thermal Commit Operational Reserves Module") - println("Thermal Commit Operational Reserves Module") - - gen = inputs["RESOURCES"] + gen = inputs["RESOURCES"] - T = inputs["T"] # Number of time steps (hours) + T = inputs["T"] # Number of time steps (hours) - THERM_COMMIT = inputs["THERM_COMMIT"] + THERM_COMMIT = inputs["THERM_COMMIT"] - REG = intersect(THERM_COMMIT, inputs["REG"]) # Set of thermal resources with regulation reserves - RSV = intersect(THERM_COMMIT, inputs["RSV"]) # Set of thermal resources with spinning reserves + REG = intersect(THERM_COMMIT, inputs["REG"]) # Set of thermal resources with regulation reserves + RSV = intersect(THERM_COMMIT, inputs["RSV"]) # Set of thermal resources with spinning reserves vP = EP[:vP] vREG = EP[:vREG] vRSV = EP[:vRSV] - commit(y,t) = cap_size(gen[y]) * EP[:vCOMMIT][y,t] - max_power(y,t) = inputs["pP_Max"][y,t] + commit(y, t) = cap_size(gen[y]) * EP[:vCOMMIT][y, t] + max_power(y, t) = inputs["pP_Max"][y, t] # Maximum regulation and reserve contributions - @constraint(EP, [y in REG, t in 1:T], vREG[y, t] <= max_power(y, t) * reg_max(gen[y]) * commit(y, t)) - @constraint(EP, [y in RSV, t in 1:T], vRSV[y, t] <= max_power(y, t) * rsv_max(gen[y]) * commit(y, t)) + @constraint(EP, + [y in REG, t in 1:T], + vREG[y, t]<=max_power(y, t) * reg_max(gen[y]) * commit(y, t)) + @constraint(EP, + [y in RSV, t in 1:T], + vRSV[y, t]<=max_power(y, t) * rsv_max(gen[y]) * commit(y, t)) # Minimum stable power generated per technology "y" at hour "t" and contribution to regulation must be > min power expr = extract_time_series_to_expression(vP, THERM_COMMIT) add_similar_to_expression!(expr[REG, :], -vREG[REG, :]) - @constraint(EP, [y in THERM_COMMIT, t in 1:T], expr[y, t] >= min_power(gen[y]) * commit(y, t)) + @constraint(EP, + [y in THERM_COMMIT, t in 1:T], + expr[y, t]>=min_power(gen[y]) * commit(y, t)) # Maximum power generated per technology "y" at hour "t" and contribution to regulation and reserves up must be < max power expr = extract_time_series_to_expression(vP, THERM_COMMIT) add_similar_to_expression!(expr[REG, :], vREG[REG, :]) add_similar_to_expression!(expr[RSV, :], vRSV[RSV, :]) - @constraint(EP, [y in THERM_COMMIT, t in 1:T], expr[y, t] <= max_power(y, t) * commit(y, t)) + @constraint(EP, + [y in THERM_COMMIT, t in 1:T], + expr[y, t]<=max_power(y, t) * commit(y, t)) end @doc raw""" @@ -307,12 +334,11 @@ end Creates maintenance variables and constraints for thermal-commit plants. """ function maintenance_formulation_thermal_commit!(EP::Model, inputs::Dict, setup::Dict) - @info "Maintenance Module for Thermal plants" ensure_maintenance_variable_records!(inputs) gen = inputs["RESOURCES"] - + by_rid(rid, sym) = by_rid_res(rid, sym, gen) MAINT = ids_with_maintenance(gen) @@ -331,16 +357,16 @@ function maintenance_formulation_thermal_commit!(EP::Model, inputs::Dict, setup: for y in MAINT maintenance_formulation!(EP, - inputs, - resource_component(y), - y, - maint_begin_cadence(y), - maint_dur(y), - maint_freq(y), - cap(y), - vcommit, - ecap, - integer_operational_unit_committment) + inputs, + resource_component(y), + y, + maint_begin_cadence(y), + maint_dur(y), + maint_freq(y), + cap(y), + vcommit, + ecap, + integer_operational_unit_committment) end end @@ -350,7 +376,7 @@ end Eliminates the contribution of a plant to the capacity reserve margin while it is down for maintenance. """ function thermal_maintenance_capacity_reserve_margin_adjustment!(EP::Model, - inputs::Dict) + inputs::Dict) gen = inputs["RESOURCES"] T = inputs["T"] # Number of time steps (hours) @@ -360,18 +386,22 @@ function thermal_maintenance_capacity_reserve_margin_adjustment!(EP::Model, applicable_resources = intersect(MAINT, THERM_COMMIT) maint_adj = @expression(EP, [capres in 1:ncapres, t in 1:T], - sum(thermal_maintenance_capacity_reserve_margin_adjustment(EP, inputs, y, capres, t) for y in applicable_resources)) + sum(thermal_maintenance_capacity_reserve_margin_adjustment(EP, + inputs, + y, + capres, + t) for y in applicable_resources)) add_similar_to_expression!(EP[:eCapResMarBalance], maint_adj) end function thermal_maintenance_capacity_reserve_margin_adjustment(EP::Model, - inputs::Dict, - y::Int, - capres::Int, - t) + inputs::Dict, + y::Int, + capres::Int, + t) gen = inputs["RESOURCES"] resource_component = resource_name(gen[y]) - capresfactor = derating_factor(gen[y], tag=capres) + capresfactor = derating_factor(gen[y], tag = capres) cap = cap_size(gen[y]) down_var = EP[Symbol(maintenance_down_name(resource_component))] return -capresfactor * down_var[t] * cap diff --git a/src/model/resources/thermal/thermal_no_commit.jl b/src/model/resources/thermal/thermal_no_commit.jl index 975a8c67f3..1a75eb0980 100644 --- a/src/model/resources/thermal/thermal_no_commit.jl +++ b/src/model/resources/thermal/thermal_no_commit.jl @@ -42,53 +42,59 @@ When not modeling regulation and reserves, thermal units not subject to unit com (See Constraints 3-4 in the code) """ function thermal_no_commit!(EP::Model, inputs::Dict, setup::Dict) + println("Thermal (No Unit Commitment) Resources Module") - println("Thermal (No Unit Commitment) Resources Module") + gen = inputs["RESOURCES"] - gen = inputs["RESOURCES"] - - T = inputs["T"] # Number of time steps (hours) - Z = inputs["Z"] # Number of zones - - p = inputs["hours_per_subperiod"] #total number of hours per subperiod - - THERM_NO_COMMIT = inputs["THERM_NO_COMMIT"] - - ### Expressions ### - - ## Power Balance Expressions ## - @expression(EP, ePowerBalanceThermNoCommit[t=1:T, z=1:Z], - sum(EP[:vP][y,t] for y in intersect(THERM_NO_COMMIT, resources_in_zone_by_rid(gen,z))) - ) - add_similar_to_expression!(EP[:ePowerBalance], ePowerBalanceThermNoCommit) - - ### Constraints ### - - ### Maximum ramp up and down between consecutive hours (Constraints #1-2) - @constraints(EP, begin - - ## Maximum ramp up between consecutive hours - [y in THERM_NO_COMMIT, t in 1:T], EP[:vP][y,t] - EP[:vP][y, hoursbefore(p,t,1)] <= ramp_up_fraction(gen[y])*EP[:eTotalCap][y] - - ## Maximum ramp down between consecutive hours - [y in THERM_NO_COMMIT, t in 1:T], EP[:vP][y, hoursbefore(p,t,1)] - EP[:vP][y,t] <= ramp_down_fraction(gen[y])*EP[:eTotalCap][y] - end) - - ### Minimum and maximum power output constraints (Constraints #3-4) - if setup["OperationalReserves"] == 1 - # If modeling with regulation and reserves, constraints are established by thermal_no_commit_operational_reserves() function below - thermal_no_commit_operational_reserves!(EP, inputs) - else - @constraints(EP, begin - # Minimum stable power generated per technology "y" at hour "t" Min_Power - [y in THERM_NO_COMMIT, t=1:T], EP[:vP][y,t] >= min_power(gen[y])*EP[:eTotalCap][y] - - # Maximum power generated per technology "y" at hour "t" - [y in THERM_NO_COMMIT, t=1:T], EP[:vP][y,t] <= inputs["pP_Max"][y,t]*EP[:eTotalCap][y] - end) - - end - # END Constraints for thermal resources not subject to unit commitment + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones + + p = inputs["hours_per_subperiod"] #total number of hours per subperiod + + THERM_NO_COMMIT = inputs["THERM_NO_COMMIT"] + + ### Expressions ### + + ## Power Balance Expressions ## + @expression(EP, ePowerBalanceThermNoCommit[t = 1:T, z = 1:Z], + sum(EP[:vP][y, t] + for y in intersect(THERM_NO_COMMIT, resources_in_zone_by_rid(gen, z)))) + add_similar_to_expression!(EP[:ePowerBalance], ePowerBalanceThermNoCommit) + + ### Constraints ### + + ### Maximum ramp up and down between consecutive hours (Constraints #1-2) + @constraints(EP, + begin + + ## Maximum ramp up between consecutive hours + [y in THERM_NO_COMMIT, t in 1:T], + EP[:vP][y, t] - EP[:vP][y, hoursbefore(p, t, 1)] <= + ramp_up_fraction(gen[y]) * EP[:eTotalCap][y] + + ## Maximum ramp down between consecutive hours + [y in THERM_NO_COMMIT, t in 1:T], + EP[:vP][y, hoursbefore(p, t, 1)] - EP[:vP][y, t] <= + ramp_down_fraction(gen[y]) * EP[:eTotalCap][y] + end) + + ### Minimum and maximum power output constraints (Constraints #3-4) + if setup["OperationalReserves"] == 1 + # If modeling with regulation and reserves, constraints are established by thermal_no_commit_operational_reserves() function below + thermal_no_commit_operational_reserves!(EP, inputs) + else + @constraints(EP, + begin + # Minimum stable power generated per technology "y" at hour "t" Min_Power + [y in THERM_NO_COMMIT, t = 1:T], + EP[:vP][y, t] >= min_power(gen[y]) * EP[:eTotalCap][y] + + # Maximum power generated per technology "y" at hour "t" + [y in THERM_NO_COMMIT, t = 1:T], + EP[:vP][y, t] <= inputs["pP_Max"][y, t] * EP[:eTotalCap][y] + end) + end + # END Constraints for thermal resources not subject to unit commitment end @doc raw""" @@ -135,10 +141,9 @@ When modeling regulation and spinning reserves, thermal units not subject to uni Note there are multiple versions of these constraints in the code in order to avoid creation of unecessary constraints and decision variables for thermal units unable to provide regulation and/or reserves contributions due to input parameters (e.g. ```Reg_Max=0``` and/or ```RSV_Max=0```). """ function thermal_no_commit_operational_reserves!(EP::Model, inputs::Dict) + println("Thermal No Commit Reserves Module") - println("Thermal No Commit Reserves Module") - - gen = inputs["RESOURCES"] + gen = inputs["RESOURCES"] T = inputs["T"] # Number of time steps (hours) @@ -152,20 +157,28 @@ function thermal_no_commit_operational_reserves!(EP::Model, inputs::Dict) vRSV = EP[:vRSV] eTotalCap = EP[:eTotalCap] - max_power(y,t) = inputs["pP_Max"][y,t] + max_power(y, t) = inputs["pP_Max"][y, t] # Maximum regulation and reserve contributions - @constraint(EP, [y in REG, t in 1:T], vREG[y, t] <= max_power(y, t) * reg_max(gen[y]) * eTotalCap[y]) - @constraint(EP, [y in RSV, t in 1:T], vRSV[y, t] <= max_power(y, t) * rsv_max(gen[y]) * eTotalCap[y]) + @constraint(EP, + [y in REG, t in 1:T], + vREG[y, t]<=max_power(y, t) * reg_max(gen[y]) * eTotalCap[y]) + @constraint(EP, + [y in RSV, t in 1:T], + vRSV[y, t]<=max_power(y, t) * rsv_max(gen[y]) * eTotalCap[y]) # Minimum stable power generated per technology "y" at hour "t" and contribution to regulation must be > min power expr = extract_time_series_to_expression(vP, THERM_NO_COMMIT) add_similar_to_expression!(expr[REG, :], -vREG[REG, :]) - @constraint(EP, [y in THERM_NO_COMMIT, t in 1:T], expr[y, t] >= min_power(gen[y]) * eTotalCap[y]) + @constraint(EP, + [y in THERM_NO_COMMIT, t in 1:T], + expr[y, t]>=min_power(gen[y]) * eTotalCap[y]) # Maximum power generated per technology "y" at hour "t" and contribution to regulation and reserves up must be < max power expr = extract_time_series_to_expression(vP, THERM_NO_COMMIT) add_similar_to_expression!(expr[REG, :], vREG[REG, :]) add_similar_to_expression!(expr[RSV, :], vRSV[RSV, :]) - @constraint(EP, [y in THERM_NO_COMMIT, t in 1:T], expr[y, t] <= max_power(y, t) * eTotalCap[y]) + @constraint(EP, + [y in THERM_NO_COMMIT, t in 1:T], + expr[y, t]<=max_power(y, t) * eTotalCap[y]) end diff --git a/src/model/resources/vre_stor/vre_stor.jl b/src/model/resources/vre_stor/vre_stor.jl index 7ad0a07dd1..911fdca66b 100644 --- a/src/model/resources/vre_stor/vre_stor.jl +++ b/src/model/resources/vre_stor/vre_stor.jl @@ -79,69 +79,70 @@ The second constraint with both capacity reserve margins and operating reserves The rest of the constraints are dependent upon specific configurable components within the module and are listed below. """ function vre_stor!(EP::Model, inputs::Dict, setup::Dict) - - println("VRE-Storage Module") + println("VRE-Storage Module") ### LOAD DATA ### # Load generators dataframe, sets, and time periods - gen = inputs["RESOURCES"] + gen = inputs["RESOURCES"] - T = inputs["T"] # Number of time steps (hours) - Z = inputs["Z"] # Number of zones + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones # Load VRE-storage inputs - VRE_STOR = inputs["VRE_STOR"] # Set of VRE-STOR generators (indices) + VRE_STOR = inputs["VRE_STOR"] # Set of VRE-STOR generators (indices) gen_VRE_STOR = gen.VreStorage # Set of VRE-STOR generators (objects) SOLAR = inputs["VS_SOLAR"] # Set of VRE-STOR generators with solar-component DC = inputs["VS_DC"] # Set of VRE-STOR generators with inverter-component WIND = inputs["VS_WIND"] # Set of VRE-STOR generators with wind-component STOR = inputs["VS_STOR"] # Set of VRE-STOR generators with storage-component NEW_CAP = intersect(VRE_STOR, inputs["NEW_CAP"]) # Set of VRE-STOR generators eligible for new buildout - + # Policy flags EnergyShareRequirement = setup["EnergyShareRequirement"] - CapacityReserveMargin = setup["CapacityReserveMargin"] + CapacityReserveMargin = setup["CapacityReserveMargin"] MinCapReq = setup["MinCapReq"] MaxCapReq = setup["MaxCapReq"] IncludeLossesInESR = setup["IncludeLossesInESR"] OperationalReserves = setup["OperationalReserves"] - + by_rid(rid, sym) = by_rid_res(rid, sym, gen_VRE_STOR) ### VARIABLES ARE DEFINED IN RESPECTIVE MODULES ### - - ### EXPRESSIONS ### + + ### EXPRESSIONS ### ## 1. Objective Function Expressions ## # Separate grid costs @expression(EP, eCGrid[y in VRE_STOR], if y in NEW_CAP # Resources eligible for new capacity - inv_cost_per_mwyr(gen[y])*EP[:vCAP][y] + fixed_om_cost_per_mwyr(gen[y])*EP[:eTotalCap][y] + inv_cost_per_mwyr(gen[y]) * EP[:vCAP][y] + + fixed_om_cost_per_mwyr(gen[y]) * EP[:eTotalCap][y] else - fixed_om_cost_per_mwyr(gen[y])*EP[:eTotalCap][y] - end - ) + fixed_om_cost_per_mwyr(gen[y]) * EP[:eTotalCap][y] + end) @expression(EP, eTotalCGrid, sum(eCGrid[y] for y in VRE_STOR)) - ## 2. Power Balance Expressions ## + ## 2. Power Balance Expressions ## # Note: The subtraction of the charging component can be found in STOR function - @expression(EP, ePowerBalance_VRE_STOR[t=1:T, z=1:Z], JuMP.AffExpr()) - for t=1:T, z=1:Z + @expression(EP, ePowerBalance_VRE_STOR[t = 1:T, z = 1:Z], JuMP.AffExpr()) + for t in 1:T, z in 1:Z if !isempty(resources_in_zone_by_rid(gen_VRE_STOR, z)) - ePowerBalance_VRE_STOR[t,z] += sum(EP[:vP][y,t] for y=resources_in_zone_by_rid(gen_VRE_STOR, z)) + ePowerBalance_VRE_STOR[t, z] += sum(EP[:vP][y, t] + for y in resources_in_zone_by_rid(gen_VRE_STOR, + z)) end end ## 3. Module Expressions ## # Inverter AC Balance - @expression(EP, eInvACBalance[y in VRE_STOR, t=1:T], JuMP.AffExpr()) + @expression(EP, eInvACBalance[y in VRE_STOR, t = 1:T], JuMP.AffExpr()) # Grid Exports - @expression(EP, eGridExport[y in VRE_STOR, t=1:T], JuMP.AffExpr()) + @expression(EP, eGridExport[y in VRE_STOR, t = 1:T], JuMP.AffExpr()) ### COMPONENT MODULE CONSTRAINTS ### @@ -169,87 +170,111 @@ function vre_stor!(EP::Model, inputs::Dict, setup::Dict) # Energy Share Requirement if EnergyShareRequirement >= 1 - @expression(EP, eESRVREStor[ESR=1:inputs["nESR"]], - sum(inputs["omega"][t]*esr_vrestor(gen[y],tag=ESR)*EP[:vP_SOLAR][y,t]*by_rid(y,:etainverter) - for y=intersect(SOLAR, ids_with_policy(gen, esr_vrestor, tag=ESR)), t=1:T) - + sum(inputs["omega"][t]*esr_vrestor(gen[y],tag=ESR)*EP[:vP_WIND][y,t] - for y=intersect(WIND, ids_with_policy(gen, esr_vrestor, tag=ESR)), t=1:T)) + @expression(EP, eESRVREStor[ESR = 1:inputs["nESR"]], + sum(inputs["omega"][t] * esr_vrestor(gen[y], tag = ESR) * EP[:vP_SOLAR][y, t] * + by_rid(y, :etainverter) + for y in intersect(SOLAR, ids_with_policy(gen, esr_vrestor, tag = ESR)), + t in 1:T) + +sum(inputs["omega"][t] * esr_vrestor(gen[y], tag = ESR) * EP[:vP_WIND][y, t] + for y in intersect(WIND, ids_with_policy(gen, esr_vrestor, tag = ESR)), + t in 1:T)) EP[:eESR] += eESRVREStor if IncludeLossesInESR == 1 - @expression(EP, eESRVREStorLosses[ESR=1:inputs["nESR"]], - sum(inputs["dfESR"][z,ESR]*sum(EP[:eELOSS_VRE_STOR][y] - for y=intersect(STOR, resources_in_zone_by_rid(gen_VRE_STOR, z))) for z=findall(x->x>0,inputs["dfESR"][:,ESR]))) + @expression(EP, eESRVREStorLosses[ESR = 1:inputs["nESR"]], + sum(inputs["dfESR"][z, ESR] * sum(EP[:eELOSS_VRE_STOR][y] + for y in intersect(STOR, resources_in_zone_by_rid(gen_VRE_STOR, z))) + for z in findall(x -> x > 0, inputs["dfESR"][:, ESR]))) EP[:eESR] -= eESRVREStorLosses end end # Minimum Capacity Requirement if MinCapReq == 1 - @expression(EP, eMinCapResSolar[mincap = 1:inputs["NumberOfMinCapReqs"]], - sum(by_rid(y,:etainverter)*EP[:eTotalCap_SOLAR][y] for y in intersect(SOLAR, ids_with_policy(gen_VRE_STOR, min_cap_solar, tag=mincap)))) - EP[:eMinCapRes] += eMinCapResSolar + @expression(EP, eMinCapResSolar[mincap = 1:inputs["NumberOfMinCapReqs"]], + sum(by_rid(y, :etainverter) * EP[:eTotalCap_SOLAR][y] for y in intersect(SOLAR, + ids_with_policy(gen_VRE_STOR, min_cap_solar, tag = mincap)))) + EP[:eMinCapRes] += eMinCapResSolar - @expression(EP, eMinCapResWind[mincap = 1:inputs["NumberOfMinCapReqs"]], - sum(EP[:eTotalCap_WIND][y] for y in intersect(WIND, ids_with_policy(gen_VRE_STOR, min_cap_wind, tag=mincap)))) - EP[:eMinCapRes] += eMinCapResWind + @expression(EP, eMinCapResWind[mincap = 1:inputs["NumberOfMinCapReqs"]], + sum(EP[:eTotalCap_WIND][y] for y in intersect(WIND, + ids_with_policy(gen_VRE_STOR, min_cap_wind, tag = mincap)))) + EP[:eMinCapRes] += eMinCapResWind if !isempty(inputs["VS_ASYM_AC_DISCHARGE"]) - @expression(EP, eMinCapResACDis[mincap = 1:inputs["NumberOfMinCapReqs"]], - sum(EP[:eTotalCapDischarge_AC][y] for y in intersect(inputs["VS_ASYM_AC_DISCHARGE"], ids_with_policy(gen_VRE_STOR, min_cap_stor, tag=mincap)))) - EP[:eMinCapRes] += eMinCapResACDis + @expression(EP, eMinCapResACDis[mincap = 1:inputs["NumberOfMinCapReqs"]], + sum(EP[:eTotalCapDischarge_AC][y] + for y in intersect(inputs["VS_ASYM_AC_DISCHARGE"], + ids_with_policy(gen_VRE_STOR, min_cap_stor, tag = mincap)))) + EP[:eMinCapRes] += eMinCapResACDis end if !isempty(inputs["VS_ASYM_DC_DISCHARGE"]) - @expression(EP, eMinCapResDCDis[mincap = 1:inputs["NumberOfMinCapReqs"]], - sum(EP[:eTotalCapDischarge_DC][y] for y in intersect(inputs["VS_ASYM_DC_DISCHARGE"], ids_with_policy(gen_VRE_STOR, min_cap_stor, tag=mincap)))) - EP[:eMinCapRes] += eMinCapResDCDis + @expression(EP, eMinCapResDCDis[mincap = 1:inputs["NumberOfMinCapReqs"]], + sum(EP[:eTotalCapDischarge_DC][y] + for y in intersect(inputs["VS_ASYM_DC_DISCHARGE"], + ids_with_policy(gen_VRE_STOR, min_cap_stor, tag = mincap)))) + EP[:eMinCapRes] += eMinCapResDCDis end if !isempty(inputs["VS_SYM_AC"]) - @expression(EP, eMinCapResACStor[mincap = 1:inputs["NumberOfMinCapReqs"]], - sum(by_rid(y,:power_to_energy_ac)*EP[:eTotalCap_STOR][y] for y in intersect(inputs["VS_SYM_AC"], ids_with_policy(gen_VRE_STOR, min_cap_stor, tag=mincap)))) - EP[:eMinCapRes] += eMinCapResACStor + @expression(EP, eMinCapResACStor[mincap = 1:inputs["NumberOfMinCapReqs"]], + sum(by_rid(y, :power_to_energy_ac) * EP[:eTotalCap_STOR][y] + for y in intersect(inputs["VS_SYM_AC"], + ids_with_policy(gen_VRE_STOR, min_cap_stor, tag = mincap)))) + EP[:eMinCapRes] += eMinCapResACStor end if !isempty(inputs["VS_SYM_DC"]) - @expression(EP, eMinCapResDCStor[mincap = 1:inputs["NumberOfMinCapReqs"]], - sum(by_rid(y,:power_to_energy_dc)*EP[:eTotalCap_STOR][y] for y in intersect(inputs["VS_SYM_DC"], ids_with_policy(gen_VRE_STOR, min_cap_stor, tag=mincap)))) - EP[:eMinCapRes] += eMinCapResDCStor + @expression(EP, eMinCapResDCStor[mincap = 1:inputs["NumberOfMinCapReqs"]], + sum(by_rid(y, :power_to_energy_dc) * EP[:eTotalCap_STOR][y] + for y in intersect(inputs["VS_SYM_DC"], + ids_with_policy(gen_VRE_STOR, min_cap_stor, tag = mincap)))) + EP[:eMinCapRes] += eMinCapResDCStor end end # Maximum Capacity Requirement if MaxCapReq == 1 - @expression(EP, eMaxCapResSolar[maxcap = 1:inputs["NumberOfMaxCapReqs"]], - sum(by_rid(y,:etainverter)*EP[:eTotalCap_SOLAR][y] for y in intersect(SOLAR, ids_with_policy(gen_VRE_STOR, max_cap_solar, tag=maxcap)))) - EP[:eMaxCapRes] += eMaxCapResSolar + @expression(EP, eMaxCapResSolar[maxcap = 1:inputs["NumberOfMaxCapReqs"]], + sum(by_rid(y, :etainverter) * EP[:eTotalCap_SOLAR][y] for y in intersect(SOLAR, + ids_with_policy(gen_VRE_STOR, max_cap_solar, tag = maxcap)))) + EP[:eMaxCapRes] += eMaxCapResSolar - @expression(EP, eMaxCapResWind[maxcap = 1:inputs["NumberOfMaxCapReqs"]], - sum(EP[:eTotalCap_WIND][y] for y in intersect(WIND, ids_with_policy(gen_VRE_STOR, max_cap_wind, tag=maxcap)))) - EP[:eMaxCapRes] += eMaxCapResWind + @expression(EP, eMaxCapResWind[maxcap = 1:inputs["NumberOfMaxCapReqs"]], + sum(EP[:eTotalCap_WIND][y] for y in intersect(WIND, + ids_with_policy(gen_VRE_STOR, max_cap_wind, tag = maxcap)))) + EP[:eMaxCapRes] += eMaxCapResWind if !isempty(inputs["VS_ASYM_AC_DISCHARGE"]) - @expression(EP, eMaxCapResACDis[maxcap = 1:inputs["NumberOfMaxCapReqs"]], - sum(EP[:eTotalCapDischarge_AC][y] for y in intersect(inputs["VS_ASYM_AC_DISCHARGE"], ids_with_policy(gen_VRE_STOR, max_cap_stor, tag=maxcap)))) - EP[:eMaxCapRes] += eMaxCapResACDis + @expression(EP, eMaxCapResACDis[maxcap = 1:inputs["NumberOfMaxCapReqs"]], + sum(EP[:eTotalCapDischarge_AC][y] + for y in intersect(inputs["VS_ASYM_AC_DISCHARGE"], + ids_with_policy(gen_VRE_STOR, max_cap_stor, tag = maxcap)))) + EP[:eMaxCapRes] += eMaxCapResACDis end if !isempty(inputs["VS_ASYM_DC_DISCHARGE"]) - @expression(EP, eMaxCapResDCDis[maxcap = 1:inputs["NumberOfMaxCapReqs"]], - sum(EP[:eTotalCapDischarge_DC][y] for y in intersect(inputs["VS_ASYM_DC_DISCHARGE"], ids_with_policy(gen_VRE_STOR, max_cap_stor, tag=maxcap)))) - EP[:eMaxCapRes] += eMaxCapResDCDis + @expression(EP, eMaxCapResDCDis[maxcap = 1:inputs["NumberOfMaxCapReqs"]], + sum(EP[:eTotalCapDischarge_DC][y] + for y in intersect(inputs["VS_ASYM_DC_DISCHARGE"], + ids_with_policy(gen_VRE_STOR, max_cap_stor, tag = maxcap)))) + EP[:eMaxCapRes] += eMaxCapResDCDis end if !isempty(inputs["VS_SYM_AC"]) - @expression(EP, eMaxCapResACStor[maxcap = 1:inputs["NumberOfMaxCapReqs"]], - sum(by_rid(y,:power_to_energy_ac)*EP[:eTotalCap_STOR][y] for y in intersect(inputs["VS_SYM_AC"], ids_with_policy(gen_VRE_STOR, max_cap_stor, tag=maxcap)))) - EP[:eMaxCapRes] += eMaxCapResACStor + @expression(EP, eMaxCapResACStor[maxcap = 1:inputs["NumberOfMaxCapReqs"]], + sum(by_rid(y, :power_to_energy_ac) * EP[:eTotalCap_STOR][y] + for y in intersect(inputs["VS_SYM_AC"], + ids_with_policy(gen_VRE_STOR, max_cap_stor, tag = maxcap)))) + EP[:eMaxCapRes] += eMaxCapResACStor end if !isempty(inputs["VS_SYM_DC"]) - @expression(EP, eMaxCapResDCStor[maxcap = 1:inputs["NumberOfMaxCapReqs"]], - sum(by_rid(y,:power_to_energy_dc)*EP[:eTotalCap_STOR][y] for y in intersect(inputs["VS_SYM_DC"], ids_with_policy(gen_VRE_STOR, max_cap_stor, tag=maxcap)))) - EP[:eMaxCapRes] += eMaxCapResDCStor + @expression(EP, eMaxCapResDCStor[maxcap = 1:inputs["NumberOfMaxCapReqs"]], + sum(by_rid(y, :power_to_energy_dc) * EP[:eTotalCap_STOR][y] + for y in intersect(inputs["VS_SYM_DC"], + ids_with_policy(gen_VRE_STOR, max_cap_stor, tag = maxcap)))) + EP[:eMaxCapRes] += eMaxCapResDCStor end end @@ -269,33 +294,49 @@ function vre_stor!(EP::Model, inputs::Dict, setup::Dict) ### CONSTRAINTS ### # Constraint 1: Energy Balance Constraint - @constraint(EP, cEnergyBalance[y in VRE_STOR, t=1:T], - EP[:vP][y,t] == eInvACBalance[y,t]) - + @constraint(EP, cEnergyBalance[y in VRE_STOR, t = 1:T], + EP[:vP][y, t]==eInvACBalance[y, t]) + # Constraint 2: Grid Export/Import Maximum - @constraint(EP, cGridExport[y in VRE_STOR, t=1:T], - EP[:vP][y,t] + eGridExport[y,t] <= EP[:eTotalCap][y]) - + @constraint(EP, cGridExport[y in VRE_STOR, t = 1:T], + EP[:vP][y, t] + eGridExport[y, t]<=EP[:eTotalCap][y]) + # Constraint 3: Inverter Export/Import Maximum (implemented in main module due to potential capacity reserve margin and operating reserve constraints) - @constraint(EP, cInverterExport[y in DC, t=1:T], EP[:eInverterExport][y,t] <= EP[:eTotalCap_DC][y]) + @constraint(EP, + cInverterExport[y in DC, t = 1:T], + EP[:eInverterExport][y, t]<=EP[:eTotalCap_DC][y]) # Constraint 4: PV Generation (implemented in main module due to potential capacity reserve margin and operating reserve constraints) - @constraint(EP, cSolarGenMaxS[y in SOLAR, t=1:T], EP[:eSolarGenMaxS][y,t] <= inputs["pP_Max_Solar"][y,t]*EP[:eTotalCap_SOLAR][y]) + @constraint(EP, + cSolarGenMaxS[y in SOLAR, t = 1:T], + EP[:eSolarGenMaxS][y, t]<=inputs["pP_Max_Solar"][y, t] * EP[:eTotalCap_SOLAR][y]) # Constraint 5: Wind Generation (implemented in main module due to potential capacity reserve margin and operating reserve constraints) - @constraint(EP, cWindGenMaxW[y in WIND, t=1:T], EP[:eWindGenMaxW][y,t] <= inputs["pP_Max_Wind"][y,t]*EP[:eTotalCap_WIND][y]) + @constraint(EP, + cWindGenMaxW[y in WIND, t = 1:T], + EP[:eWindGenMaxW][y, t]<=inputs["pP_Max_Wind"][y, t] * EP[:eTotalCap_WIND][y]) # Constraint 6: Symmetric Storage Resources (implemented in main module due to potential capacity reserve margin and operating reserve constraints) - @constraint(EP, cChargeDischargeMaxDC[y in inputs["VS_SYM_DC"], t=1:T], - EP[:eChargeDischargeMaxDC][y,t] <= by_rid(y,:power_to_energy_dc)*EP[:eTotalCap_STOR][y]) - @constraint(EP, cChargeDischargeMaxAC[y in inputs["VS_SYM_AC"], t=1:T], - EP[:eChargeDischargeMaxAC][y,t] <= by_rid(y,:power_to_energy_ac)*EP[:eTotalCap_STOR][y]) + @constraint(EP, cChargeDischargeMaxDC[y in inputs["VS_SYM_DC"], t = 1:T], + EP[:eChargeDischargeMaxDC][y, + t]<=by_rid(y, :power_to_energy_dc) * EP[:eTotalCap_STOR][y]) + @constraint(EP, cChargeDischargeMaxAC[y in inputs["VS_SYM_AC"], t = 1:T], + EP[:eChargeDischargeMaxAC][y, + t]<=by_rid(y, :power_to_energy_ac) * EP[:eTotalCap_STOR][y]) # Constraint 7: Asymmetric Storage Resources (implemented in main module due to potential capacity reserve margin and operating reserve constraints) - @constraint(EP, cVreStorMaxDischargingDC[y in inputs["VS_ASYM_DC_DISCHARGE"], t=1:T], EP[:eVreStorMaxDischargingDC][y,t] <= EP[:eTotalCapDischarge_DC][y]) - @constraint(EP, cVreStorMaxChargingDC[y in inputs["VS_ASYM_DC_CHARGE"], t=1:T], EP[:eVreStorMaxChargingDC][y,t] <= EP[:eTotalCapCharge_DC][y]) - @constraint(EP, cVreStorMaxDischargingAC[y in inputs["VS_ASYM_AC_DISCHARGE"], t=1:T], EP[:eVreStorMaxDischargingAC][y,t] <= EP[:eTotalCapDischarge_AC][y]) - @constraint(EP, cVreStorMaxChargingAC[y in inputs["VS_ASYM_AC_CHARGE"], t=1:T], EP[:eVreStorMaxChargingAC][y,t] <= EP[:eTotalCapCharge_AC][y]) + @constraint(EP, + cVreStorMaxDischargingDC[y in inputs["VS_ASYM_DC_DISCHARGE"], t = 1:T], + EP[:eVreStorMaxDischargingDC][y, t]<=EP[:eTotalCapDischarge_DC][y]) + @constraint(EP, + cVreStorMaxChargingDC[y in inputs["VS_ASYM_DC_CHARGE"], t = 1:T], + EP[:eVreStorMaxChargingDC][y, t]<=EP[:eTotalCapCharge_DC][y]) + @constraint(EP, + cVreStorMaxDischargingAC[y in inputs["VS_ASYM_AC_DISCHARGE"], t = 1:T], + EP[:eVreStorMaxDischargingAC][y, t]<=EP[:eTotalCapDischarge_AC][y]) + @constraint(EP, + cVreStorMaxChargingAC[y in inputs["VS_ASYM_AC_CHARGE"], t = 1:T], + EP[:eVreStorMaxChargingAC][y, t]<=EP[:eTotalCapCharge_AC][y]) end @doc raw""" @@ -371,7 +412,6 @@ In addition, this function adds investment and fixed O&M related costs related t ``` """ function inverter_vre_stor!(EP::Model, inputs::Dict, setup::Dict) - println("VRE-STOR Inverter Module") ### LOAD DATA ### @@ -382,7 +422,7 @@ function inverter_vre_stor!(EP::Model, inputs::Dict, setup::Dict) RET_CAP_DC = inputs["RET_CAP_DC"] gen = inputs["RESOURCES"] gen_VRE_STOR = gen.VreStorage - + MultiStage = setup["MultiStage"] by_rid(rid, sym) = by_rid_res(rid, sym, gen_VRE_STOR) @@ -396,72 +436,73 @@ function inverter_vre_stor!(EP::Model, inputs::Dict, setup::Dict) end) if MultiStage == 1 - @variable(EP, vEXISTINGDCCAP[y in DC] >= 0); + @variable(EP, vEXISTINGDCCAP[y in DC]>=0) end ### EXPRESSIONS ### # 0. Multistage existing capacity definition if MultiStage == 1 - @expression(EP, eExistingCapDC[y in DC], vEXISTINGDCCAP[y]) - else - @expression(EP, eExistingCapDC[y in DC], by_rid(y,:existing_cap_inverter_mw)) - end + @expression(EP, eExistingCapDC[y in DC], vEXISTINGDCCAP[y]) + else + @expression(EP, eExistingCapDC[y in DC], by_rid(y, :existing_cap_inverter_mw)) + end # 1. Total inverter capacity @expression(EP, eTotalCap_DC[y in DC], - if (y in intersect(NEW_CAP_DC, RET_CAP_DC)) # Resources eligible for new capacity and retirements - eExistingCapDC[y] + EP[:vDCCAP][y] - EP[:vRETDCCAP][y] - elseif (y in setdiff(NEW_CAP_DC, RET_CAP_DC)) # Resources eligible for only new capacity - eExistingCapDC[y] + EP[:vDCCAP][y] - elseif (y in setdiff(RET_CAP_DC, NEW_CAP_DC)) # Resources eligible for only capacity retirements - eExistingCapDC[y] - EP[:vRETDCCAP][y] - else - eExistingCapDC[y] - end - ) + if (y in intersect(NEW_CAP_DC, RET_CAP_DC)) # Resources eligible for new capacity and retirements + eExistingCapDC[y] + EP[:vDCCAP][y] - EP[:vRETDCCAP][y] + elseif (y in setdiff(NEW_CAP_DC, RET_CAP_DC)) # Resources eligible for only new capacity + eExistingCapDC[y] + EP[:vDCCAP][y] + elseif (y in setdiff(RET_CAP_DC, NEW_CAP_DC)) # Resources eligible for only capacity retirements + eExistingCapDC[y] - EP[:vRETDCCAP][y] + else + eExistingCapDC[y] + end) # 2. Objective function additions # Fixed costs for inverter component (if resource is not eligible for new inverter capacity, fixed costs are only O&M costs) @expression(EP, eCFixDC[y in DC], if y in NEW_CAP_DC # Resources eligible for new capacity - by_rid(y,:inv_cost_inverter_per_mwyr)*vDCCAP[y] + by_rid(y,:fixed_om_inverter_cost_per_mwyr)*eTotalCap_DC[y] + by_rid(y, :inv_cost_inverter_per_mwyr) * vDCCAP[y] + + by_rid(y, :fixed_om_inverter_cost_per_mwyr) * eTotalCap_DC[y] else - by_rid(y,:fixed_om_inverter_cost_per_mwyr)*eTotalCap_DC[y] - end - ) - + by_rid(y, :fixed_om_inverter_cost_per_mwyr) * eTotalCap_DC[y] + end) + # Sum individual resource contributions @expression(EP, eTotalCFixDC, sum(eCFixDC[y] for y in DC)) if MultiStage == 1 - EP[:eObj] += eTotalCFixDC/inputs["OPEXMULT"] + EP[:eObj] += eTotalCFixDC / inputs["OPEXMULT"] else EP[:eObj] += eTotalCFixDC end # 3. Inverter exports expression - @expression(EP, eInverterExport[y in DC, t=1:T], JuMP.AffExpr()) + @expression(EP, eInverterExport[y in DC, t = 1:T], JuMP.AffExpr()) ### CONSTRAINTS ### # Constraint 0: Existing capacity variable is equal to existing capacity specified in the input file if MultiStage == 1 - @constraint(EP, cExistingCapDC[y in DC], EP[:vEXISTINGDCCAP][y] == by_rid(y,:existing_cap_inverter_mw)) + @constraint(EP, + cExistingCapDC[y in DC], + EP[:vEXISTINGDCCAP][y]==by_rid(y, :existing_cap_inverter_mw)) end # Constraints 1: Retirements and capacity additions # Cannot retire more capacity than existing capacity for VRE-STOR technologies - @constraint(EP, cMaxRet_DC[y=RET_CAP_DC], vRETDCCAP[y] <= eExistingCapDC[y]) + @constraint(EP, cMaxRet_DC[y = RET_CAP_DC], vRETDCCAP[y]<=eExistingCapDC[y]) # Constraint on maximum capacity (if applicable) [set input to -1 if no constraint on maximum capacity] - # DEV NOTE: This constraint may be violated in some cases where Existing_Cap_MW is >= Max_Cap_MW and lead to infeasabilty - @constraint(EP, cMaxCap_DC[y in ids_with_nonneg(gen_VRE_STOR, max_cap_inverter_mw)], - eTotalCap_DC[y] <= by_rid(y,:max_cap_inverter_mw)) + # DEV NOTE: This constraint may be violated in some cases where Existing_Cap_MW is >= Max_Cap_MW and lead to infeasabilty + @constraint(EP, cMaxCap_DC[y in ids_with_nonneg(gen_VRE_STOR, max_cap_inverter_mw)], + eTotalCap_DC[y]<=by_rid(y, :max_cap_inverter_mw)) # Constraint on Minimum capacity (if applicable) [set input to -1 if no constraint on minimum capacity] # DEV NOTE: This constraint may be violated in some cases where Existing_Cap_MW is <= Min_Cap_MW and lead to infeasabilty - @constraint(EP, cMinCap_DC[y in ids_with_positive(gen_VRE_STOR, min_cap_inverter_mw)], - eTotalCap_DC[y] >= by_rid(y,:min_cap_inverter_mw)) + @constraint(EP, cMinCap_DC[y in ids_with_positive(gen_VRE_STOR, min_cap_inverter_mw)], + eTotalCap_DC[y]>=by_rid(y, :min_cap_inverter_mw)) # Constraint 2: Inverter Exports Maximum: see main module because capacity reserve margin/operating reserves may alter constraint end @@ -530,7 +571,6 @@ In addition, this function adds investment, fixed O&M, and variable O&M costs re ``` """ function solar_vre_stor!(EP::Model, inputs::Dict, setup::Dict) - println("VRE-STOR Solar Module") ### LOAD DATA ### @@ -554,91 +594,94 @@ function solar_vre_stor!(EP::Model, inputs::Dict, setup::Dict) vSOLARCAP[y in NEW_CAP_SOLAR] >= 0 # New installed solar capacity [MW DC] # Solar-component generation [MWh] - vP_SOLAR[y in SOLAR, t=1:T] >= 0 + vP_SOLAR[y in SOLAR, t = 1:T] >= 0 end) if MultiStage == 1 - @variable(EP, vEXISTINGSOLARCAP[y in SOLAR] >= 0); + @variable(EP, vEXISTINGSOLARCAP[y in SOLAR]>=0) end ### EXPRESSIONS ### # 0. Multistage existing capacity definition if MultiStage == 1 - @expression(EP, eExistingCapSolar[y in SOLAR], vEXISTINGSOLARCAP[y]) - else - @expression(EP, eExistingCapSolar[y in SOLAR], by_rid(y,:existing_cap_solar_mw)) - end + @expression(EP, eExistingCapSolar[y in SOLAR], vEXISTINGSOLARCAP[y]) + else + @expression(EP, eExistingCapSolar[y in SOLAR], by_rid(y, :existing_cap_solar_mw)) + end # 1. Total solar capacity @expression(EP, eTotalCap_SOLAR[y in SOLAR], - if (y in intersect(NEW_CAP_SOLAR, RET_CAP_SOLAR)) # Resources eligible for new capacity and retirements - eExistingCapSolar[y] + EP[:vSOLARCAP][y] - EP[:vRETSOLARCAP][y] - elseif (y in setdiff(NEW_CAP_SOLAR, RET_CAP_SOLAR)) # Resources eligible for only new capacity - eExistingCapSolar[y] + EP[:vSOLARCAP][y] - elseif (y in setdiff(RET_CAP_SOLAR, NEW_CAP_SOLAR)) # Resources eligible for only capacity retirements - eExistingCapSolar[y] - EP[:vRETSOLARCAP][y] - else - eExistingCapSolar[y] - end - ) + if (y in intersect(NEW_CAP_SOLAR, RET_CAP_SOLAR)) # Resources eligible for new capacity and retirements + eExistingCapSolar[y] + EP[:vSOLARCAP][y] - EP[:vRETSOLARCAP][y] + elseif (y in setdiff(NEW_CAP_SOLAR, RET_CAP_SOLAR)) # Resources eligible for only new capacity + eExistingCapSolar[y] + EP[:vSOLARCAP][y] + elseif (y in setdiff(RET_CAP_SOLAR, NEW_CAP_SOLAR)) # Resources eligible for only capacity retirements + eExistingCapSolar[y] - EP[:vRETSOLARCAP][y] + else + eExistingCapSolar[y] + end) # 2. Objective function additions # Fixed costs for solar resources (if resource is not eligible for new solar capacity, fixed costs are only O&M costs) @expression(EP, eCFixSolar[y in SOLAR], if y in NEW_CAP_SOLAR # Resources eligible for new capacity - by_rid(y,:inv_cost_solar_per_mwyr)*vSOLARCAP[y] + by_rid(y,:fixed_om_solar_cost_per_mwyr)*eTotalCap_SOLAR[y] + by_rid(y, :inv_cost_solar_per_mwyr) * vSOLARCAP[y] + + by_rid(y, :fixed_om_solar_cost_per_mwyr) * eTotalCap_SOLAR[y] else - by_rid(y,:fixed_om_solar_cost_per_mwyr)*eTotalCap_SOLAR[y] - end - ) + by_rid(y, :fixed_om_solar_cost_per_mwyr) * eTotalCap_SOLAR[y] + end) @expression(EP, eTotalCFixSolar, sum(eCFixSolar[y] for y in SOLAR)) if MultiStage == 1 - EP[:eObj] += eTotalCFixSolar/inputs["OPEXMULT"] + EP[:eObj] += eTotalCFixSolar / inputs["OPEXMULT"] else EP[:eObj] += eTotalCFixSolar end # Variable costs of "generation" for solar resource "y" during hour "t" - @expression(EP, eCVarOutSolar[y in SOLAR, t=1:T], - inputs["omega"][t]*by_rid(y,:var_om_cost_per_mwh_solar)*by_rid(y,:etainverter)*EP[:vP_SOLAR][y,t]) - @expression(EP, eTotalCVarOutSolar, sum(eCVarOutSolar[y,t] for y in SOLAR, t=1:T)) + @expression(EP, eCVarOutSolar[y in SOLAR, t = 1:T], + inputs["omega"][t]*by_rid(y, :var_om_cost_per_mwh_solar)*by_rid(y, :etainverter)* + EP[:vP_SOLAR][y, t]) + @expression(EP, eTotalCVarOutSolar, sum(eCVarOutSolar[y, t] for y in SOLAR, t in 1:T)) EP[:eObj] += eTotalCVarOutSolar # 3. Inverter Balance, PV Generation Maximum - @expression(EP, eSolarGenMaxS[y in SOLAR, t=1:T], JuMP.AffExpr()) - for y in SOLAR, t=1:T - EP[:eInvACBalance][y,t] += by_rid(y,:etainverter)*EP[:vP_SOLAR][y,t] - EP[:eInverterExport][y,t] += by_rid(y,:etainverter)*EP[:vP_SOLAR][y,t] - eSolarGenMaxS[y,t] += EP[:vP_SOLAR][y,t] + @expression(EP, eSolarGenMaxS[y in SOLAR, t = 1:T], JuMP.AffExpr()) + for y in SOLAR, t in 1:T + EP[:eInvACBalance][y, t] += by_rid(y, :etainverter) * EP[:vP_SOLAR][y, t] + EP[:eInverterExport][y, t] += by_rid(y, :etainverter) * EP[:vP_SOLAR][y, t] + eSolarGenMaxS[y, t] += EP[:vP_SOLAR][y, t] end ### CONSTRAINTS ### # Constraint 0: Existing capacity variable is equal to existing capacity specified in the input file if MultiStage == 1 - @constraint(EP, cExistingCapSolar[y in SOLAR], EP[:vEXISTINGSOLARCAP][y] == by_rid(y,:existing_cap_solar_mw)) - end + @constraint(EP, + cExistingCapSolar[y in SOLAR], + EP[:vEXISTINGSOLARCAP][y]==by_rid(y, :existing_cap_solar_mw)) + end # Constraints 1: Retirements and capacity additions # Cannot retire more capacity than existing capacity for VRE-STOR technologies - @constraint(EP, cMaxRet_Solar[y=RET_CAP_SOLAR], vRETSOLARCAP[y] <= eExistingCapSolar[y]) + @constraint(EP, cMaxRet_Solar[y = RET_CAP_SOLAR], vRETSOLARCAP[y]<=eExistingCapSolar[y]) # Constraint on maximum capacity (if applicable) [set input to -1 if no constraint on maximum capacity] - # DEV NOTE: This constraint may be violated in some cases where Existing_Cap_MW is >= Max_Cap_MW and lead to infeasabilty - @constraint(EP, cMaxCap_Solar[y in ids_with_nonneg(gen_VRE_STOR, max_cap_solar_mw)], - eTotalCap_SOLAR[y] <= by_rid(y,:max_cap_solar_mw)) + # DEV NOTE: This constraint may be violated in some cases where Existing_Cap_MW is >= Max_Cap_MW and lead to infeasabilty + @constraint(EP, cMaxCap_Solar[y in ids_with_nonneg(gen_VRE_STOR, max_cap_solar_mw)], + eTotalCap_SOLAR[y]<=by_rid(y, :max_cap_solar_mw)) # Constraint on Minimum capacity (if applicable) [set input to -1 if no constraint on minimum capacity] # DEV NOTE: This constraint may be violated in some cases where Existing_Cap_MW is <= Min_Cap_MW and lead to infeasabilty - @constraint(EP, cMinCap_Solar[y in ids_with_positive(gen_VRE_STOR, min_cap_solar_mw)], - eTotalCap_SOLAR[y] >= by_rid(y,:min_cap_solar_mw)) + @constraint(EP, cMinCap_Solar[y in ids_with_positive(gen_VRE_STOR, min_cap_solar_mw)], + eTotalCap_SOLAR[y]>=by_rid(y, :min_cap_solar_mw)) # Constraint 2: PV Generation: see main module because operating reserves may alter constraint # Constraint 3: Inverter Ratio between solar capacity and grid - @constraint(EP, cInverterRatio_Solar[y in ids_with_positive(gen_VRE_STOR, inverter_ratio_solar)], - EP[:eTotalCap_SOLAR][y] == by_rid(y,:inverter_ratio_solar)*EP[:eTotalCap_DC][y]) + @constraint(EP, + cInverterRatio_Solar[y in ids_with_positive(gen_VRE_STOR, inverter_ratio_solar)], + EP[:eTotalCap_SOLAR][y]==by_rid(y, :inverter_ratio_solar) * EP[:eTotalCap_DC][y]) end @doc raw""" @@ -705,7 +748,6 @@ In addition, this function adds investment, fixed O&M, and variable O&M costs re ``` """ function wind_vre_stor!(EP::Model, inputs::Dict, setup::Dict) - println("VRE-STOR Wind Module") ### LOAD DATA ### @@ -729,89 +771,93 @@ function wind_vre_stor!(EP::Model, inputs::Dict, setup::Dict) vWINDCAP[y in NEW_CAP_WIND] >= 0 # New installed wind capacity [MW AC] # Wind-component generation [MWh] - vP_WIND[y in WIND, t=1:T] >= 0 + vP_WIND[y in WIND, t = 1:T] >= 0 end) if MultiStage == 1 - @variable(EP, vEXISTINGWINDCAP[y in WIND] >= 0); - end + @variable(EP, vEXISTINGWINDCAP[y in WIND]>=0) + end ### EXPRESSIONS ### # 0. Multistage existing capacity definition if MultiStage == 1 - @expression(EP, eExistingCapWind[y in WIND], vEXISTINGWINDCAP[y]) - else - @expression(EP, eExistingCapWind[y in WIND], by_rid(y,:existing_cap_wind_mw)) - end + @expression(EP, eExistingCapWind[y in WIND], vEXISTINGWINDCAP[y]) + else + @expression(EP, eExistingCapWind[y in WIND], by_rid(y, :existing_cap_wind_mw)) + end # 1. Total wind capacity @expression(EP, eTotalCap_WIND[y in WIND], - if (y in intersect(NEW_CAP_WIND, RET_CAP_WIND)) # Resources eligible for new capacity and retirements - eExistingCapWind[y] + EP[:vWINDCAP][y] - EP[:vRETWINDCAP][y] - elseif (y in setdiff(NEW_CAP_WIND, RET_CAP_WIND)) # Resources eligible for only new capacity - eExistingCapWind[y] + EP[:vWINDCAP][y] - elseif (y in setdiff(RET_CAP_WIND, NEW_CAP_WIND)) # Resources eligible for only capacity retirements - eExistingCapWind[y] - EP[:vRETWINDCAP][y] - else - eExistingCapWind[y] - end - ) + if (y in intersect(NEW_CAP_WIND, RET_CAP_WIND)) # Resources eligible for new capacity and retirements + eExistingCapWind[y] + EP[:vWINDCAP][y] - EP[:vRETWINDCAP][y] + elseif (y in setdiff(NEW_CAP_WIND, RET_CAP_WIND)) # Resources eligible for only new capacity + eExistingCapWind[y] + EP[:vWINDCAP][y] + elseif (y in setdiff(RET_CAP_WIND, NEW_CAP_WIND)) # Resources eligible for only capacity retirements + eExistingCapWind[y] - EP[:vRETWINDCAP][y] + else + eExistingCapWind[y] + end) # 2. Objective function additions # Fixed costs for wind resources (if resource is not eligible for new wind capacity, fixed costs are only O&M costs) @expression(EP, eCFixWind[y in WIND], if y in NEW_CAP_WIND # Resources eligible for new capacity - by_rid(y,:inv_cost_wind_per_mwyr)*vWINDCAP[y] + by_rid(y,:fixed_om_wind_cost_per_mwyr)*eTotalCap_WIND[y] + by_rid(y, :inv_cost_wind_per_mwyr) * vWINDCAP[y] + + by_rid(y, :fixed_om_wind_cost_per_mwyr) * eTotalCap_WIND[y] else - by_rid(y,:fixed_om_wind_cost_per_mwyr)*eTotalCap_WIND[y] - end - ) + by_rid(y, :fixed_om_wind_cost_per_mwyr) * eTotalCap_WIND[y] + end) @expression(EP, eTotalCFixWind, sum(eCFixWind[y] for y in WIND)) if MultiStage == 1 - EP[:eObj] += eTotalCFixWind/inputs["OPEXMULT"] + EP[:eObj] += eTotalCFixWind / inputs["OPEXMULT"] else EP[:eObj] += eTotalCFixWind end # Variable costs of "generation" for wind resource "y" during hour "t" - @expression(EP, eCVarOutWind[y in WIND, t=1:T], inputs["omega"][t]*by_rid(y,:var_om_cost_per_mwh_wind)*EP[:vP_WIND][y,t]) - @expression(EP, eTotalCVarOutWind, sum(eCVarOutWind[y,t] for y in WIND, t=1:T)) + @expression(EP, + eCVarOutWind[y in WIND, t = 1:T], + inputs["omega"][t]*by_rid(y, :var_om_cost_per_mwh_wind)*EP[:vP_WIND][y, t]) + @expression(EP, eTotalCVarOutWind, sum(eCVarOutWind[y, t] for y in WIND, t in 1:T)) EP[:eObj] += eTotalCVarOutWind # 3. Inverter Balance, Wind Generation Maximum - @expression(EP, eWindGenMaxW[y in WIND, t=1:T], JuMP.AffExpr()) - for y in WIND, t=1:T - EP[:eInvACBalance][y,t] += EP[:vP_WIND][y,t] - eWindGenMaxW[y,t] += EP[:vP_WIND][y,t] + @expression(EP, eWindGenMaxW[y in WIND, t = 1:T], JuMP.AffExpr()) + for y in WIND, t in 1:T + EP[:eInvACBalance][y, t] += EP[:vP_WIND][y, t] + eWindGenMaxW[y, t] += EP[:vP_WIND][y, t] end ### CONSTRAINTS ### # Constraint 0: Existing capacity variable is equal to existing capacity specified in the input file if MultiStage == 1 - @constraint(EP, cExistingCapWind[y in WIND], EP[:vEXISTINGWINDCAP][y] == by_rid(y,:existing_cap_wind_mw)) - end + @constraint(EP, + cExistingCapWind[y in WIND], + EP[:vEXISTINGWINDCAP][y]==by_rid(y, :existing_cap_wind_mw)) + end # Constraints 1: Retirements and capacity additions # Cannot retire more capacity than existing capacity for VRE-STOR technologies - @constraint(EP, cMaxRet_Wind[y=RET_CAP_WIND], vRETWINDCAP[y] <= eExistingCapWind[y]) + @constraint(EP, cMaxRet_Wind[y = RET_CAP_WIND], vRETWINDCAP[y]<=eExistingCapWind[y]) # Constraint on maximum capacity (if applicable) [set input to -1 if no constraint on maximum capacity] - # DEV NOTE: This constraint may be violated in some cases where Existing_Cap_MW is >= Max_Cap_MW and lead to infeasabilty - @constraint(EP, cMaxCap_Wind[y in ids_with_nonneg(gen_VRE_STOR, max_cap_wind_mw)], - eTotalCap_WIND[y] <= by_rid(y,:max_cap_wind_mw)) + # DEV NOTE: This constraint may be violated in some cases where Existing_Cap_MW is >= Max_Cap_MW and lead to infeasabilty + @constraint(EP, cMaxCap_Wind[y in ids_with_nonneg(gen_VRE_STOR, max_cap_wind_mw)], + eTotalCap_WIND[y]<=by_rid(y, :max_cap_wind_mw)) # Constraint on Minimum capacity (if applicable) [set input to -1 if no constraint on minimum capacity] # DEV NOTE: This constraint may be violated in some cases where Existing_Cap_MW is <= Min_Cap_MW and lead to infeasabilty - @constraint(EP, cMinCap_Wind[y in ids_with_positive(gen_VRE_STOR, min_cap_wind_mw)], - eTotalCap_WIND[y] >= by_rid(y,:min_cap_wind_mw)) + @constraint(EP, cMinCap_Wind[y in ids_with_positive(gen_VRE_STOR, min_cap_wind_mw)], + eTotalCap_WIND[y]>=by_rid(y, :min_cap_wind_mw)) # Constraint 2: Wind Generation: see main module because capacity reserve margin/operating reserves may alter constraint # Constraint 3: Inverter Ratio between wind capacity and grid - @constraint(EP, cInverterRatio_Wind[y in ids_with_positive(gen_VRE_STOR, inverter_ratio_wind)], - EP[:eTotalCap_WIND][y] == by_rid(y,:inverter_ratio_wind)*EP[:eTotalCap][y]) + @constraint(EP, + cInverterRatio_Wind[y in ids_with_positive(gen_VRE_STOR, inverter_ratio_wind)], + EP[:eTotalCap_WIND][y]==by_rid(y, :inverter_ratio_wind) * EP[:eTotalCap][y]) end @doc raw""" @@ -941,12 +987,11 @@ In addition, this function adds investment, fixed O&M, and variable O&M costs re ``` """ function stor_vre_stor!(EP::Model, inputs::Dict, setup::Dict) - println("VRE-STOR Storage Module") ### LOAD DATA ### - T = inputs["T"] + T = inputs["T"] Z = inputs["Z"] gen = inputs["RESOURCES"] @@ -964,8 +1009,8 @@ function stor_vre_stor!(EP::Model, inputs::Dict, setup::Dict) VS_LDS = inputs["VS_LDS"] START_SUBPERIODS = inputs["START_SUBPERIODS"] - INTERIOR_SUBPERIODS = inputs["INTERIOR_SUBPERIODS"] - hours_per_subperiod = inputs["hours_per_subperiod"] # total number of hours per subperiod + INTERIOR_SUBPERIODS = inputs["INTERIOR_SUBPERIODS"] + hours_per_subperiod = inputs["hours_per_subperiod"] # total number of hours per subperiod rep_periods = inputs["REP_PERIOD"] MultiStage = setup["MultiStage"] @@ -978,104 +1023,110 @@ function stor_vre_stor!(EP::Model, inputs::Dict, setup::Dict) # Storage energy capacity vCAPENERGY_VS[y in NEW_CAP_STOR] >= 0 # Energy storage reservoir capacity (MWh capacity) built for VRE storage [MWh] vRETCAPENERGY_VS[y in RET_CAP_STOR] >= 0 # Energy storage reservoir capacity retired for VRE storage [MWh] - + # State of charge variable - vS_VRE_STOR[y in STOR, t=1:T] >= 0 # Storage level of resource "y" at hour "t" [MWh] on zone "z" + vS_VRE_STOR[y in STOR, t = 1:T] >= 0 # Storage level of resource "y" at hour "t" [MWh] on zone "z" # DC-battery discharge [MWh] - vP_DC_DISCHARGE[y in DC_DISCHARGE, t=1:T] >= 0 + vP_DC_DISCHARGE[y in DC_DISCHARGE, t = 1:T] >= 0 # DC-battery charge [MWh] - vP_DC_CHARGE[y in DC_CHARGE, t=1:T] >= 0 + vP_DC_CHARGE[y in DC_CHARGE, t = 1:T] >= 0 # AC-battery discharge [MWh] - vP_AC_DISCHARGE[y in AC_DISCHARGE, t=1:T] >= 0 + vP_AC_DISCHARGE[y in AC_DISCHARGE, t = 1:T] >= 0 # AC-battery charge [MWh] - vP_AC_CHARGE[y in AC_CHARGE, t=1:T] >= 0 + vP_AC_CHARGE[y in AC_CHARGE, t = 1:T] >= 0 # Grid-interfacing charge (Energy withdrawn from grid by resource VRE_STOR at hour "t") [MWh] - vCHARGE_VRE_STOR[y in STOR, t=1:T] >= 0 + vCHARGE_VRE_STOR[y in STOR, t = 1:T] >= 0 end) if MultiStage == 1 - @variable(EP, vEXISTINGCAPENERGY_VS[y in STOR] >= 0); - end + @variable(EP, vEXISTINGCAPENERGY_VS[y in STOR]>=0) + end ### EXPRESSIONS ### # 0. Multistage existing capacity definition if MultiStage == 1 - @expression(EP, eExistingCapEnergy_VS[y in STOR], vEXISTINGCAPENERGY_VS[y]) - else - @expression(EP, eExistingCapEnergy_VS[y in STOR], existing_cap_mwh(gen[y])) - end + @expression(EP, eExistingCapEnergy_VS[y in STOR], vEXISTINGCAPENERGY_VS[y]) + else + @expression(EP, eExistingCapEnergy_VS[y in STOR], existing_cap_mwh(gen[y])) + end # 1. Total storage energy capacity @expression(EP, eTotalCap_STOR[y in STOR], - if (y in intersect(NEW_CAP_STOR, RET_CAP_STOR)) # Resources eligible for new capacity and retirements - eExistingCapEnergy_VS[y] + EP[:vCAPENERGY_VS][y] - EP[:vRETCAPENERGY_VS][y] - elseif (y in setdiff(NEW_CAP_STOR, RET_CAP_STOR)) # Resources eligible for only new capacity - eExistingCapEnergy_VS[y] + EP[:vCAPENERGY_VS][y] - elseif (y in setdiff(RET_CAP_STOR, NEW_CAP_STOR)) # Resources eligible for only capacity retirements - eExistingCapEnergy_VS[y] - EP[:vRETCAPENERGY_VS][y] - else - eExistingCapEnergy_VS[y] - end - ) + if (y in intersect(NEW_CAP_STOR, RET_CAP_STOR)) # Resources eligible for new capacity and retirements + eExistingCapEnergy_VS[y] + EP[:vCAPENERGY_VS][y] - EP[:vRETCAPENERGY_VS][y] + elseif (y in setdiff(NEW_CAP_STOR, RET_CAP_STOR)) # Resources eligible for only new capacity + eExistingCapEnergy_VS[y] + EP[:vCAPENERGY_VS][y] + elseif (y in setdiff(RET_CAP_STOR, NEW_CAP_STOR)) # Resources eligible for only capacity retirements + eExistingCapEnergy_VS[y] - EP[:vRETCAPENERGY_VS][y] + else + eExistingCapEnergy_VS[y] + end) # 2. Objective function additions # Fixed costs for storage resources (if resource is not eligible for new energy capacity, fixed costs are only O&M costs) - @expression(EP, eCFixEnergy_VS[y in STOR], + @expression(EP, eCFixEnergy_VS[y in STOR], if y in NEW_CAP_STOR # Resources eligible for new capacity - inv_cost_per_mwhyr(gen[y])*vCAPENERGY_VS[y] + fixed_om_cost_per_mwhyr(gen[y])*eTotalCap_STOR[y] + inv_cost_per_mwhyr(gen[y]) * vCAPENERGY_VS[y] + + fixed_om_cost_per_mwhyr(gen[y]) * eTotalCap_STOR[y] else - fixed_om_cost_per_mwhyr(gen[y])*eTotalCap_STOR[y] - end - ) + fixed_om_cost_per_mwhyr(gen[y]) * eTotalCap_STOR[y] + end) @expression(EP, eTotalCFixStor, sum(eCFixEnergy_VS[y] for y in STOR)) if MultiStage == 1 - EP[:eObj] += eTotalCFixStor/inputs["OPEXMULT"] + EP[:eObj] += eTotalCFixStor / inputs["OPEXMULT"] else EP[:eObj] += eTotalCFixStor end # Variable costs of charging DC for VRE-STOR resources "y" during hour "t" - @expression(EP, eCVar_Charge_DC[y in DC_CHARGE, t=1:T], - inputs["omega"][t]*by_rid(y,:var_om_cost_per_mwh_charge_dc)*EP[:vP_DC_CHARGE][y,t]/by_rid(y,:etainverter)) + @expression(EP, eCVar_Charge_DC[y in DC_CHARGE, t = 1:T], + inputs["omega"][t] * by_rid(y, :var_om_cost_per_mwh_charge_dc) * + EP[:vP_DC_CHARGE][y, t]/by_rid(y, :etainverter)) # Variable costs of discharging DC for VRE-STOR resources "y" during hour "t" - @expression(EP, eCVar_Discharge_DC[y in DC_DISCHARGE, t=1:T], - inputs["omega"][t]*by_rid(y,:var_om_cost_per_mwh_discharge_dc)*by_rid(y,:etainverter)*EP[:vP_DC_DISCHARGE][y,t]) + @expression(EP, eCVar_Discharge_DC[y in DC_DISCHARGE, t = 1:T], + inputs["omega"][t]*by_rid(y, :var_om_cost_per_mwh_discharge_dc)* + by_rid(y, :etainverter)*EP[:vP_DC_DISCHARGE][y, t]) # Variable costs of charging AC for VRE-STOR resources "y" during hour "t" - @expression(EP, eCVar_Charge_AC[y in AC_CHARGE, t=1:T], - inputs["omega"][t]*by_rid(y,:var_om_cost_per_mwh_charge_ac)*EP[:vP_AC_CHARGE][y,t]) + @expression(EP, eCVar_Charge_AC[y in AC_CHARGE, t = 1:T], + inputs["omega"][t]*by_rid(y, :var_om_cost_per_mwh_charge_ac)* + EP[:vP_AC_CHARGE][y, t]) # Variable costs of discharging AC for VRE-STOR resources "y" during hour "t" - @expression(EP, eCVar_Discharge_AC[y in AC_DISCHARGE, t=1:T], - inputs["omega"][t]*by_rid(y,:var_om_cost_per_mwh_discharge_ac)*EP[:vP_AC_DISCHARGE][y,t]) + @expression(EP, eCVar_Discharge_AC[y in AC_DISCHARGE, t = 1:T], + inputs["omega"][t]*by_rid(y, :var_om_cost_per_mwh_discharge_ac)* + EP[:vP_AC_DISCHARGE][y, t]) # Sum individual resource contributions - @expression(EP, eTotalCVarStor, sum(eCVar_Charge_DC[y,t] for y in DC_CHARGE, t=1:T) - + sum(eCVar_Discharge_DC[y,t] for y in DC_DISCHARGE, t=1:T) - + sum(eCVar_Charge_AC[y,t] for y in AC_CHARGE, t=1:T) - + sum(eCVar_Discharge_AC[y,t] for y in AC_CHARGE, t=1:T)) + @expression(EP, + eTotalCVarStor, + sum(eCVar_Charge_DC[y, t] for y in DC_CHARGE, t in 1:T) + +sum(eCVar_Discharge_DC[y, t] for y in DC_DISCHARGE, t in 1:T) + +sum(eCVar_Charge_AC[y, t] for y in AC_CHARGE, t in 1:T) + +sum(eCVar_Discharge_AC[y, t] for y in AC_CHARGE, t in 1:T)) EP[:eObj] += eTotalCVarStor # 3. Inverter & Power Balance, SoC Expressions # Check for rep_periods > 1 & LDS=1 - if rep_periods > 1 && !isempty(VS_LDS) - CONSTRAINTSET = inputs["VS_nonLDS"] - else - CONSTRAINTSET = STOR - end + if rep_periods > 1 && !isempty(VS_LDS) + CONSTRAINTSET = inputs["VS_nonLDS"] + else + CONSTRAINTSET = STOR + end # SoC expressions @expression(EP, eSoCBalStart_VRE_STOR[y in CONSTRAINTSET, t in START_SUBPERIODS], - vS_VRE_STOR[y,t+hours_per_subperiod-1] - self_discharge(gen[y])*vS_VRE_STOR[y,t+hours_per_subperiod-1]) + vS_VRE_STOR[y, + t + hours_per_subperiod - 1]-self_discharge(gen[y]) * vS_VRE_STOR[y, t + hours_per_subperiod - 1]) @expression(EP, eSoCBalInterior_VRE_STOR[y in STOR, t in INTERIOR_SUBPERIODS], - vS_VRE_STOR[y,t-1] - self_discharge(gen[y])*vS_VRE_STOR[y,t-1]) + vS_VRE_STOR[y, t - 1]-self_discharge(gen[y]) * vS_VRE_STOR[y, t - 1]) # Expression for energy losses related to technologies (increase in effective demand) @expression(EP, eELOSS_VRE_STOR[y in STOR], JuMP.AffExpr()) @@ -1085,117 +1136,134 @@ function stor_vre_stor!(EP::Model, inputs::Dict, setup::Dict) AC_CHARGE_CONSTRAINTSET = intersect(CONSTRAINTSET, AC_CHARGE) for t in START_SUBPERIODS for y in DC_DISCHARGE_CONSTRAINTSET - eSoCBalStart_VRE_STOR[y,t] -= EP[:vP_DC_DISCHARGE][y,t]/by_rid(y,:eff_down_dc) + eSoCBalStart_VRE_STOR[y, t] -= EP[:vP_DC_DISCHARGE][y, t] / + by_rid(y, :eff_down_dc) end for y in DC_CHARGE_CONSTRAINTSET - eSoCBalStart_VRE_STOR[y,t] += by_rid(y,:eff_up_dc)*EP[:vP_DC_CHARGE][y,t] + eSoCBalStart_VRE_STOR[y, t] += by_rid(y, :eff_up_dc) * EP[:vP_DC_CHARGE][y, t] end for y in AC_DISCHARGE_CONSTRAINTSET - eSoCBalStart_VRE_STOR[y,t] -= EP[:vP_AC_DISCHARGE][y,t]/by_rid(y,:eff_down_ac) + eSoCBalStart_VRE_STOR[y, t] -= EP[:vP_AC_DISCHARGE][y, t] / + by_rid(y, :eff_down_ac) end for y in AC_CHARGE_CONSTRAINTSET - eSoCBalStart_VRE_STOR[y,t] += by_rid(y,:eff_up_ac)*EP[:vP_AC_CHARGE][y,t] + eSoCBalStart_VRE_STOR[y, t] += by_rid(y, :eff_up_ac) * EP[:vP_AC_CHARGE][y, t] end end for y in DC_DISCHARGE - EP[:eELOSS_VRE_STOR][y] -= sum(inputs["omega"][t]*vP_DC_DISCHARGE[y,t]*by_rid(y,:etainverter) for t=1:T) - for t=1:T - EP[:eInvACBalance][y,t] += by_rid(y,:etainverter)*vP_DC_DISCHARGE[y,t] - EP[:eInverterExport][y,t] += by_rid(y,:etainverter)*vP_DC_DISCHARGE[y,t] + EP[:eELOSS_VRE_STOR][y] -= sum(inputs["omega"][t] * vP_DC_DISCHARGE[y, t] * + by_rid(y, :etainverter) for t in 1:T) + for t in 1:T + EP[:eInvACBalance][y, t] += by_rid(y, :etainverter) * vP_DC_DISCHARGE[y, t] + EP[:eInverterExport][y, t] += by_rid(y, :etainverter) * vP_DC_DISCHARGE[y, t] end for t in INTERIOR_SUBPERIODS - eSoCBalInterior_VRE_STOR[y,t] -= EP[:vP_DC_DISCHARGE][y,t]/by_rid(y,:eff_down_dc) + eSoCBalInterior_VRE_STOR[y, t] -= EP[:vP_DC_DISCHARGE][y, t] / + by_rid(y, :eff_down_dc) end end for y in DC_CHARGE - EP[:eELOSS_VRE_STOR][y] += sum(inputs["omega"][t]*vP_DC_CHARGE[y,t]/by_rid(y,:etainverter) for t=1:T) - for t=1:T - EP[:eInvACBalance][y,t] -= vP_DC_CHARGE[y,t]/by_rid(y,:etainverter) - EP[:eInverterExport][y,t] += vP_DC_CHARGE[y,t]/by_rid(y,:etainverter) + EP[:eELOSS_VRE_STOR][y] += sum(inputs["omega"][t] * vP_DC_CHARGE[y, t] / + by_rid(y, :etainverter) for t in 1:T) + for t in 1:T + EP[:eInvACBalance][y, t] -= vP_DC_CHARGE[y, t] / by_rid(y, :etainverter) + EP[:eInverterExport][y, t] += vP_DC_CHARGE[y, t] / by_rid(y, :etainverter) end for t in INTERIOR_SUBPERIODS - eSoCBalInterior_VRE_STOR[y,t] += by_rid(y,:eff_up_dc)*EP[:vP_DC_CHARGE][y,t] + eSoCBalInterior_VRE_STOR[y, t] += by_rid(y, :eff_up_dc) * + EP[:vP_DC_CHARGE][y, t] end end for y in AC_DISCHARGE - EP[:eELOSS_VRE_STOR][y] -= sum(inputs["omega"][t]*vP_AC_DISCHARGE[y,t] for t=1:T) - for t=1:T - EP[:eInvACBalance][y,t] += vP_AC_DISCHARGE[y,t] + EP[:eELOSS_VRE_STOR][y] -= sum(inputs["omega"][t] * vP_AC_DISCHARGE[y, t] + for t in 1:T) + for t in 1:T + EP[:eInvACBalance][y, t] += vP_AC_DISCHARGE[y, t] end for t in INTERIOR_SUBPERIODS - eSoCBalInterior_VRE_STOR[y,t] -= EP[:vP_AC_DISCHARGE][y,t]/by_rid(y,:eff_down_ac) + eSoCBalInterior_VRE_STOR[y, t] -= EP[:vP_AC_DISCHARGE][y, t] / + by_rid(y, :eff_down_ac) end end for y in AC_CHARGE - EP[:eELOSS_VRE_STOR][y] += sum(inputs["omega"][t]*vP_AC_CHARGE[y,t] for t=1:T) - for t=1:T - EP[:eInvACBalance][y,t] -= vP_AC_CHARGE[y,t] + EP[:eELOSS_VRE_STOR][y] += sum(inputs["omega"][t] * vP_AC_CHARGE[y, t] for t in 1:T) + for t in 1:T + EP[:eInvACBalance][y, t] -= vP_AC_CHARGE[y, t] end for t in INTERIOR_SUBPERIODS - eSoCBalInterior_VRE_STOR[y,t] += by_rid(y,:eff_up_ac)*EP[:vP_AC_CHARGE][y,t] + eSoCBalInterior_VRE_STOR[y, t] += by_rid(y, :eff_up_ac) * + EP[:vP_AC_CHARGE][y, t] end end - for y in STOR, t=1:T - EP[:eInvACBalance][y,t] += vCHARGE_VRE_STOR[y,t] - EP[:eGridExport][y,t] += vCHARGE_VRE_STOR[y,t] + for y in STOR, t in 1:T + EP[:eInvACBalance][y, t] += vCHARGE_VRE_STOR[y, t] + EP[:eGridExport][y, t] += vCHARGE_VRE_STOR[y, t] end - for z in 1:Z, t=1:T - if !isempty(resources_in_zone_by_rid(gen_VRE_STOR,z)) - EP[:ePowerBalance_VRE_STOR][t, z] -= sum(vCHARGE_VRE_STOR[y,t] for y=intersect(resources_in_zone_by_rid(gen_VRE_STOR,z),STOR)) + for z in 1:Z, t in 1:T + if !isempty(resources_in_zone_by_rid(gen_VRE_STOR, z)) + EP[:ePowerBalance_VRE_STOR][t, z] -= sum(vCHARGE_VRE_STOR[y, t] + for y in intersect(resources_in_zone_by_rid(gen_VRE_STOR, + z), + STOR)) end end # 4. Energy Share Requirement & CO2 Policy Module # From CO2 Policy module - @expression(EP, eELOSSByZone_VRE_STOR[z=1:Z], - sum(EP[:eELOSS_VRE_STOR][y] for y in intersect(resources_in_zone_by_rid(gen_VRE_STOR,z),STOR))) + @expression(EP, eELOSSByZone_VRE_STOR[z = 1:Z], + sum(EP[:eELOSS_VRE_STOR][y] + for y in intersect(resources_in_zone_by_rid(gen_VRE_STOR, z), STOR))) add_similar_to_expression!(EP[:eELOSSByZone], eELOSSByZone_VRE_STOR) ### CONSTRAINTS ### # Constraint 0: Existing capacity variable is equal to existing capacity specified in the input file if MultiStage == 1 - @constraint(EP, cExistingCapEnergy_VS[y in STOR], EP[:vEXISTINGCAPENERGY_VS][y] == existing_cap_mwh(gen[y])) - end + @constraint(EP, + cExistingCapEnergy_VS[y in STOR], + EP[:vEXISTINGCAPENERGY_VS][y]==existing_cap_mwh(gen[y])) + end # Constraints 1: Retirements and capacity additions # Cannot retire more capacity than existing capacity for VRE-STOR technologies - @constraint(EP, cMaxRet_Stor[y=RET_CAP_STOR], vRETCAPENERGY_VS[y] <= eExistingCapEnergy_VS[y]) + @constraint(EP, + cMaxRet_Stor[y = RET_CAP_STOR], + vRETCAPENERGY_VS[y]<=eExistingCapEnergy_VS[y]) # Constraint on maximum capacity (if applicable) [set input to -1 if no constraint on maximum capacity] - # DEV NOTE: This constraint may be violated in some cases where Existing_Cap_MW is >= Max_Cap_MW and lead to infeasabilty - @constraint(EP, cMaxCap_Stor[y in intersect(ids_with_nonneg(gen, max_cap_mwh), STOR)], - eTotalCap_STOR[y] <= max_cap_mwh(gen[y])) + # DEV NOTE: This constraint may be violated in some cases where Existing_Cap_MW is >= Max_Cap_MW and lead to infeasabilty + @constraint(EP, cMaxCap_Stor[y in intersect(ids_with_nonneg(gen, max_cap_mwh), STOR)], + eTotalCap_STOR[y]<=max_cap_mwh(gen[y])) # Constraint on minimum capacity (if applicable) [set input to -1 if no constraint on minimum capacity] # DEV NOTE: This constraint may be violated in some cases where Existing_Cap_MW is <= Min_Cap_MW and lead to infeasabilty - @constraint(EP, cMinCap_Stor[y in intersect(ids_with_positive(gen, min_cap_mwh), STOR)], - eTotalCap_STOR[y] >= min_cap_mwh(gen[y])) + @constraint(EP, cMinCap_Stor[y in intersect(ids_with_positive(gen, min_cap_mwh), STOR)], + eTotalCap_STOR[y]>=min_cap_mwh(gen[y])) # Constraint 2: SOC Maximum - @constraint(EP, cSOCMax[y in STOR, t=1:T], vS_VRE_STOR[y,t] <= eTotalCap_STOR[y]) + @constraint(EP, cSOCMax[y in STOR, t = 1:T], vS_VRE_STOR[y, t]<=eTotalCap_STOR[y]) # Constraint 3: State of Charge (energy stored for the next hour) @constraint(EP, cSoCBalStart_VRE_STOR[y in CONSTRAINTSET, t in START_SUBPERIODS], - vS_VRE_STOR[y,t] == eSoCBalStart_VRE_STOR[y,t]) - @constraint(EP, cSoCBalInterior_VRE_STOR[y in STOR, t in INTERIOR_SUBPERIODS], - vS_VRE_STOR[y,t] == eSoCBalInterior_VRE_STOR[y,t]) + vS_VRE_STOR[y, t]==eSoCBalStart_VRE_STOR[y, t]) + @constraint(EP, cSoCBalInterior_VRE_STOR[y in STOR, t in INTERIOR_SUBPERIODS], + vS_VRE_STOR[y, t]==eSoCBalInterior_VRE_STOR[y, t]) ### SYMMETRIC RESOURCE CONSTRAINTS ### if !isempty(VS_SYM_DC) # Constraint 4: Charging + Discharging DC Maximum: see main module because capacity reserve margin/operating reserves may alter constraint - @expression(EP, eChargeDischargeMaxDC[y in VS_SYM_DC, t=1:T], - EP[:vP_DC_DISCHARGE][y,t] + EP[:vP_DC_CHARGE][y,t]) + @expression(EP, eChargeDischargeMaxDC[y in VS_SYM_DC, t = 1:T], + EP[:vP_DC_DISCHARGE][y, t]+EP[:vP_DC_CHARGE][y, t]) end if !isempty(VS_SYM_AC) # Constraint 4: Charging + Discharging AC Maximum: see main module because capacity reserve margin/operating reserves may alter constraint - @expression(EP, eChargeDischargeMaxAC[y in VS_SYM_AC, t=1:T], - EP[:vP_AC_DISCHARGE][y,t] + EP[:vP_AC_CHARGE][y,t]) + @expression(EP, eChargeDischargeMaxAC[y in VS_SYM_AC, t = 1:T], + EP[:vP_AC_DISCHARGE][y, t]+EP[:vP_AC_CHARGE][y, t]) end ### ASYMMETRIC RESOURCE MODULE ### @@ -1234,7 +1302,6 @@ The rest of the long duration energy storage constraints are copied and applied long duration energy storage resources are further elaborated upon in ```vre_stor_capres!()```. """ function lds_vre_stor!(EP::Model, inputs::Dict) - println("VRE-STOR LDS Module") ### LOAD DATA ### @@ -1244,11 +1311,11 @@ function lds_vre_stor!(EP::Model, inputs::Dict) gen_VRE_STOR = gen.VreStorage REP_PERIOD = inputs["REP_PERIOD"] # Number of representative periods - dfPeriodMap = inputs["Period_Map"] # Dataframe that maps modeled periods to representative periods - NPeriods = size(inputs["Period_Map"])[1] # Number of modeled periods + dfPeriodMap = inputs["Period_Map"] # Dataframe that maps modeled periods to representative periods + NPeriods = size(inputs["Period_Map"])[1] # Number of modeled periods hours_per_subperiod = inputs["hours_per_subperiod"] #total number of hours per subperiod - MODELED_PERIODS_INDEX = 1:NPeriods - REP_PERIODS_INDEX = MODELED_PERIODS_INDEX[dfPeriodMap[!,:Rep_Period] .== MODELED_PERIODS_INDEX] + MODELED_PERIODS_INDEX = 1:NPeriods + REP_PERIODS_INDEX = MODELED_PERIODS_INDEX[dfPeriodMap[!, :Rep_Period] .== MODELED_PERIODS_INDEX] by_rid(rid, sym) = by_rid_res(rid, sym, gen_VRE_STOR) @@ -1259,57 +1326,73 @@ function lds_vre_stor!(EP::Model, inputs::Dict) vSOCw_VRE_STOR[y in VS_LDS, n in MODELED_PERIODS_INDEX] >= 0 # Build up in storage inventory over each representative period w (can be pos or neg) - vdSOC_VRE_STOR[y in VS_LDS, w=1:REP_PERIOD] + vdSOC_VRE_STOR[y in VS_LDS, w = 1:REP_PERIOD] end) ### EXPRESSIONS ### # Note: tw_min = hours_per_subperiod*(w-1)+1; tw_max = hours_per_subperiod*w - @expression(EP, eVreStorSoCBalLongDurationStorageStart[y in VS_LDS, w=1:REP_PERIOD], - (1-self_discharge(gen[y])) * (EP[:vS_VRE_STOR][y,hours_per_subperiod*w]-EP[:vdSOC_VRE_STOR][y,w])) - + @expression(EP, eVreStorSoCBalLongDurationStorageStart[y in VS_LDS, w = 1:REP_PERIOD], + (1 - + self_discharge(gen[y]))*(EP[:vS_VRE_STOR][y, hours_per_subperiod * w] - + EP[:vdSOC_VRE_STOR][y, w])) + DC_DISCHARGE_CONSTRAINTSET = intersect(inputs["VS_STOR_DC_DISCHARGE"], VS_LDS) DC_CHARGE_CONSTRAINTSET = intersect(inputs["VS_STOR_DC_CHARGE"], VS_LDS) AC_DISCHARGE_CONSTRAINTSET = intersect(inputs["VS_STOR_AC_DISCHARGE"], VS_LDS) AC_CHARGE_CONSTRAINTSET = intersect(inputs["VS_STOR_AC_CHARGE"], VS_LDS) - for w=1:REP_PERIOD + for w in 1:REP_PERIOD for y in DC_DISCHARGE_CONSTRAINTSET - EP[:eVreStorSoCBalLongDurationStorageStart][y,w] -= EP[:vP_DC_DISCHARGE][y,hours_per_subperiod*(w-1)+1]/by_rid(y,:eff_down_dc) + EP[:eVreStorSoCBalLongDurationStorageStart][y, w] -= EP[:vP_DC_DISCHARGE][y, + hours_per_subperiod * (w - 1) + 1] / by_rid(y, :eff_down_dc) end for y in DC_CHARGE_CONSTRAINTSET - EP[:eVreStorSoCBalLongDurationStorageStart][y,w] += by_rid(y,:eff_up_dc)*EP[:vP_DC_CHARGE][y,hours_per_subperiod*(w-1)+1] + EP[:eVreStorSoCBalLongDurationStorageStart][y, w] += by_rid(y, :eff_up_dc) * + EP[:vP_DC_CHARGE][y, + hours_per_subperiod * (w - 1) + 1] end for y in AC_DISCHARGE_CONSTRAINTSET - EP[:eVreStorSoCBalLongDurationStorageStart][y,w] -= EP[:vP_AC_DISCHARGE][y,hours_per_subperiod*(w-1)+1]/by_rid(y,:eff_down_ac) + EP[:eVreStorSoCBalLongDurationStorageStart][y, w] -= EP[:vP_AC_DISCHARGE][y, + hours_per_subperiod * (w - 1) + 1] / by_rid(y, :eff_down_ac) end for y in AC_CHARGE_CONSTRAINTSET - EP[:eVreStorSoCBalLongDurationStorageStart][y,w] += by_rid(y,:eff_up_ac)*EP[:vP_AC_CHARGE][y,hours_per_subperiod*(w-1)+1] + EP[:eVreStorSoCBalLongDurationStorageStart][y, w] += by_rid(y, :eff_up_ac) * + EP[:vP_AC_CHARGE][y, + hours_per_subperiod * (w - 1) + 1] end end ### CONSTRAINTS ### # Constraint 1: Link the state of charge between the start of periods for LDS resources - @constraint(EP, cVreStorSoCBalLongDurationStorageStart[y in VS_LDS, w=1:REP_PERIOD], - EP[:vS_VRE_STOR][y,hours_per_subperiod*(w-1)+1] == EP[:eVreStorSoCBalLongDurationStorageStart][y,w]) + @constraint(EP, cVreStorSoCBalLongDurationStorageStart[y in VS_LDS, w = 1:REP_PERIOD], + EP[:vS_VRE_STOR][y, + hours_per_subperiod * (w - 1) + 1]==EP[:eVreStorSoCBalLongDurationStorageStart][y, w]) # Constraint 2: Storage at beginning of period w = storage at beginning of period w-1 + storage built up in period w (after n representative periods) # Multiply storage build up term from prior period with corresponding weight - @constraint(EP, cVreStorSoCBalLongDurationStorage[y in VS_LDS, r in MODELED_PERIODS_INDEX], - EP[:vSOCw_VRE_STOR][y,mod1(r+1, NPeriods)] == EP[:vSOCw_VRE_STOR][y,r] + EP[:vdSOC_VRE_STOR][y,dfPeriodMap[r,:Rep_Period_Index]]) + @constraint(EP, + cVreStorSoCBalLongDurationStorage[y in VS_LDS, r in MODELED_PERIODS_INDEX], + EP[:vSOCw_VRE_STOR][y, + mod1(r + 1, NPeriods)]==EP[:vSOCw_VRE_STOR][y, r] + + EP[:vdSOC_VRE_STOR][y, dfPeriodMap[r, :Rep_Period_Index]]) # Constraint 3: Storage at beginning of each modeled period cannot exceed installed energy capacity - @constraint(EP, cVreStorSoCBalLongDurationStorageUpper[y in VS_LDS, r in MODELED_PERIODS_INDEX], - EP[:vSOCw_VRE_STOR][y,r] <= EP[:eTotalCap_STOR][y]) + @constraint(EP, + cVreStorSoCBalLongDurationStorageUpper[y in VS_LDS, r in MODELED_PERIODS_INDEX], + EP[:vSOCw_VRE_STOR][y, r]<=EP[:eTotalCap_STOR][y]) # Constraint 4: Initial storage level for representative periods must also adhere to sub-period storage inventory balance # Initial storage = Final storage - change in storage inventory across representative period - @constraint(EP, cVreStorSoCBalLongDurationStorageSub[y in VS_LDS, r in REP_PERIODS_INDEX], - EP[:vSOCw_VRE_STOR][y,r] == EP[:vS_VRE_STOR][y,hours_per_subperiod*dfPeriodMap[r,:Rep_Period_Index]] - - EP[:vdSOC_VRE_STOR][y,dfPeriodMap[r,:Rep_Period_Index]]) + @constraint(EP, + cVreStorSoCBalLongDurationStorageSub[y in VS_LDS, r in REP_PERIODS_INDEX], + EP[:vSOCw_VRE_STOR][y, + r]==EP[:vS_VRE_STOR][y, hours_per_subperiod * dfPeriodMap[r, :Rep_Period_Index]] + - + EP[:vdSOC_VRE_STOR][y, dfPeriodMap[r, :Rep_Period_Index]]) end @doc raw""" @@ -1437,7 +1520,6 @@ In addition, this function adds investment and fixed O&M costs related to charge ``` """ function investment_charge_vre_stor!(EP::Model, inputs::Dict, setup::Dict) - println("VRE-STOR Charge Investment Module") ### LOAD INPUTS ### @@ -1464,8 +1546,11 @@ function investment_charge_vre_stor!(EP::Model, inputs::Dict, setup::Dict) by_rid(rid, sym) = by_rid_res(rid, sym, gen_VRE_STOR) if !isempty(VS_ASYM_DC_DISCHARGE) - MAX_DC_DISCHARGE = intersect(ids_with_nonneg(gen_VRE_STOR, max_cap_discharge_dc_mw), VS_ASYM_DC_DISCHARGE) - MIN_DC_DISCHARGE = intersect(ids_with_positive(gen_VRE_STOR, min_cap_discharge_dc_mw), VS_ASYM_DC_DISCHARGE) + MAX_DC_DISCHARGE = intersect(ids_with_nonneg(gen_VRE_STOR, max_cap_discharge_dc_mw), + VS_ASYM_DC_DISCHARGE) + MIN_DC_DISCHARGE = intersect(ids_with_positive(gen_VRE_STOR, + min_cap_discharge_dc_mw), + VS_ASYM_DC_DISCHARGE) ### VARIABLES ### @variables(EP, begin @@ -1474,47 +1559,53 @@ function investment_charge_vre_stor!(EP::Model, inputs::Dict, setup::Dict) end) if MultiStage == 1 - @variable(EP, vEXISTINGCAPDISCHARGEDC[y in VS_ASYM_DC_DISCHARGE] >= 0); + @variable(EP, vEXISTINGCAPDISCHARGEDC[y in VS_ASYM_DC_DISCHARGE]>=0) end ### EXPRESSIONS ### # 0. Multistage existing capacity definition if MultiStage == 1 - @expression(EP, eExistingCapDischargeDC[y in VS_ASYM_DC_DISCHARGE], vEXISTINGCAPDISCHARGEDC[y]) + @expression(EP, + eExistingCapDischargeDC[y in VS_ASYM_DC_DISCHARGE], + vEXISTINGCAPDISCHARGEDC[y]) else - @expression(EP, eExistingCapDischargeDC[y in VS_ASYM_DC_DISCHARGE], by_rid(y,:existing_cap_discharge_dc_mw)) + @expression(EP, + eExistingCapDischargeDC[y in VS_ASYM_DC_DISCHARGE], + by_rid(y, :existing_cap_discharge_dc_mw)) end # 1. Total storage discharge DC capacity @expression(EP, eTotalCapDischarge_DC[y in VS_ASYM_DC_DISCHARGE], if (y in intersect(NEW_CAP_DISCHARGE_DC, RET_CAP_DISCHARGE_DC)) - eExistingCapDischargeDC[y] + EP[:vCAPDISCHARGE_DC][y] - EP[:vRETCAPDISCHARGE_DC][y] + eExistingCapDischargeDC[y] + EP[:vCAPDISCHARGE_DC][y] - + EP[:vRETCAPDISCHARGE_DC][y] elseif (y in setdiff(NEW_CAP_DISCHARGE_DC, RET_CAP_DISCHARGE_DC)) eExistingCapDischargeDC[y] + EP[:vCAPDISCHARGE_DC][y] elseif (y in setdiff(RET_CAP_DISCHARGE_DC, NEW_CAP_DISCHARGE_DC)) eExistingCapDischargeDC[y] - EP[:vRETCAPDISCHARGE_DC][y] else eExistingCapDischargeDC[y] - end - ) + end) # 2. Objective Function Additions # If resource is not eligible for new discharge DC capacity, fixed costs are only O&M costs @expression(EP, eCFixDischarge_DC[y in VS_ASYM_DC_DISCHARGE], if y in NEW_CAP_DISCHARGE_DC # Resources eligible for new discharge DC capacity - by_rid(y,:inv_cost_discharge_dc_per_mwyr)*vCAPDISCHARGE_DC[y] + by_rid(y,:fixed_om_cost_discharge_dc_per_mwyr)*eTotalCapDischarge_DC[y] + by_rid(y, :inv_cost_discharge_dc_per_mwyr) * vCAPDISCHARGE_DC[y] + + by_rid(y, :fixed_om_cost_discharge_dc_per_mwyr) * eTotalCapDischarge_DC[y] else - by_rid(y,:fixed_om_cost_discharge_dc_per_mwyr)*eTotalCapDischarge_DC[y] - end - ) - + by_rid(y, :fixed_om_cost_discharge_dc_per_mwyr) * eTotalCapDischarge_DC[y] + end) + # Sum individual resource contributions to fixed costs to get total fixed costs - @expression(EP, eTotalCFixDischarge_DC, sum(EP[:eCFixDischarge_DC][y] for y in VS_ASYM_DC_DISCHARGE)) + @expression(EP, + eTotalCFixDischarge_DC, + sum(EP[:eCFixDischarge_DC][y] for y in VS_ASYM_DC_DISCHARGE)) if MultiStage == 1 - EP[:eObj] += eTotalCFixDischarge_DC/inputs["OPEXMULT"] + EP[:eObj] += eTotalCFixDischarge_DC / inputs["OPEXMULT"] else EP[:eObj] += eTotalCFixDischarge_DC end @@ -1523,29 +1614,41 @@ function investment_charge_vre_stor!(EP::Model, inputs::Dict, setup::Dict) # Constraint 0: Existing capacity variable is equal to existing capacity specified in the input file if MultiStage == 1 - @constraint(EP, cExistingCapDischargeDC[y in VS_ASYM_DC_DISCHARGE], EP[:vEXISTINGCAPDISCHARGEDC][y] == by_rid(y,:existing_cap_discharge_dc_mw)) + @constraint(EP, + cExistingCapDischargeDC[y in VS_ASYM_DC_DISCHARGE], + EP[:vEXISTINGCAPDISCHARGEDC][y]==by_rid(y, :existing_cap_discharge_dc_mw)) end # Constraints 1: Retirements and capacity additions # Cannot retire more discharge DC capacity than existing discharge capacity - @constraint(EP, cVreStorMaxRetDischargeDC[y in RET_CAP_DISCHARGE_DC], vRETCAPDISCHARGE_DC[y] <= eExistingCapDischargeDC[y]) + @constraint(EP, + cVreStorMaxRetDischargeDC[y in RET_CAP_DISCHARGE_DC], + vRETCAPDISCHARGE_DC[y]<=eExistingCapDischargeDC[y]) # Constraint on maximum discharge DC capacity (if applicable) [set input to -1 if no constraint on maximum discharge capacity] # DEV NOTE: This constraint may be violated in some cases where Existing_Charge_Cap_MW is >= Max_Charge_Cap_MWh and lead to infeasabilty - @constraint(EP, cVreStorMaxCapDischargeDC[y in MAX_DC_DISCHARGE], eTotalCapDischarge_DC[y] <= by_rid(y,:Max_Cap_Discharge_DC_MW)) + @constraint(EP, + cVreStorMaxCapDischargeDC[y in MAX_DC_DISCHARGE], + eTotalCapDischarge_DC[y]<=by_rid(y, :Max_Cap_Discharge_DC_MW)) # Constraint on minimum discharge DC capacity (if applicable) [set input to -1 if no constraint on minimum discharge capacity] # DEV NOTE: This constraint may be violated in some cases where Existing_Charge_Cap_MW is <= Min_Charge_Cap_MWh and lead to infeasabilty - @constraint(EP, cVreStorMinCapDischargeDC[y in MIN_DC_DISCHARGE], eTotalCapDischarge_DC[y] >= by_rid(y,:Min_Cap_Discharge_DC_MW)) + @constraint(EP, + cVreStorMinCapDischargeDC[y in MIN_DC_DISCHARGE], + eTotalCapDischarge_DC[y]>=by_rid(y, :Min_Cap_Discharge_DC_MW)) # Constraint 2: Maximum discharging must be less than discharge power rating - @expression(EP, eVreStorMaxDischargingDC[y in VS_ASYM_DC_DISCHARGE, t=1:T], JuMP.AffExpr()) - for y in VS_ASYM_DC_DISCHARGE, t=1:T - eVreStorMaxDischargingDC[y,t] += EP[:vP_DC_DISCHARGE][y,t] + @expression(EP, + eVreStorMaxDischargingDC[y in VS_ASYM_DC_DISCHARGE, t = 1:T], + JuMP.AffExpr()) + for y in VS_ASYM_DC_DISCHARGE, t in 1:T + eVreStorMaxDischargingDC[y, t] += EP[:vP_DC_DISCHARGE][y, t] end end - + if !isempty(VS_ASYM_DC_CHARGE) - MAX_DC_CHARGE = intersect(ids_with_nonneg(gen_VRE_STOR, max_cap_charge_dc_mw), VS_ASYM_DC_CHARGE) - MIN_DC_CHARGE = intersect(ids_with_positive(gen_VRE_STOR, min_cap_charge_dc_mw), VS_ASYM_DC_CHARGE) + MAX_DC_CHARGE = intersect(ids_with_nonneg(gen_VRE_STOR, max_cap_charge_dc_mw), + VS_ASYM_DC_CHARGE) + MIN_DC_CHARGE = intersect(ids_with_positive(gen_VRE_STOR, min_cap_charge_dc_mw), + VS_ASYM_DC_CHARGE) ### VARIABLES ### @variables(EP, begin @@ -1554,16 +1657,20 @@ function investment_charge_vre_stor!(EP::Model, inputs::Dict, setup::Dict) end) if MultiStage == 1 - @variable(EP, vEXISTINGCAPCHARGEDC[y in VS_ASYM_DC_CHARGE] >= 0); + @variable(EP, vEXISTINGCAPCHARGEDC[y in VS_ASYM_DC_CHARGE]>=0) end ### EXPRESSIONS ### # 0. Multistage existing capacity definition if MultiStage == 1 - @expression(EP, eExistingCapChargeDC[y in VS_ASYM_DC_CHARGE], vEXISTINGCAPCHARGEDC[y]) + @expression(EP, + eExistingCapChargeDC[y in VS_ASYM_DC_CHARGE], + vEXISTINGCAPCHARGEDC[y]) else - @expression(EP, eExistingCapChargeDC[y in VS_ASYM_DC_CHARGE], by_rid(y,:existing_cap_charge_dc_mw)) + @expression(EP, + eExistingCapChargeDC[y in VS_ASYM_DC_CHARGE], + by_rid(y, :existing_cap_charge_dc_mw)) end # 1. Total storage charge DC capacity @@ -1576,25 +1683,26 @@ function investment_charge_vre_stor!(EP::Model, inputs::Dict, setup::Dict) eExistingCapChargeDC[y] - EP[:vRETCAPCHARGE_DC][y] else eExistingCapChargeDC[y] - end - ) + end) # 2. Objective Function Additions # If resource is not eligible for new charge DC capacity, fixed costs are only O&M costs @expression(EP, eCFixCharge_DC[y in VS_ASYM_DC_CHARGE], if y in NEW_CAP_CHARGE_DC # Resources eligible for new charge DC capacity - by_rid(y,:inv_cost_charge_dc_per_mwyr)*vCAPCHARGE_DC[y] + by_rid(y,:fixed_om_cost_charge_dc_per_mwyr)*eTotalCapCharge_DC[y] + by_rid(y, :inv_cost_charge_dc_per_mwyr) * vCAPCHARGE_DC[y] + + by_rid(y, :fixed_om_cost_charge_dc_per_mwyr) * eTotalCapCharge_DC[y] else - by_rid(y,:fixed_om_cost_charge_dc_per_mwyr)*eTotalCapCharge_DC[y] - end - ) - + by_rid(y, :fixed_om_cost_charge_dc_per_mwyr) * eTotalCapCharge_DC[y] + end) + # Sum individual resource contributions to fixed costs to get total fixed costs - @expression(EP, eTotalCFixCharge_DC, sum(EP[:eCFixCharge_DC][y] for y in VS_ASYM_DC_CHARGE)) + @expression(EP, + eTotalCFixCharge_DC, + sum(EP[:eCFixCharge_DC][y] for y in VS_ASYM_DC_CHARGE)) if MultiStage == 1 - EP[:eObj] += eTotalCFixCharge_DC/inputs["OPEXMULT"] + EP[:eObj] += eTotalCFixCharge_DC / inputs["OPEXMULT"] else EP[:eObj] += eTotalCFixCharge_DC end @@ -1603,29 +1711,42 @@ function investment_charge_vre_stor!(EP::Model, inputs::Dict, setup::Dict) # Constraint 0: Existing capacity variable is equal to existing capacity specified in the input file if MultiStage == 1 - @constraint(EP, cExistingCapChargeDC[y in VS_ASYM_DC_CHARGE], EP[:vEXISTINGCAPCHARGEDC][y] == by_rid(y,:Existing_Cap_Charge_DC_MW)) + @constraint(EP, + cExistingCapChargeDC[y in VS_ASYM_DC_CHARGE], + EP[:vEXISTINGCAPCHARGEDC][y]==by_rid(y, :Existing_Cap_Charge_DC_MW)) end # Constraints 1: Retirements and capacity additions # Cannot retire more charge DC capacity than existing charge capacity - @constraint(EP, cVreStorMaxRetChargeDC[y in RET_CAP_CHARGE_DC], vRETCAPCHARGE_DC[y] <= eExistingCapChargeDC[y]) + @constraint(EP, + cVreStorMaxRetChargeDC[y in RET_CAP_CHARGE_DC], + vRETCAPCHARGE_DC[y]<=eExistingCapChargeDC[y]) # Constraint on maximum charge DC capacity (if applicable) [set input to -1 if no constraint on maximum charge capacity] # DEV NOTE: This constraint may be violated in some cases where Existing_Charge_Cap_MW is >= Max_Charge_Cap_MWh and lead to infeasabilty - @constraint(EP, cVreStorMaxCapChargeDC[y in MAX_DC_CHARGE], eTotalCapCharge_DC[y] <= by_rid(y,:max_cap_charge_dc_mw)) + @constraint(EP, + cVreStorMaxCapChargeDC[y in MAX_DC_CHARGE], + eTotalCapCharge_DC[y]<=by_rid(y, :max_cap_charge_dc_mw)) # Constraint on minimum charge DC capacity (if applicable) [set input to -1 if no constraint on minimum charge capacity] # DEV NOTE: This constraint may be violated in some cases where Existing_Charge_Cap_MW is <= Min_Charge_Cap_MWh and lead to infeasabilty - @constraint(EP, cVreStorMinCapChargeDC[y in MIN_DC_CHARGE], eTotalCapCharge_DC[y] >= by_rid(y,:min_cap_charge_dc_mw)) + @constraint(EP, + cVreStorMinCapChargeDC[y in MIN_DC_CHARGE], + eTotalCapCharge_DC[y]>=by_rid(y, :min_cap_charge_dc_mw)) # Constraint 2: Maximum charging must be less than charge power rating - @expression(EP, eVreStorMaxChargingDC[y in VS_ASYM_DC_CHARGE, t=1:T], JuMP.AffExpr()) - for y in VS_ASYM_DC_CHARGE, t=1:T - eVreStorMaxChargingDC[y,t] += EP[:vP_DC_CHARGE][y,t] + @expression(EP, + eVreStorMaxChargingDC[y in VS_ASYM_DC_CHARGE, t = 1:T], + JuMP.AffExpr()) + for y in VS_ASYM_DC_CHARGE, t in 1:T + eVreStorMaxChargingDC[y, t] += EP[:vP_DC_CHARGE][y, t] end end if !isempty(VS_ASYM_AC_DISCHARGE) - MAX_AC_DISCHARGE = intersect(ids_with_nonneg(gen_VRE_STOR, max_cap_discharge_ac_mw), VS_ASYM_AC_DISCHARGE) - MIN_AC_DISCHARGE = intersect(ids_with_positive(gen_VRE_STOR, min_cap_discharge_ac_mw), VS_ASYM_AC_DISCHARGE) + MAX_AC_DISCHARGE = intersect(ids_with_nonneg(gen_VRE_STOR, max_cap_discharge_ac_mw), + VS_ASYM_AC_DISCHARGE) + MIN_AC_DISCHARGE = intersect(ids_with_positive(gen_VRE_STOR, + min_cap_discharge_ac_mw), + VS_ASYM_AC_DISCHARGE) ### VARIABLES ### @variables(EP, begin @@ -1634,47 +1755,53 @@ function investment_charge_vre_stor!(EP::Model, inputs::Dict, setup::Dict) end) if MultiStage == 1 - @variable(EP, vEXISTINGCAPDISCHARGEAC[y in VS_ASYM_AC_DISCHARGE] >= 0); + @variable(EP, vEXISTINGCAPDISCHARGEAC[y in VS_ASYM_AC_DISCHARGE]>=0) end ### EXPRESSIONS ### # 0. Multistage existing capacity definition if MultiStage == 1 - @expression(EP, eExistingCapDischargeAC[y in VS_ASYM_AC_DISCHARGE], vEXISTINGCAPDISCHARGEAC[y]) + @expression(EP, + eExistingCapDischargeAC[y in VS_ASYM_AC_DISCHARGE], + vEXISTINGCAPDISCHARGEAC[y]) else - @expression(EP, eExistingCapDischargeAC[y in VS_ASYM_AC_DISCHARGE], by_rid(y,:existing_cap_discharge_ac_mw)) + @expression(EP, + eExistingCapDischargeAC[y in VS_ASYM_AC_DISCHARGE], + by_rid(y, :existing_cap_discharge_ac_mw)) end # 1. Total storage discharge AC capacity @expression(EP, eTotalCapDischarge_AC[y in VS_ASYM_AC_DISCHARGE], if (y in intersect(NEW_CAP_DISCHARGE_AC, RET_CAP_DISCHARGE_AC)) - eExistingCapDischargeAC[y] + EP[:vCAPDISCHARGE_AC][y] - EP[:vRETCAPDISCHARGE_AC][y] + eExistingCapDischargeAC[y] + EP[:vCAPDISCHARGE_AC][y] - + EP[:vRETCAPDISCHARGE_AC][y] elseif (y in setdiff(NEW_CAP_DISCHARGE_AC, RET_CAP_DISCHARGE_AC)) eExistingCapDischargeAC[y] + EP[:vCAPDISCHARGE_AC][y] elseif (y in setdiff(RET_CAP_DISCHARGE_AC, NEW_CAP_DISCHARGE_AC)) eExistingCapDischargeAC[y] - EP[:vRETCAPDISCHARGE_AC][y] else eExistingCapDischargeAC[y] - end - ) + end) # 2. Objective Function Additions # If resource is not eligible for new discharge AC capacity, fixed costs are only O&M costs @expression(EP, eCFixDischarge_AC[y in VS_ASYM_AC_DISCHARGE], if y in NEW_CAP_DISCHARGE_AC # Resources eligible for new discharge AC capacity - by_rid(y,:inv_cost_discharge_ac_per_mwyr)*vCAPDISCHARGE_AC[y] + by_rid(y,:fixed_om_cost_discharge_ac_per_mwyr)*eTotalCapDischarge_AC[y] + by_rid(y, :inv_cost_discharge_ac_per_mwyr) * vCAPDISCHARGE_AC[y] + + by_rid(y, :fixed_om_cost_discharge_ac_per_mwyr) * eTotalCapDischarge_AC[y] else - by_rid(y,:fixed_om_cost_discharge_ac_per_mwyr)*eTotalCapDischarge_AC[y] - end - ) - + by_rid(y, :fixed_om_cost_discharge_ac_per_mwyr) * eTotalCapDischarge_AC[y] + end) + # Sum individual resource contributions to fixed costs to get total fixed costs - @expression(EP, eTotalCFixDischarge_AC, sum(EP[:eCFixDischarge_AC][y] for y in VS_ASYM_AC_DISCHARGE)) + @expression(EP, + eTotalCFixDischarge_AC, + sum(EP[:eCFixDischarge_AC][y] for y in VS_ASYM_AC_DISCHARGE)) if MultiStage == 1 - EP[:eObj] += eTotalCFixDischarge_AC/inputs["OPEXMULT"] + EP[:eObj] += eTotalCFixDischarge_AC / inputs["OPEXMULT"] else EP[:eObj] += eTotalCFixDischarge_AC end @@ -1683,29 +1810,41 @@ function investment_charge_vre_stor!(EP::Model, inputs::Dict, setup::Dict) # Constraint 0: Existing capacity variable is equal to existing capacity specified in the input file if MultiStage == 1 - @constraint(EP, cExistingCapDischargeAC[y in VS_ASYM_AC_DISCHARGE], EP[:vEXISTINGCAPDISCHARGEAC][y] == by_rid(y,:existing_cap_discharge_ac_mw)) + @constraint(EP, + cExistingCapDischargeAC[y in VS_ASYM_AC_DISCHARGE], + EP[:vEXISTINGCAPDISCHARGEAC][y]==by_rid(y, :existing_cap_discharge_ac_mw)) end # Constraints 1: Retirements and capacity additions # Cannot retire more discharge AC capacity than existing charge capacity - @constraint(EP, cVreStorMaxRetDischargeAC[y in RET_CAP_DISCHARGE_AC], vRETCAPDISCHARGE_AC[y] <= eExistingCapDischargeAC[y]) + @constraint(EP, + cVreStorMaxRetDischargeAC[y in RET_CAP_DISCHARGE_AC], + vRETCAPDISCHARGE_AC[y]<=eExistingCapDischargeAC[y]) # Constraint on maximum discharge AC capacity (if applicable) [set input to -1 if no constraint on maximum charge capacity] # DEV NOTE: This constraint may be violated in some cases where Existing_Charge_Cap_MW is >= Max_Charge_Cap_MWh and lead to infeasabilty - @constraint(EP, cVreStorMaxCapDischargeAC[y in MAX_AC_DISCHARGE], eTotalCapDischarge_AC[y] <= by_rid(y,:max_cap_discharge_ac_mw)) + @constraint(EP, + cVreStorMaxCapDischargeAC[y in MAX_AC_DISCHARGE], + eTotalCapDischarge_AC[y]<=by_rid(y, :max_cap_discharge_ac_mw)) # Constraint on minimum discharge AC capacity (if applicable) [set input to -1 if no constraint on minimum charge capacity] # DEV NOTE: This constraint may be violated in some cases where Existing_Charge_Cap_MW is <= Min_Charge_Cap_MWh and lead to infeasabilty - @constraint(EP, cVreStorMinCapDischargeAC[y in MIN_AC_DISCHARGE], eTotalCapDischarge_AC[y] >= by_rid(y,:min_cap_discharge_ac_mw)) + @constraint(EP, + cVreStorMinCapDischargeAC[y in MIN_AC_DISCHARGE], + eTotalCapDischarge_AC[y]>=by_rid(y, :min_cap_discharge_ac_mw)) # Constraint 2: Maximum discharging rate must be less than discharge power rating - @expression(EP, eVreStorMaxDischargingAC[y in VS_ASYM_AC_DISCHARGE, t=1:T], JuMP.AffExpr()) - for y in VS_ASYM_AC_DISCHARGE, t=1:T - eVreStorMaxDischargingAC[y,t] += EP[:vP_AC_DISCHARGE][y,t] + @expression(EP, + eVreStorMaxDischargingAC[y in VS_ASYM_AC_DISCHARGE, t = 1:T], + JuMP.AffExpr()) + for y in VS_ASYM_AC_DISCHARGE, t in 1:T + eVreStorMaxDischargingAC[y, t] += EP[:vP_AC_DISCHARGE][y, t] end end if !isempty(VS_ASYM_AC_CHARGE) - MAX_AC_CHARGE = intersect(ids_with_nonneg(gen_VRE_STOR, max_cap_charge_ac_mw), VS_ASYM_AC_CHARGE) - MIN_AC_CHARGE = intersect(ids_with_positive(gen_VRE_STOR, min_cap_charge_ac_mw), VS_ASYM_AC_CHARGE) + MAX_AC_CHARGE = intersect(ids_with_nonneg(gen_VRE_STOR, max_cap_charge_ac_mw), + VS_ASYM_AC_CHARGE) + MIN_AC_CHARGE = intersect(ids_with_positive(gen_VRE_STOR, min_cap_charge_ac_mw), + VS_ASYM_AC_CHARGE) ### VARIABLES ### @variables(EP, begin @@ -1714,16 +1853,20 @@ function investment_charge_vre_stor!(EP::Model, inputs::Dict, setup::Dict) end) if MultiStage == 1 - @variable(EP, vEXISTINGCAPCHARGEAC[y in VS_ASYM_AC_CHARGE] >= 0); + @variable(EP, vEXISTINGCAPCHARGEAC[y in VS_ASYM_AC_CHARGE]>=0) end ### EXPRESSIONS ### # 0. Multistage existing capacity definition if MultiStage == 1 - @expression(EP, eExistingCapChargeAC[y in VS_ASYM_AC_CHARGE], vEXISTINGCAPCHARGEAC[y]) + @expression(EP, + eExistingCapChargeAC[y in VS_ASYM_AC_CHARGE], + vEXISTINGCAPCHARGEAC[y]) else - @expression(EP, eExistingCapChargeAC[y in VS_ASYM_AC_CHARGE], by_rid(y,:existing_cap_charge_ac_mw)) + @expression(EP, + eExistingCapChargeAC[y in VS_ASYM_AC_CHARGE], + by_rid(y, :existing_cap_charge_ac_mw)) end # 1. Total storage charge AC capacity @@ -1736,25 +1879,26 @@ function investment_charge_vre_stor!(EP::Model, inputs::Dict, setup::Dict) eExistingCapChargeAC[y] - EP[:vRETCAPCHARGE_AC][y] else eExistingCapChargeAC[y] - end - ) + end) # 2. Objective Function Additions # If resource is not eligible for new charge AC capacity, fixed costs are only O&M costs @expression(EP, eCFixCharge_AC[y in VS_ASYM_AC_CHARGE], if y in NEW_CAP_CHARGE_AC # Resources eligible for new charge AC capacity - by_rid(y,:inv_cost_charge_ac_per_mwyr)*vCAPCHARGE_AC[y] + by_rid(y,:fixed_om_cost_charge_ac_per_mwyr)*eTotalCapCharge_AC[y] + by_rid(y, :inv_cost_charge_ac_per_mwyr) * vCAPCHARGE_AC[y] + + by_rid(y, :fixed_om_cost_charge_ac_per_mwyr) * eTotalCapCharge_AC[y] else - by_rid(y,:fixed_om_cost_charge_ac_per_mwyr)*eTotalCapCharge_AC[y] - end - ) - + by_rid(y, :fixed_om_cost_charge_ac_per_mwyr) * eTotalCapCharge_AC[y] + end) + # Sum individual resource contributions to fixed costs to get total fixed costs - @expression(EP, eTotalCFixCharge_AC, sum(EP[:eCFixCharge_AC][y] for y in VS_ASYM_AC_CHARGE)) + @expression(EP, + eTotalCFixCharge_AC, + sum(EP[:eCFixCharge_AC][y] for y in VS_ASYM_AC_CHARGE)) if MultiStage == 1 - EP[:eObj] += eTotalCFixCharge_AC/inputs["OPEXMULT"] + EP[:eObj] += eTotalCFixCharge_AC / inputs["OPEXMULT"] else EP[:eObj] += eTotalCFixCharge_AC end @@ -1763,23 +1907,33 @@ function investment_charge_vre_stor!(EP::Model, inputs::Dict, setup::Dict) # Constraint 0: Existing capacity variable is equal to existing capacity specified in the input file if MultiStage == 1 - @constraint(EP, cExistingCapChargeAC[y in VS_ASYM_AC_CHARGE], EP[:vEXISTINGCAPCHARGEAC][y] == by_rid(y,:existing_cap_charge_ac_mw)) + @constraint(EP, + cExistingCapChargeAC[y in VS_ASYM_AC_CHARGE], + EP[:vEXISTINGCAPCHARGEAC][y]==by_rid(y, :existing_cap_charge_ac_mw)) end # Constraints 1: Retirements and capacity additions # Cannot retire more charge AC capacity than existing charge capacity - @constraint(EP, cVreStorMaxRetChargeAC[y in RET_CAP_CHARGE_AC], vRETCAPCHARGE_AC[y] <= eExistingCapChargeAC[y]) + @constraint(EP, + cVreStorMaxRetChargeAC[y in RET_CAP_CHARGE_AC], + vRETCAPCHARGE_AC[y]<=eExistingCapChargeAC[y]) # Constraint on maximum charge AC capacity (if applicable) [set input to -1 if no constraint on maximum charge capacity] # DEV NOTE: This constraint may be violated in some cases where Existing_Charge_Cap_MW is >= Max_Charge_Cap_MWh and lead to infeasabilty - @constraint(EP, cVreStorMaxCapChargeAC[y in MAX_AC_CHARGE], eTotalCapCharge_AC[y] <= by_rid(y,:max_cap_charge_ac_mw)) + @constraint(EP, + cVreStorMaxCapChargeAC[y in MAX_AC_CHARGE], + eTotalCapCharge_AC[y]<=by_rid(y, :max_cap_charge_ac_mw)) # Constraint on minimum charge AC capacity (if applicable) [set input to -1 if no constraint on minimum charge capacity] # DEV NOTE: This constraint may be violated in some cases where Existing_Charge_Cap_MW is <= Min_Charge_Cap_MWh and lead to infeasabilty - @constraint(EP, cVreStorMinCapChargeAC[y in MIN_AC_CHARGE], eTotalCapCharge_AC[y] >= by_rid(y,:min_cap_charge_ac_mw)) + @constraint(EP, + cVreStorMinCapChargeAC[y in MIN_AC_CHARGE], + eTotalCapCharge_AC[y]>=by_rid(y, :min_cap_charge_ac_mw)) # Constraint 2: Maximum charging rate must be less than charge power rating - @expression(EP, eVreStorMaxChargingAC[y in VS_ASYM_AC_CHARGE, t=1:T], JuMP.AffExpr()) - for y in VS_ASYM_AC_CHARGE, t=1:T - eVreStorMaxChargingAC[y,t] += EP[:vP_AC_CHARGE][y,t] + @expression(EP, + eVreStorMaxChargingAC[y in VS_ASYM_AC_CHARGE, t = 1:T], + JuMP.AffExpr()) + for y in VS_ASYM_AC_CHARGE, t in 1:T + eVreStorMaxChargingAC[y, t] += EP[:vP_AC_CHARGE][y, t] end end end @@ -1858,7 +2012,6 @@ All other constraints are identical to those used to track the actual state of c state of charge, build up storage inventory and state of charge at the beginning of each period. """ function vre_stor_capres!(EP::Model, inputs::Dict, setup::Dict) - println("VRE-STOR Capacity Reserve Margin Module") ### LOAD DATA ### @@ -1880,32 +2033,32 @@ function vre_stor_capres!(EP::Model, inputs::Dict, setup::Dict) VS_LDS = inputs["VS_LDS"] START_SUBPERIODS = inputs["START_SUBPERIODS"] - INTERIOR_SUBPERIODS = inputs["INTERIOR_SUBPERIODS"] - hours_per_subperiod = inputs["hours_per_subperiod"] # total number of hours per subperiod + INTERIOR_SUBPERIODS = inputs["INTERIOR_SUBPERIODS"] + hours_per_subperiod = inputs["hours_per_subperiod"] # total number of hours per subperiod rep_periods = inputs["REP_PERIOD"] virtual_discharge_cost = inputs["VirtualChargeDischargeCost"] StorageVirtualDischarge = setup["StorageVirtualDischarge"] - + by_rid(rid, sym) = by_rid_res(rid, sym, gen_VRE_STOR) - + ### VARIABLES ### @variables(EP, begin # Virtual DC discharge contributing to capacity reserves at timestep t for VRE-storage cluster y - vCAPRES_DC_DISCHARGE[y in DC_DISCHARGE, t=1:T] >= 0 + vCAPRES_DC_DISCHARGE[y in DC_DISCHARGE, t = 1:T] >= 0 # Virtual AC discharge contributing to capacity reserves at timestep t for VRE-storage cluster y - vCAPRES_AC_DISCHARGE[y in AC_DISCHARGE, t=1:T] >= 0 + vCAPRES_AC_DISCHARGE[y in AC_DISCHARGE, t = 1:T] >= 0 # Virtual DC charge contributing to capacity reserves at timestep t for VRE-storage cluster y - vCAPRES_DC_CHARGE[y in DC_CHARGE, t=1:T] >= 0 + vCAPRES_DC_CHARGE[y in DC_CHARGE, t = 1:T] >= 0 # Virtual AC charge contributing to capacity reserves at timestep t for VRE-storage cluster y - vCAPRES_AC_CHARGE[y in AC_CHARGE, t=1:T] >= 0 + vCAPRES_AC_CHARGE[y in AC_CHARGE, t = 1:T] >= 0 # Total state of charge being held in reserve at timestep t for VRE-storage cluster y - vCAPRES_VS_VRE_STOR[y in STOR, t=1:T] >= 0 + vCAPRES_VS_VRE_STOR[y in STOR, t = 1:T] >= 0 end) ### EXPRESSIONS ### @@ -1921,11 +2074,13 @@ function vre_stor_capres!(EP::Model, inputs::Dict, setup::Dict) # Virtual State of Charge Expressions @expression(EP, eVreStorVSoCBalStart[y in CONSTRAINTSET, t in START_SUBPERIODS], - EP[:vCAPRES_VS_VRE_STOR][y,t+hours_per_subperiod-1] - - self_discharge(gen[y])*EP[:vCAPRES_VS_VRE_STOR][y,t+hours_per_subperiod-1]) + EP[:vCAPRES_VS_VRE_STOR][y, + t + hours_per_subperiod - 1] + -self_discharge(gen[y]) * EP[:vCAPRES_VS_VRE_STOR][y, t + hours_per_subperiod - 1]) @expression(EP, eVreStorVSoCBalInterior[y in STOR, t in INTERIOR_SUBPERIODS], - EP[:vCAPRES_VS_VRE_STOR][y,t-1] - - self_discharge(gen[y])*EP[:vCAPRES_VS_VRE_STOR][y,t-1]) + EP[:vCAPRES_VS_VRE_STOR][y, + t - 1] + -self_discharge(gen[y]) * EP[:vCAPRES_VS_VRE_STOR][y, t - 1]) DC_DISCHARGE_CONSTRAINTSET = intersect(CONSTRAINTSET, DC_DISCHARGE) DC_CHARGE_CONSTRAINTSET = intersect(CONSTRAINTSET, DC_CHARGE) @@ -1933,132 +2088,184 @@ function vre_stor_capres!(EP::Model, inputs::Dict, setup::Dict) AC_CHARGE_CONSTRAINTSET = intersect(CONSTRAINTSET, AC_CHARGE) for t in START_SUBPERIODS for y in DC_DISCHARGE_CONSTRAINTSET - eVreStorVSoCBalStart[y,t] += EP[:vCAPRES_DC_DISCHARGE][y,t]/by_rid(y,:eff_down_dc) + eVreStorVSoCBalStart[y, t] += EP[:vCAPRES_DC_DISCHARGE][y, t] / + by_rid(y, :eff_down_dc) end for y in DC_CHARGE_CONSTRAINTSET - eVreStorVSoCBalStart[y,t] -= by_rid(y,:eff_up_dc)*EP[:vCAPRES_DC_CHARGE][y,t] + eVreStorVSoCBalStart[y, t] -= by_rid(y, :eff_up_dc) * + EP[:vCAPRES_DC_CHARGE][y, t] end for y in AC_DISCHARGE_CONSTRAINTSET - eVreStorVSoCBalStart[y,t] += EP[:vCAPRES_AC_DISCHARGE][y,t]/by_rid(y,:eff_down_ac) + eVreStorVSoCBalStart[y, t] += EP[:vCAPRES_AC_DISCHARGE][y, t] / + by_rid(y, :eff_down_ac) end for y in AC_CHARGE_CONSTRAINTSET - eVreStorVSoCBalStart[y,t] -= by_rid(y,:eff_up_ac)*EP[:vCAPRES_AC_CHARGE][y,t] + eVreStorVSoCBalStart[y, t] -= by_rid(y, :eff_up_ac) * + EP[:vCAPRES_AC_CHARGE][y, t] end end for t in INTERIOR_SUBPERIODS for y in DC_DISCHARGE - eVreStorVSoCBalInterior[y,t] += EP[:vCAPRES_DC_DISCHARGE][y,t]/by_rid(y,:eff_down_dc) + eVreStorVSoCBalInterior[y, t] += EP[:vCAPRES_DC_DISCHARGE][y, t] / + by_rid(y, :eff_down_dc) end for y in DC_CHARGE - eVreStorVSoCBalInterior[y,t] -= by_rid(y,:eff_up_dc)*EP[:vCAPRES_DC_CHARGE][y,t] + eVreStorVSoCBalInterior[y, t] -= by_rid(y, :eff_up_dc) * + EP[:vCAPRES_DC_CHARGE][y, t] end for y in AC_DISCHARGE - eVreStorVSoCBalInterior[y,t] += EP[:vCAPRES_AC_DISCHARGE][y,t]/by_rid(y,:eff_down_ac) + eVreStorVSoCBalInterior[y, t] += EP[:vCAPRES_AC_DISCHARGE][y, t] / + by_rid(y, :eff_down_ac) end for y in AC_CHARGE - eVreStorVSoCBalInterior[y,t] -= by_rid(y,:eff_up_ac)*EP[:vCAPRES_AC_CHARGE][y,t] + eVreStorVSoCBalInterior[y, t] -= by_rid(y, :eff_up_ac) * + EP[:vCAPRES_AC_CHARGE][y, t] end end # Inverter & grid connection export additions - for t=1:T + for t in 1:T for y in DC_DISCHARGE - EP[:eInverterExport][y,t] += by_rid(y,:etainverter)*vCAPRES_DC_DISCHARGE[y,t] - EP[:eGridExport][y,t] += by_rid(y,:etainverter)*vCAPRES_DC_DISCHARGE[y,t] + EP[:eInverterExport][y, t] += by_rid(y, :etainverter) * + vCAPRES_DC_DISCHARGE[y, t] + EP[:eGridExport][y, t] += by_rid(y, :etainverter) * vCAPRES_DC_DISCHARGE[y, t] end for y in DC_CHARGE - EP[:eInverterExport][y,t] += vCAPRES_DC_CHARGE[y,t]/by_rid(y,:etainverter) - EP[:eGridExport][y,t] += vCAPRES_DC_CHARGE[y,t]/by_rid(y,:etainverter) + EP[:eInverterExport][y, t] += vCAPRES_DC_CHARGE[y, t] / by_rid(y, :etainverter) + EP[:eGridExport][y, t] += vCAPRES_DC_CHARGE[y, t] / by_rid(y, :etainverter) end for y in AC_DISCHARGE - EP[:eGridExport][y,t] += vCAPRES_AC_DISCHARGE[y,t] + EP[:eGridExport][y, t] += vCAPRES_AC_DISCHARGE[y, t] end for y in AC_CHARGE - EP[:eGridExport][y,t] += vCAPRES_AC_CHARGE[y,t] + EP[:eGridExport][y, t] += vCAPRES_AC_CHARGE[y, t] end - - # Asymmetric and symmetric storage contributions + + # Asymmetric and symmetric storage contributions for y in VS_ASYM_DC_DISCHARGE - EP[:eVreStorMaxDischargingDC][y,t] += vCAPRES_DC_DISCHARGE[y,t] + EP[:eVreStorMaxDischargingDC][y, t] += vCAPRES_DC_DISCHARGE[y, t] end for y in VS_ASYM_AC_DISCHARGE - EP[:eVreStorMaxDischargingAC][y,t] += vCAPRES_AC_DISCHARGE[y,t] + EP[:eVreStorMaxDischargingAC][y, t] += vCAPRES_AC_DISCHARGE[y, t] end for y in VS_ASYM_DC_CHARGE - EP[:eVreStorMaxChargingDC][y,t] += vCAPRES_DC_CHARGE[y,t] + EP[:eVreStorMaxChargingDC][y, t] += vCAPRES_DC_CHARGE[y, t] end for y in VS_ASYM_AC_CHARGE - EP[:eVreStorMaxChargingAC][y,t] += vCAPRES_AC_CHARGE[y,t] + EP[:eVreStorMaxChargingAC][y, t] += vCAPRES_AC_CHARGE[y, t] end for y in VS_SYM_DC - EP[:eChargeDischargeMaxDC][y,t] += (vCAPRES_DC_DISCHARGE[y,t] - + vCAPRES_DC_CHARGE[y,t]) + EP[:eChargeDischargeMaxDC][y, t] += (vCAPRES_DC_DISCHARGE[y, t] + + + vCAPRES_DC_CHARGE[y, t]) end for y in VS_SYM_AC - EP[:eChargeDischargeMaxAC][y,t] += (vCAPRES_AC_DISCHARGE[y,t] - + vCAPRES_AC_CHARGE[y,t]) + EP[:eChargeDischargeMaxAC][y, t] += (vCAPRES_AC_DISCHARGE[y, t] + + + vCAPRES_AC_CHARGE[y, t]) end end ### CONSTRAINTS ### # Constraint 1: Links energy held in reserve in first time step with decisions in last time step of each subperiod # We use a modified formulation of this constraint (cVSoCBalLongDurationStorageStart) when modeling multiple representative periods and long duration storage - @constraint(EP, cVreStorVSoCBalStart[y in CONSTRAINTSET, t in START_SUBPERIODS], - vCAPRES_VS_VRE_STOR[y,t] == eVreStorVSoCBalStart[y,t]) + @constraint(EP, cVreStorVSoCBalStart[y in CONSTRAINTSET, t in START_SUBPERIODS], + vCAPRES_VS_VRE_STOR[y, t]==eVreStorVSoCBalStart[y, t]) # Energy held in reserve for the next hour - @constraint(EP, cVreStorVSoCBalInterior[y in STOR, t in INTERIOR_SUBPERIODS], - vCAPRES_VS_VRE_STOR[y,t] == eVreStorVSoCBalInterior[y,t]) + @constraint(EP, cVreStorVSoCBalInterior[y in STOR, t in INTERIOR_SUBPERIODS], + vCAPRES_VS_VRE_STOR[y, t]==eVreStorVSoCBalInterior[y, t]) # Constraint 2: Energy held in reserve acts as a lower bound on the total energy held in storage - @constraint(EP, cVreStorSOCMinCapRes[y in STOR, t=1:T], EP[:vS_VRE_STOR][y,t] >= vCAPRES_VS_VRE_STOR[y,t]) + @constraint(EP, + cVreStorSOCMinCapRes[y in STOR, t = 1:T], + EP[:vS_VRE_STOR][y, t]>=vCAPRES_VS_VRE_STOR[y, t]) # Constraint 3: Add capacity reserve margin contributions from VRE-STOR resources to capacity reserve margin constraint - @expression(EP, eCapResMarBalanceStor_VRE_STOR[res=1:inputs["NCapacityReserveMargin"], t=1:T],( - sum(derating_factor(gen[y],tag=res)*by_rid(y,:etainverter)*inputs["pP_Max_Solar"][y,t]*EP[:eTotalCap_SOLAR][y] for y in inputs["VS_SOLAR"]) - + sum(derating_factor(gen[y],tag=res)*inputs["pP_Max_Wind"][y,t]*EP[:eTotalCap_WIND][y] for y in inputs["VS_WIND"]) - + sum(derating_factor(gen[y],tag=res)*by_rid(y,:etainverter)*(EP[:vP_DC_DISCHARGE][y,t]) for y in DC_DISCHARGE) - + sum(derating_factor(gen[y],tag=res)*(EP[:vP_AC_DISCHARGE][y,t]) for y in AC_DISCHARGE) - - sum(derating_factor(gen[y],tag=res)*(EP[:vP_DC_CHARGE][y,t])/by_rid(y,:etainverter) for y in DC_CHARGE) - - sum(derating_factor(gen[y],tag=res)*(EP[:vP_AC_CHARGE][y,t]) for y in AC_CHARGE))) + @expression(EP, + eCapResMarBalanceStor_VRE_STOR[res = 1:inputs["NCapacityReserveMargin"], t = 1:T], + (sum(derating_factor(gen[y], tag = res) * by_rid(y, :etainverter) * + inputs["pP_Max_Solar"][y, t] * EP[:eTotalCap_SOLAR][y] + for y in inputs["VS_SOLAR"]) + + + sum(derating_factor(gen[y], tag = res) * inputs["pP_Max_Wind"][y, t] * + EP[:eTotalCap_WIND][y] for y in inputs["VS_WIND"]) + + + sum(derating_factor(gen[y], tag = res) * by_rid(y, :etainverter) * + (EP[:vP_DC_DISCHARGE][y, t]) for y in DC_DISCHARGE) + + + sum(derating_factor(gen[y], tag = res) * (EP[:vP_AC_DISCHARGE][y, t]) + for y in AC_DISCHARGE) + - + sum(derating_factor(gen[y], tag = res) * (EP[:vP_DC_CHARGE][y, t]) / + by_rid(y, :etainverter) for y in DC_CHARGE) + -sum(derating_factor(gen[y], tag = res) * (EP[:vP_AC_CHARGE][y, t]) + for y in AC_CHARGE))) if StorageVirtualDischarge > 0 - @expression(EP, eCapResMarBalanceStor_VRE_STOR_Virtual[res=1:inputs["NCapacityReserveMargin"], t=1:T],( - sum(derating_factor(gen[y],tag=res)*by_rid(y,:etainverter)*(vCAPRES_DC_DISCHARGE[y,t]) for y in DC_DISCHARGE) - + sum(derating_factor(gen[y],tag=res)*(vCAPRES_AC_DISCHARGE[y,t]) for y in AC_DISCHARGE) - - sum(derating_factor(gen[y],tag=res)*(vCAPRES_DC_CHARGE[y,t])/by_rid(y,:etainverter) for y in DC_CHARGE) - - sum(derating_factor(gen[y],tag=res)*(vCAPRES_AC_CHARGE[y,t]) for y in AC_CHARGE))) - add_similar_to_expression!(eCapResMarBalanceStor_VRE_STOR,eCapResMarBalanceStor_VRE_STOR_Virtual) + @expression(EP, + eCapResMarBalanceStor_VRE_STOR_Virtual[res = 1:inputs["NCapacityReserveMargin"], + t = 1:T], + (sum(derating_factor(gen[y], tag = res) * by_rid(y, :etainverter) * + (vCAPRES_DC_DISCHARGE[y, t]) for y in DC_DISCHARGE) + + + sum(derating_factor(gen[y], tag = res) * (vCAPRES_AC_DISCHARGE[y, t]) + for y in AC_DISCHARGE) + - + sum(derating_factor(gen[y], tag = res) * (vCAPRES_DC_CHARGE[y, t]) / + by_rid(y, :etainverter) for y in DC_CHARGE) + -sum(derating_factor(gen[y], tag = res) * (vCAPRES_AC_CHARGE[y, t]) + for y in AC_CHARGE))) + add_similar_to_expression!(eCapResMarBalanceStor_VRE_STOR, + eCapResMarBalanceStor_VRE_STOR_Virtual) end EP[:eCapResMarBalance] += EP[:eCapResMarBalanceStor_VRE_STOR] ### OBJECTIVE FUNCTION ADDITIONS ### #Variable costs of DC "virtual charging" for technologies "y" during hour "t" in zone "z" - @expression(EP, eCVar_Charge_DC_virtual[y in DC_CHARGE,t=1:T], - inputs["omega"][t]*virtual_discharge_cost*vCAPRES_DC_CHARGE[y,t]/by_rid(y,:etainverter)) - @expression(EP, eTotalCVar_Charge_DC_T_virtual[t=1:T], sum(eCVar_Charge_DC_virtual[y,t] for y in DC_CHARGE)) - @expression(EP, eTotalCVar_Charge_DC_virtual, sum(eTotalCVar_Charge_DC_T_virtual[t] for t in 1:T)) + @expression(EP, eCVar_Charge_DC_virtual[y in DC_CHARGE, t = 1:T], + inputs["omega"][t] * virtual_discharge_cost * + vCAPRES_DC_CHARGE[y, t]/by_rid(y, :etainverter)) + @expression(EP, + eTotalCVar_Charge_DC_T_virtual[t = 1:T], + sum(eCVar_Charge_DC_virtual[y, t] for y in DC_CHARGE)) + @expression(EP, + eTotalCVar_Charge_DC_virtual, + sum(eTotalCVar_Charge_DC_T_virtual[t] for t in 1:T)) EP[:eObj] += eTotalCVar_Charge_DC_virtual #Variable costs of DC "virtual discharging" for technologies "y" during hour "t" in zone "z" - @expression(EP, eCVar_Discharge_DC_virtual[y in DC_DISCHARGE,t=1:T], - inputs["omega"][t]*virtual_discharge_cost*by_rid(y,:etainverter)*vCAPRES_DC_DISCHARGE[y,t]) - @expression(EP, eTotalCVar_Discharge_DC_T_virtual[t=1:T], sum(eCVar_Discharge_DC_virtual[y,t] for y in DC_DISCHARGE)) - @expression(EP, eTotalCVar_Discharge_DC_virtual, sum(eTotalCVar_Discharge_DC_T_virtual[t] for t in 1:T)) + @expression(EP, eCVar_Discharge_DC_virtual[y in DC_DISCHARGE, t = 1:T], + inputs["omega"][t]*virtual_discharge_cost*by_rid(y, :etainverter)* + vCAPRES_DC_DISCHARGE[y, t]) + @expression(EP, + eTotalCVar_Discharge_DC_T_virtual[t = 1:T], + sum(eCVar_Discharge_DC_virtual[y, t] for y in DC_DISCHARGE)) + @expression(EP, + eTotalCVar_Discharge_DC_virtual, + sum(eTotalCVar_Discharge_DC_T_virtual[t] for t in 1:T)) EP[:eObj] += eTotalCVar_Discharge_DC_virtual #Variable costs of AC "virtual charging" for technologies "y" during hour "t" in zone "z" - @expression(EP, eCVar_Charge_AC_virtual[y in AC_CHARGE,t=1:T], - inputs["omega"][t]*virtual_discharge_cost*vCAPRES_AC_CHARGE[y,t]) - @expression(EP, eTotalCVar_Charge_AC_T_virtual[t=1:T], sum(eCVar_Charge_AC_virtual[y,t] for y in AC_CHARGE)) - @expression(EP, eTotalCVar_Charge_AC_virtual, sum(eTotalCVar_Charge_AC_T_virtual[t] for t in 1:T)) + @expression(EP, eCVar_Charge_AC_virtual[y in AC_CHARGE, t = 1:T], + inputs["omega"][t]*virtual_discharge_cost*vCAPRES_AC_CHARGE[y, t]) + @expression(EP, + eTotalCVar_Charge_AC_T_virtual[t = 1:T], + sum(eCVar_Charge_AC_virtual[y, t] for y in AC_CHARGE)) + @expression(EP, + eTotalCVar_Charge_AC_virtual, + sum(eTotalCVar_Charge_AC_T_virtual[t] for t in 1:T)) EP[:eObj] += eTotalCVar_Charge_AC_virtual #Variable costs of AC "virtual discharging" for technologies "y" during hour "t" in zone "z" - @expression(EP, eCVar_Discharge_AC_virtual[y in AC_DISCHARGE,t=1:T], - inputs["omega"][t]*virtual_discharge_cost*vCAPRES_AC_DISCHARGE[y,t]) - @expression(EP, eTotalCVar_Discharge_AC_T_virtual[t=1:T], sum(eCVar_Discharge_AC_virtual[y,t] for y in AC_DISCHARGE)) - @expression(EP, eTotalCVar_Discharge_AC_virtual, sum(eTotalCVar_Discharge_AC_T_virtual[t] for t in 1:T)) + @expression(EP, eCVar_Discharge_AC_virtual[y in AC_DISCHARGE, t = 1:T], + inputs["omega"][t]*virtual_discharge_cost*vCAPRES_AC_DISCHARGE[y, t]) + @expression(EP, + eTotalCVar_Discharge_AC_T_virtual[t = 1:T], + sum(eCVar_Discharge_AC_virtual[y, t] for y in AC_DISCHARGE)) + @expression(EP, + eTotalCVar_Discharge_AC_virtual, + sum(eTotalCVar_Discharge_AC_T_virtual[t] for t in 1:T)) EP[:eObj] += eTotalCVar_Discharge_AC_virtual ### LONG DURATION ENERGY STORAGE CAPACITY RESERVE MARGIN MODULE ### @@ -2070,63 +2277,83 @@ function vre_stor_capres!(EP::Model, inputs::Dict, setup::Dict) dfPeriodMap = inputs["Period_Map"] # Dataframe that maps modeled periods to representative periods NPeriods = size(inputs["Period_Map"])[1] # Number of modeled periods MODELED_PERIODS_INDEX = 1:NPeriods - REP_PERIODS_INDEX = MODELED_PERIODS_INDEX[dfPeriodMap[!,:Rep_Period] .== MODELED_PERIODS_INDEX] + REP_PERIODS_INDEX = MODELED_PERIODS_INDEX[dfPeriodMap[!, :Rep_Period] .== MODELED_PERIODS_INDEX] ### VARIABLES ### - @variables(EP, begin - # State of charge held in reserve for storage at beginning of each modeled period n - vCAPCONTRSTOR_VSOCw_VRE_STOR[y in VS_LDS, n in MODELED_PERIODS_INDEX] >= 0 + @variables(EP, + begin + # State of charge held in reserve for storage at beginning of each modeled period n + vCAPCONTRSTOR_VSOCw_VRE_STOR[y in VS_LDS, n in MODELED_PERIODS_INDEX] >= 0 - # Build up in storage inventory held in reserve over each representative period w (can be pos or neg) - vCAPCONTRSTOR_VdSOC_VRE_STOR[y in VS_LDS, w=1:REP_PERIOD] - end) + # Build up in storage inventory held in reserve over each representative period w (can be pos or neg) + vCAPCONTRSTOR_VdSOC_VRE_STOR[y in VS_LDS, w = 1:REP_PERIOD] + end) ### EXPRESSIONS ### - @expression(EP, eVreStorVSoCBalLongDurationStorageStart[y in VS_LDS, w=1:REP_PERIOD], - (1-self_discharge(gen[y]))*(EP[:vCAPRES_VS_VRE_STOR][y,hours_per_subperiod*w]-vCAPCONTRSTOR_VdSOC_VRE_STOR[y,w])) - + @expression(EP, + eVreStorVSoCBalLongDurationStorageStart[y in VS_LDS, w = 1:REP_PERIOD], + (1 - + self_discharge(gen[y]))*(EP[:vCAPRES_VS_VRE_STOR][y, hours_per_subperiod * w] - + vCAPCONTRSTOR_VdSOC_VRE_STOR[y, w])) + DC_DISCHARGE_CONSTRAINTSET = intersect(DC_DISCHARGE, VS_LDS) DC_CHARGE_CONSTRAINTSET = intersect(DC_CHARGE, VS_LDS) AC_DISCHARGE_CONSTRAINTSET = intersect(AC_DISCHARGE, VS_LDS) AC_CHARGE_CONSTRAINTSET = intersect(AC_CHARGE, VS_LDS) - for w=1:REP_PERIOD + for w in 1:REP_PERIOD for y in DC_DISCHARGE_CONSTRAINTSET - eVreStorVSoCBalLongDurationStorageStart[y,w] += EP[:vCAPRES_DC_DISCHARGE][y,hours_per_subperiod*(w-1)+1]/by_rid(y,:eff_down_dc) + eVreStorVSoCBalLongDurationStorageStart[y, w] += EP[:vCAPRES_DC_DISCHARGE][y, + hours_per_subperiod * (w - 1) + 1] / by_rid(y, :eff_down_dc) end for y in DC_CHARGE_CONSTRAINTSET - eVreStorVSoCBalLongDurationStorageStart[y,w] -= by_rid(y,:eff_up_dc)*EP[:vCAPRES_DC_CHARGE][y,hours_per_subperiod*(w-1)+1] + eVreStorVSoCBalLongDurationStorageStart[y, w] -= by_rid(y, :eff_up_dc) * + EP[:vCAPRES_DC_CHARGE][y, + hours_per_subperiod * (w - 1) + 1] end for y in AC_DISCHARGE_CONSTRAINTSET - eVreStorVSoCBalLongDurationStorageStart[y,w] += EP[:vCAPRES_AC_DISCHARGE][y,hours_per_subperiod*(w-1)+1]/by_rid(y,:eff_down_ac) + eVreStorVSoCBalLongDurationStorageStart[y, w] += EP[:vCAPRES_AC_DISCHARGE][y, + hours_per_subperiod * (w - 1) + 1] / by_rid(y, :eff_down_ac) end for y in AC_CHARGE_CONSTRAINTSET - eVreStorVSoCBalLongDurationStorageStart[y,w] -= by_rid(y,:eff_up_ac)*EP[:vCAPRES_AC_CHARGE][y,hours_per_subperiod*(w-1)+1] + eVreStorVSoCBalLongDurationStorageStart[y, w] -= by_rid(y, :eff_up_ac) * + EP[:vCAPRES_AC_CHARGE][y, + hours_per_subperiod * (w - 1) + 1] end end ### CONSTRAINTS ### - # Constraint 1: Links last time step with first time step, ensuring position in hour 1 is within eligible change from final hour position - # Modified initial virtual state of storage for long duration storage - initialize wth value carried over from last period - # Alternative to cVSoCBalStart constraint which is included when modeling multiple representative periods and long duration storage - # Note: tw_min = hours_per_subperiod*(w-1)+1; tw_max = hours_per_subperiod*w - @constraint(EP, cVreStorVSoCBalLongDurationStorageStart[y in VS_LDS, w=1:REP_PERIOD], - EP[:vCAPRES_VS_VRE_STOR][y,hours_per_subperiod*(w-1)+1] == eVreStorVSoCBalLongDurationStorageStart[y,w]) + # Constraint 1: Links last time step with first time step, ensuring position in hour 1 is within eligible change from final hour position + # Modified initial virtual state of storage for long duration storage - initialize wth value carried over from last period + # Alternative to cVSoCBalStart constraint which is included when modeling multiple representative periods and long duration storage + # Note: tw_min = hours_per_subperiod*(w-1)+1; tw_max = hours_per_subperiod*w + @constraint(EP, + cVreStorVSoCBalLongDurationStorageStart[y in VS_LDS, w = 1:REP_PERIOD], + EP[:vCAPRES_VS_VRE_STOR][y, + hours_per_subperiod * (w - 1) + 1]==eVreStorVSoCBalLongDurationStorageStart[y, w]) # Constraint 2: Storage held in reserve at beginning of period w = storage at beginning of period w-1 + storage built up in period w (after n representative periods) # Multiply storage build up term from prior period with corresponding weight - @constraint(EP, cVreStorVSoCBalLongDurationStorage[y in VS_LDS, r in MODELED_PERIODS_INDEX], - vCAPCONTRSTOR_VSOCw_VRE_STOR[y,mod1(r+1, NPeriods)] == vCAPCONTRSTOR_VSOCw_VRE_STOR[y,r] + vCAPCONTRSTOR_VdSOC_VRE_STOR[y,dfPeriodMap[r,:Rep_Period_Index]]) + @constraint(EP, + cVreStorVSoCBalLongDurationStorage[y in VS_LDS, r in MODELED_PERIODS_INDEX], + vCAPCONTRSTOR_VSOCw_VRE_STOR[y, + mod1(r + 1, NPeriods)]==vCAPCONTRSTOR_VSOCw_VRE_STOR[y, r] + + vCAPCONTRSTOR_VdSOC_VRE_STOR[y, dfPeriodMap[r, :Rep_Period_Index]]) # Constraint 3: Initial reserve storage level for representative periods must also adhere to sub-period storage inventory balance # Initial storage = Final storage - change in storage inventory across representative period - @constraint(EP, cVreStorVSoCBalLongDurationStorageSub[y in VS_LDS, r in REP_PERIODS_INDEX], - vCAPCONTRSTOR_VSOCw_VRE_STOR[y,r] == EP[:vCAPRES_VS_VRE_STOR][y,hours_per_subperiod*dfPeriodMap[r,:Rep_Period_Index]] - vCAPCONTRSTOR_VdSOC_VRE_STOR[y,dfPeriodMap[r,:Rep_Period_Index]]) + @constraint(EP, + cVreStorVSoCBalLongDurationStorageSub[y in VS_LDS, r in REP_PERIODS_INDEX], + vCAPCONTRSTOR_VSOCw_VRE_STOR[y, + r]==EP[:vCAPRES_VS_VRE_STOR][y, + hours_per_subperiod * dfPeriodMap[r, :Rep_Period_Index]] - vCAPCONTRSTOR_VdSOC_VRE_STOR[y, dfPeriodMap[r, :Rep_Period_Index]]) # Constraint 4: Energy held in reserve at the beginning of each modeled period acts as a lower bound on the total energy held in storage - @constraint(EP, cSOCMinCapResLongDurationStorage[y in VS_LDS, r in MODELED_PERIODS_INDEX], EP[:vSOCw_VRE_STOR][y,r] >= vCAPCONTRSTOR_VSOCw_VRE_STOR[y,r]) + @constraint(EP, + cSOCMinCapResLongDurationStorage[y in VS_LDS, r in MODELED_PERIODS_INDEX], + EP[:vSOCw_VRE_STOR][y, r]>=vCAPCONTRSTOR_VSOCw_VRE_STOR[y, r]) end end @@ -2212,15 +2439,14 @@ Lastly, if the co-located resource has a variable renewable energy component, th ``` """ function vre_stor_operational_reserves!(EP::Model, inputs::Dict, setup::Dict) - println("VRE-STOR Operational Reserves Module") ### LOAD DATA & CREATE SETS ### - gen = inputs["RESOURCES"] + gen = inputs["RESOURCES"] gen_VRE_STOR = gen.VreStorage - T = inputs["T"] + T = inputs["T"] VRE_STOR = inputs["VRE_STOR"] STOR = inputs["VS_STOR"] DC_DISCHARGE = inputs["VS_STOR_DC_DISCHARGE"] @@ -2250,10 +2476,10 @@ function vre_stor_operational_reserves!(EP::Model, inputs::Dict, setup::Dict) SOLAR_RSV = intersect(SOLAR, inputs["RSV"]) # Set of solar resources with RSV reserves WIND_REG = intersect(WIND, inputs["REG"]) # Set of wind resources with REG reserves WIND_RSV = intersect(WIND, inputs["RSV"]) # Set of wind resources with RSV reserves - - STOR_REG = intersect(STOR, inputs["REG"]) # Set of storage resources with REG reserves - STOR_RSV = intersect(STOR, inputs["RSV"]) # Set of storage resources with RSV reserves - STOR_REG_RSV_UNION = union(STOR_REG, STOR_RSV) # Set of storage resources with either or both REG and RSV reserves + + STOR_REG = intersect(STOR, inputs["REG"]) # Set of storage resources with REG reserves + STOR_RSV = intersect(STOR, inputs["RSV"]) # Set of storage resources with RSV reserves + STOR_REG_RSV_UNION = union(STOR_REG, STOR_RSV) # Set of storage resources with either or both REG and RSV reserves DC_DISCHARGE_REG = intersect(DC_DISCHARGE, STOR_REG) # Set of DC discharge resources with REG reserves DC_DISCHARGE_RSV = intersect(DC_DISCHARGE, STOR_RSV) # Set of DC discharge resources with RSV reserves AC_DISCHARGE_REG = intersect(AC_DISCHARGE, STOR_REG) # Set of AC discharge resources with REG reserves @@ -2279,173 +2505,179 @@ function vre_stor_operational_reserves!(EP::Model, inputs::Dict, setup::Dict) @variables(EP, begin # Contribution to regulation (primary reserves), assumed to be symmetric (up & down directions equal) - vREG_SOLAR[y in SOLAR_REG, t=1:T] >= 0 - vREG_WIND[y in WIND_REG, t=1:T] >= 0 - vREG_DC_Discharge[y in DC_DISCHARGE_REG, t=1:T] >= 0 - vREG_DC_Charge[y in DC_CHARGE_REG, t=1:T] >= 0 - vREG_AC_Discharge[y in AC_DISCHARGE_REG, t=1:T] >= 0 - vREG_AC_Charge[y in AC_CHARGE_REG, t=1:T] >= 0 + vREG_SOLAR[y in SOLAR_REG, t = 1:T] >= 0 + vREG_WIND[y in WIND_REG, t = 1:T] >= 0 + vREG_DC_Discharge[y in DC_DISCHARGE_REG, t = 1:T] >= 0 + vREG_DC_Charge[y in DC_CHARGE_REG, t = 1:T] >= 0 + vREG_AC_Discharge[y in AC_DISCHARGE_REG, t = 1:T] >= 0 + vREG_AC_Charge[y in AC_CHARGE_REG, t = 1:T] >= 0 # Contribution to operating reserves (secondary reserves or contingency reserves); only model upward reserve requirements - vRSV_SOLAR[y in SOLAR_RSV, t=1:T] >= 0 - vRSV_WIND[y in WIND_RSV, t=1:T] >= 0 - vRSV_DC_Discharge[y in DC_DISCHARGE_RSV, t=1:T] >= 0 - vRSV_DC_Charge[y in DC_CHARGE_RSV, t=1:T] >= 0 - vRSV_AC_Discharge[y in AC_DISCHARGE_RSV, t=1:T] >= 0 - vRSV_AC_Charge[y in AC_CHARGE_RSV, t=1:T] >= 0 + vRSV_SOLAR[y in SOLAR_RSV, t = 1:T] >= 0 + vRSV_WIND[y in WIND_RSV, t = 1:T] >= 0 + vRSV_DC_Discharge[y in DC_DISCHARGE_RSV, t = 1:T] >= 0 + vRSV_DC_Charge[y in DC_CHARGE_RSV, t = 1:T] >= 0 + vRSV_AC_Discharge[y in AC_DISCHARGE_RSV, t = 1:T] >= 0 + vRSV_AC_Charge[y in AC_CHARGE_RSV, t = 1:T] >= 0 end) ### EXPRESSIONS ### - @expression(EP, eVreStorRegOnlyBalance[y in VRE_STOR_REG, t=1:T], JuMP.AffExpr()) - @expression(EP, eVreStorRsvOnlyBalance[y in VRE_STOR_RSV, t=1:T], JuMP.AffExpr()) - @expression(EP, eDischargeDCMin[y in DC_DISCHARGE, t=1:T], JuMP.AffExpr()) - @expression(EP, eChargeDCMin[y in DC_CHARGE, t=1:T], JuMP.AffExpr()) - @expression(EP, eDischargeACMin[y in AC_DISCHARGE, t=1:T], JuMP.AffExpr()) - @expression(EP, eChargeACMin[y in AC_CHARGE, t=1:T], JuMP.AffExpr()) - @expression(EP, eChargeMax[y in STOR_REG_RSV_UNION, t=1:T], JuMP.AffExpr()) - @expression(EP, eDischargeMax[y in STOR_REG_RSV_UNION, t=1:T], JuMP.AffExpr()) + @expression(EP, eVreStorRegOnlyBalance[y in VRE_STOR_REG, t = 1:T], JuMP.AffExpr()) + @expression(EP, eVreStorRsvOnlyBalance[y in VRE_STOR_RSV, t = 1:T], JuMP.AffExpr()) + @expression(EP, eDischargeDCMin[y in DC_DISCHARGE, t = 1:T], JuMP.AffExpr()) + @expression(EP, eChargeDCMin[y in DC_CHARGE, t = 1:T], JuMP.AffExpr()) + @expression(EP, eDischargeACMin[y in AC_DISCHARGE, t = 1:T], JuMP.AffExpr()) + @expression(EP, eChargeACMin[y in AC_CHARGE, t = 1:T], JuMP.AffExpr()) + @expression(EP, eChargeMax[y in STOR_REG_RSV_UNION, t = 1:T], JuMP.AffExpr()) + @expression(EP, eDischargeMax[y in STOR_REG_RSV_UNION, t = 1:T], JuMP.AffExpr()) - for t=1:T + for t in 1:T for y in DC_DISCHARGE - eDischargeDCMin[y,t] += EP[:vP_DC_DISCHARGE][y,t] - eDischargeMax[y,t] += EP[:vP_DC_DISCHARGE][y,t]/by_rid(y,:eff_down_dc) + eDischargeDCMin[y, t] += EP[:vP_DC_DISCHARGE][y, t] + eDischargeMax[y, t] += EP[:vP_DC_DISCHARGE][y, t] / by_rid(y, :eff_down_dc) end for y in DC_CHARGE - eChargeDCMin[y,t] += EP[:vP_DC_CHARGE][y,t] - eChargeMax[y,t] += by_rid(y,:eff_up_dc)*EP[:vP_DC_CHARGE][y,t] + eChargeDCMin[y, t] += EP[:vP_DC_CHARGE][y, t] + eChargeMax[y, t] += by_rid(y, :eff_up_dc) * EP[:vP_DC_CHARGE][y, t] end for y in AC_DISCHARGE - eDischargeACMin[y,t] += EP[:vP_AC_DISCHARGE][y,t] - eDischargeMax[y,t] += EP[:vP_AC_DISCHARGE][y,t]/by_rid(y,:eff_down_ac) + eDischargeACMin[y, t] += EP[:vP_AC_DISCHARGE][y, t] + eDischargeMax[y, t] += EP[:vP_AC_DISCHARGE][y, t] / by_rid(y, :eff_down_ac) end for y in AC_CHARGE - eChargeACMin[y,t] += EP[:vP_AC_CHARGE][y,t] - eChargeMax[y,t] += by_rid(y,:eff_up_ac)*EP[:vP_AC_CHARGE][y,t] + eChargeACMin[y, t] += EP[:vP_AC_CHARGE][y, t] + eChargeMax[y, t] += by_rid(y, :eff_up_ac) * EP[:vP_AC_CHARGE][y, t] end for y in SOLAR_REG - eVreStorRegOnlyBalance[y,t] += by_rid(y,:etainverter)*vREG_SOLAR[y,t] - EP[:eGridExport][y,t] += by_rid(y,:etainverter)*vREG_SOLAR[y,t] - EP[:eInverterExport][y,t] += by_rid(y,:etainverter)*vREG_SOLAR[y,t] - EP[:eSolarGenMaxS][y,t] += vREG_SOLAR[y,t] + eVreStorRegOnlyBalance[y, t] += by_rid(y, :etainverter) * vREG_SOLAR[y, t] + EP[:eGridExport][y, t] += by_rid(y, :etainverter) * vREG_SOLAR[y, t] + EP[:eInverterExport][y, t] += by_rid(y, :etainverter) * vREG_SOLAR[y, t] + EP[:eSolarGenMaxS][y, t] += vREG_SOLAR[y, t] end for y in SOLAR_RSV - eVreStorRsvOnlyBalance[y,t] += by_rid(y,:etainverter)*vRSV_SOLAR[y,t] - EP[:eGridExport][y,t] += by_rid(y,:etainverter)*vRSV_SOLAR[y,t] - EP[:eInverterExport][y,t] += by_rid(y,:etainverter)*vRSV_SOLAR[y,t] - EP[:eSolarGenMaxS][y,t] += vRSV_SOLAR[y,t] + eVreStorRsvOnlyBalance[y, t] += by_rid(y, :etainverter) * vRSV_SOLAR[y, t] + EP[:eGridExport][y, t] += by_rid(y, :etainverter) * vRSV_SOLAR[y, t] + EP[:eInverterExport][y, t] += by_rid(y, :etainverter) * vRSV_SOLAR[y, t] + EP[:eSolarGenMaxS][y, t] += vRSV_SOLAR[y, t] end for y in WIND_REG - eVreStorRegOnlyBalance[y,t] += vREG_WIND[y,t] - EP[:eGridExport][y,t] += vREG_WIND[y,t] - EP[:eWindGenMaxW][y,t] += vREG_WIND[y,t] + eVreStorRegOnlyBalance[y, t] += vREG_WIND[y, t] + EP[:eGridExport][y, t] += vREG_WIND[y, t] + EP[:eWindGenMaxW][y, t] += vREG_WIND[y, t] end for y in WIND_RSV - eVreStorRsvOnlyBalance[y,t] += vRSV_WIND[y,t] - EP[:eGridExport][y,t] += vRSV_WIND[y,t] - EP[:eWindGenMaxW][y,t] += vRSV_WIND[y,t] + eVreStorRsvOnlyBalance[y, t] += vRSV_WIND[y, t] + EP[:eGridExport][y, t] += vRSV_WIND[y, t] + EP[:eWindGenMaxW][y, t] += vRSV_WIND[y, t] end for y in DC_DISCHARGE_REG - eVreStorRegOnlyBalance[y,t] += by_rid(y,:etainverter)*vREG_DC_Discharge[y,t] - eDischargeDCMin[y,t] -= vREG_DC_Discharge[y,t] - eDischargeMax[y,t] += EP[:vREG_DC_Discharge][y,t]/by_rid(y,:eff_down_dc) - EP[:eGridExport][y,t] += by_rid(y,:etainverter)*vREG_DC_Discharge[y,t] - EP[:eInverterExport][y,t] += by_rid(y,:etainverter)*vREG_DC_Discharge[y,t] + eVreStorRegOnlyBalance[y, t] += by_rid(y, :etainverter) * + vREG_DC_Discharge[y, t] + eDischargeDCMin[y, t] -= vREG_DC_Discharge[y, t] + eDischargeMax[y, t] += EP[:vREG_DC_Discharge][y, t] / by_rid(y, :eff_down_dc) + EP[:eGridExport][y, t] += by_rid(y, :etainverter) * vREG_DC_Discharge[y, t] + EP[:eInverterExport][y, t] += by_rid(y, :etainverter) * vREG_DC_Discharge[y, t] end for y in DC_DISCHARGE_RSV - eVreStorRsvOnlyBalance[y,t] += by_rid(y,:etainverter)*vRSV_DC_Discharge[y,t] - eDischargeMax[y,t] += EP[:vRSV_DC_Discharge][y,t]/by_rid(y,:eff_down_dc) - EP[:eGridExport][y,t] += by_rid(y,:etainverter)*vRSV_DC_Discharge[y,t] - EP[:eInverterExport][y,t] += by_rid(y,:etainverter)*vRSV_DC_Discharge[y,t] + eVreStorRsvOnlyBalance[y, t] += by_rid(y, :etainverter) * + vRSV_DC_Discharge[y, t] + eDischargeMax[y, t] += EP[:vRSV_DC_Discharge][y, t] / by_rid(y, :eff_down_dc) + EP[:eGridExport][y, t] += by_rid(y, :etainverter) * vRSV_DC_Discharge[y, t] + EP[:eInverterExport][y, t] += by_rid(y, :etainverter) * vRSV_DC_Discharge[y, t] end for y in DC_CHARGE_REG - eVreStorRegOnlyBalance[y,t] += vREG_DC_Charge[y,t]/by_rid(y,:etainverter) - eChargeDCMin[y,t] -= vREG_DC_Charge[y,t] - eChargeMax[y,t] += by_rid(y,:eff_up_dc)*EP[:vREG_DC_Charge][y,t] - EP[:eGridExport][y,t] += vREG_DC_Charge[y,t]/by_rid(y,:etainverter) - EP[:eInverterExport][y,t] += vREG_DC_Charge[y,t]/by_rid(y,:etainverter) + eVreStorRegOnlyBalance[y, t] += vREG_DC_Charge[y, t] / by_rid(y, :etainverter) + eChargeDCMin[y, t] -= vREG_DC_Charge[y, t] + eChargeMax[y, t] += by_rid(y, :eff_up_dc) * EP[:vREG_DC_Charge][y, t] + EP[:eGridExport][y, t] += vREG_DC_Charge[y, t] / by_rid(y, :etainverter) + EP[:eInverterExport][y, t] += vREG_DC_Charge[y, t] / by_rid(y, :etainverter) end for y in DC_CHARGE_RSV - eVreStorRsvOnlyBalance[y,t] += vRSV_DC_Charge[y,t]/by_rid(y,:etainverter) - eChargeDCMin[y,t] -= vRSV_DC_Charge[y,t] + eVreStorRsvOnlyBalance[y, t] += vRSV_DC_Charge[y, t] / by_rid(y, :etainverter) + eChargeDCMin[y, t] -= vRSV_DC_Charge[y, t] end for y in AC_DISCHARGE_REG - eVreStorRegOnlyBalance[y,t] += vREG_AC_Discharge[y,t] - eDischargeACMin[y,t] -= vREG_AC_Discharge[y,t] - eDischargeMax[y,t] += EP[:vREG_AC_Discharge][y,t]/by_rid(y,:eff_down_ac) - EP[:eGridExport][y,t] += vREG_AC_Discharge[y,t] + eVreStorRegOnlyBalance[y, t] += vREG_AC_Discharge[y, t] + eDischargeACMin[y, t] -= vREG_AC_Discharge[y, t] + eDischargeMax[y, t] += EP[:vREG_AC_Discharge][y, t] / by_rid(y, :eff_down_ac) + EP[:eGridExport][y, t] += vREG_AC_Discharge[y, t] end for y in AC_DISCHARGE_RSV - eVreStorRsvOnlyBalance[y,t] += vRSV_AC_Discharge[y,t] - eDischargeMax[y,t] += EP[:vRSV_AC_Discharge][y,t]/by_rid(y,:eff_down_ac) - EP[:eGridExport][y,t] += vRSV_AC_Discharge[y,t] + eVreStorRsvOnlyBalance[y, t] += vRSV_AC_Discharge[y, t] + eDischargeMax[y, t] += EP[:vRSV_AC_Discharge][y, t] / by_rid(y, :eff_down_ac) + EP[:eGridExport][y, t] += vRSV_AC_Discharge[y, t] end for y in AC_CHARGE_REG - eVreStorRegOnlyBalance[y,t] += vREG_AC_Charge[y,t] - eChargeACMin[y,t] -= vREG_AC_Charge[y,t] - eChargeMax[y,t] += by_rid(y,:eff_down_ac)*EP[:vREG_AC_Charge][y,t] - EP[:eGridExport][y,t] += vREG_AC_Charge[y,t] + eVreStorRegOnlyBalance[y, t] += vREG_AC_Charge[y, t] + eChargeACMin[y, t] -= vREG_AC_Charge[y, t] + eChargeMax[y, t] += by_rid(y, :eff_down_ac) * EP[:vREG_AC_Charge][y, t] + EP[:eGridExport][y, t] += vREG_AC_Charge[y, t] end for y in AC_CHARGE_RSV - eVreStorRsvOnlyBalance[y,t] += vRSV_AC_Charge[y,t] - eChargeACMin[y,t] -= vRSV_AC_Charge[y,t] + eVreStorRsvOnlyBalance[y, t] += vRSV_AC_Charge[y, t] + eChargeACMin[y, t] -= vRSV_AC_Charge[y, t] end for y in VS_SYM_DC_REG - EP[:eChargeDischargeMaxDC][y,t] += (vREG_DC_Discharge[y,t] - + vREG_DC_Charge[y,t]) + EP[:eChargeDischargeMaxDC][y, t] += (vREG_DC_Discharge[y, t] + + + vREG_DC_Charge[y, t]) end for y in VS_SYM_DC_RSV - EP[:eChargeDischargeMaxDC][y,t] += vRSV_DC_Discharge[y,t] + EP[:eChargeDischargeMaxDC][y, t] += vRSV_DC_Discharge[y, t] end for y in VS_SYM_AC_REG - EP[:eChargeDischargeMaxAC][y,t] += (vREG_AC_Discharge[y,t] - + vREG_AC_Charge[y,t]) + EP[:eChargeDischargeMaxAC][y, t] += (vREG_AC_Discharge[y, t] + + + vREG_AC_Charge[y, t]) end for y in VS_SYM_AC_RSV - EP[:eChargeDischargeMaxAC][y,t] += vRSV_AC_Discharge[y,t] + EP[:eChargeDischargeMaxAC][y, t] += vRSV_AC_Discharge[y, t] end for y in VS_ASYM_DC_DISCHARGE_REG - EP[:eVreStorMaxDischargingDC][y,t] += vREG_DC_Discharge[y,t] + EP[:eVreStorMaxDischargingDC][y, t] += vREG_DC_Discharge[y, t] end for y in VS_ASYM_DC_DISCHARGE_RSV - EP[:eVreStorMaxDischargingDC][y,t] += vRSV_DC_Discharge[y,t] + EP[:eVreStorMaxDischargingDC][y, t] += vRSV_DC_Discharge[y, t] end for y in VS_ASYM_DC_CHARGE_REG - EP[:eVreStorMaxChargingDC][y,t] += vREG_DC_Charge[y,t] + EP[:eVreStorMaxChargingDC][y, t] += vREG_DC_Charge[y, t] end for y in VS_ASYM_AC_DISCHARGE_REG - EP[:eVreStorMaxDischargingAC][y,t] += vREG_AC_Discharge[y,t] + EP[:eVreStorMaxDischargingAC][y, t] += vREG_AC_Discharge[y, t] end for y in VS_ASYM_AC_DISCHARGE_RSV - EP[:eVreStorMaxDischargingAC][y,t] += vRSV_AC_Discharge[y,t] + EP[:eVreStorMaxDischargingAC][y, t] += vRSV_AC_Discharge[y, t] end for y in VS_ASYM_AC_CHARGE_REG - EP[:eVreStorMaxChargingAC][y,t] += vREG_AC_Charge[y,t] + EP[:eVreStorMaxChargingAC][y, t] += vREG_AC_Charge[y, t] end end if CapacityReserveMargin > 0 - for t=1:T + for t in 1:T for y in DC_DISCHARGE - eDischargeMax[y,t] += EP[:vCAPRES_DC_DISCHARGE][y,t]/by_rid(y,:eff_down_dc) + eDischargeMax[y, t] += EP[:vCAPRES_DC_DISCHARGE][y, t] / + by_rid(y, :eff_down_dc) end for y in AC_DISCHARGE - eDischargeMax[y,t] += EP[:vCAPRES_AC_DISCHARGE][y,t]/by_rid(y,:eff_down_ac) + eDischargeMax[y, t] += EP[:vCAPRES_AC_DISCHARGE][y, t] / + by_rid(y, :eff_down_ac) end end end @@ -2454,88 +2686,121 @@ function vre_stor_operational_reserves!(EP::Model, inputs::Dict, setup::Dict) # Frequency regulation and operating reserves for all co-located VRE-STOR resources if !isempty(VRE_STOR_REG_RSV) - @constraints(EP, begin - # Maximum VRE-STOR contribution to reserves is a specified fraction of installed grid connection capacity - [y in VRE_STOR_REG_RSV, t=1:T], EP[:vREG][y,t] <= reg_max(gen[y])*EP[:eTotalCap][y] - [y in VRE_STOR_REG_RSV, t=1:T], EP[:vRSV][y,t] <= rsv_max(gen[y])*EP[:eTotalCap][y] - - # Actual contribution to regulation and reserves is sum of auxilary variables - [y in VRE_STOR_REG_RSV, t=1:T], EP[:vREG][y,t] == eVreStorRegOnlyBalance[y,t] - [y in VRE_STOR_REG_RSV, t=1:T], EP[:vRSV][y,t] == eVreStorRsvOnlyBalance[y,t] - end) + @constraints(EP, + begin + # Maximum VRE-STOR contribution to reserves is a specified fraction of installed grid connection capacity + [y in VRE_STOR_REG_RSV, t = 1:T], + EP[:vREG][y, t] <= reg_max(gen[y]) * EP[:eTotalCap][y] + [y in VRE_STOR_REG_RSV, t = 1:T], + EP[:vRSV][y, t] <= rsv_max(gen[y]) * EP[:eTotalCap][y] + + # Actual contribution to regulation and reserves is sum of auxilary variables + [y in VRE_STOR_REG_RSV, t = 1:T], + EP[:vREG][y, t] == eVreStorRegOnlyBalance[y, t] + [y in VRE_STOR_REG_RSV, t = 1:T], + EP[:vRSV][y, t] == eVreStorRsvOnlyBalance[y, t] + end) end if !isempty(VRE_STOR_REG_ONLY) - @constraints(EP, begin - # Maximum VRE-STOR contribution to reserves is a specified fraction of installed grid connection capacity - [y in VRE_STOR_REG_ONLY, t=1:T], EP[:vREG][y,t] <= reg_max(gen[y])*EP[:eTotalCap][y] - - # Actual contribution to regulation is sum of auxilary variables - [y in VRE_STOR_REG_ONLY, t=1:T], EP[:vREG][y,t] == eVreStorRegOnlyBalance[y,t] - end) + @constraints(EP, + begin + # Maximum VRE-STOR contribution to reserves is a specified fraction of installed grid connection capacity + [y in VRE_STOR_REG_ONLY, t = 1:T], + EP[:vREG][y, t] <= reg_max(gen[y]) * EP[:eTotalCap][y] + + # Actual contribution to regulation is sum of auxilary variables + [y in VRE_STOR_REG_ONLY, t = 1:T], + EP[:vREG][y, t] == eVreStorRegOnlyBalance[y, t] + end) end if !isempty(VRE_STOR_RSV_ONLY) - @constraints(EP, begin - # Maximum VRE-STOR contribution to reserves is a specified fraction of installed grid connection capacity - [y in VRE_STOR_RSV_ONLY, t=1:T], EP[:vRSV][y,t] <= rsv_max(gen[y])*EP[:eTotalCap][y] - - # Actual contribution to reserves is sum of auxilary variables - [y in VRE_STOR_RSV_ONLY, t=1:T], EP[:vRSV][y,t] == eVreStorRsvOnlyBalance[y,t] - end) + @constraints(EP, + begin + # Maximum VRE-STOR contribution to reserves is a specified fraction of installed grid connection capacity + [y in VRE_STOR_RSV_ONLY, t = 1:T], + EP[:vRSV][y, t] <= rsv_max(gen[y]) * EP[:eTotalCap][y] + + # Actual contribution to reserves is sum of auxilary variables + [y in VRE_STOR_RSV_ONLY, t = 1:T], + EP[:vRSV][y, t] == eVreStorRsvOnlyBalance[y, t] + end) end # Frequency regulation and operating reserves for VRE-STOR resources with a VRE component if !isempty(SOLAR_REG) - @constraints(EP, begin - # Maximum generation and contribution to reserves up must be greater than zero - [y in SOLAR_REG, t=1:T], EP[:vP_SOLAR][y,t] - EP[:vREG_SOLAR][y,t] >= 0 - end) + @constraints(EP, + begin + # Maximum generation and contribution to reserves up must be greater than zero + [y in SOLAR_REG, t = 1:T], EP[:vP_SOLAR][y, t] - EP[:vREG_SOLAR][y, t] >= 0 + end) end if !isempty(WIND_REG) - @constraints(EP, begin - # Maximum generation and contribution to reserves up must be greater than zero - [y in WIND_REG, t=1:T], EP[:vP_WIND][y,t] - EP[:vREG_WIND][y,t] >= 0 - end) + @constraints(EP, + begin + # Maximum generation and contribution to reserves up must be greater than zero + [y in WIND_REG, t = 1:T], EP[:vP_WIND][y, t] - EP[:vREG_WIND][y, t] >= 0 + end) end # Frequency regulation and operating reserves for VRE-STOR resources with a storage component if !isempty(STOR_REG_RSV_UNION) - @constraints(EP, begin - # Maximum DC charging rate plus contribution to reserves up must be greater than zero - # Note: when charging, reducing charge rate is contributing to upwards reserve & regulation as it drops net demand - [y in DC_CHARGE, t=1:T], eChargeDCMin[y,t] >= 0 - - # Maximum AC charging rate plus contribution to reserves up must be greater than zero - # Note: when charging, reducing charge rate is contributing to upwards reserve & regulation as it drops net demand - [y in AC_CHARGE, t=1:T], eChargeACMin[y,t] >= 0 - - # Maximum DC discharging rate and contribution to reserves down must be greater than zero - # Note: when discharging, reducing discharge rate is contributing to downwards regulation as it drops net supply - [y in DC_DISCHARGE, t=1:T], eDischargeDCMin[y,t] >= 0 - - # Maximum AC discharging rate and contribution to reserves down must be greater than zero - # Note: when discharging, reducing discharge rate is contributing to downwards regulation as it drops net supply - [y in AC_DISCHARGE, t=1:T], eDischargeACMin[y,t] >= 0 - - # Maximum charging rate plus contributions must be less than available storage capacity - [y in STOR_REG_RSV_UNION, t=1:T], eChargeMax[y,t] <= EP[:eTotalCap_STOR][y]-EP[:vS_VRE_STOR][y, hoursbefore(p,t,1)] - - # Maximum discharging rate and contributions must be less than the available stored energy in prior period - # wrapping from end of sample period to start of sample period for energy capacity constraint - [y in STOR_REG_RSV_UNION, t=1:T], eDischargeMax[y,t] <= EP[:vS_VRE_STOR][y, hoursbefore(p,t,1)] - end) + @constraints(EP, + begin + # Maximum DC charging rate plus contribution to reserves up must be greater than zero + # Note: when charging, reducing charge rate is contributing to upwards reserve & regulation as it drops net demand + [y in DC_CHARGE, t = 1:T], eChargeDCMin[y, t] >= 0 + + # Maximum AC charging rate plus contribution to reserves up must be greater than zero + # Note: when charging, reducing charge rate is contributing to upwards reserve & regulation as it drops net demand + [y in AC_CHARGE, t = 1:T], eChargeACMin[y, t] >= 0 + + # Maximum DC discharging rate and contribution to reserves down must be greater than zero + # Note: when discharging, reducing discharge rate is contributing to downwards regulation as it drops net supply + [y in DC_DISCHARGE, t = 1:T], eDischargeDCMin[y, t] >= 0 + + # Maximum AC discharging rate and contribution to reserves down must be greater than zero + # Note: when discharging, reducing discharge rate is contributing to downwards regulation as it drops net supply + [y in AC_DISCHARGE, t = 1:T], eDischargeACMin[y, t] >= 0 + + # Maximum charging rate plus contributions must be less than available storage capacity + [y in STOR_REG_RSV_UNION, t = 1:T], + eChargeMax[y, t] <= + EP[:eTotalCap_STOR][y] - EP[:vS_VRE_STOR][y, hoursbefore(p, t, 1)] + + # Maximum discharging rate and contributions must be less than the available stored energy in prior period + # wrapping from end of sample period to start of sample period for energy capacity constraint + [y in STOR_REG_RSV_UNION, t = 1:T], + eDischargeMax[y, t] <= EP[:vS_VRE_STOR][y, hoursbefore(p, t, 1)] + end) end # Total system reserve constraints - @expression(EP, eRegReqVreStor[t=1:T], inputs["pReg_Req_VRE"]*sum(inputs["pP_Max_Solar"][y,t]*EP[:eTotalCap_SOLAR][y]*by_rid(y, :etainverter) for y in SOLAR_REG) - + inputs["pReg_Req_VRE"]*sum(inputs["pP_Max_Wind"][y,t]*EP[:eTotalCap_WIND][y] for y in WIND_REG)) - @expression(EP, eRsvReqVreStor[t=1:T], inputs["pRsv_Req_VRE"]*sum(inputs["pP_Max_Solar"][y,t]*EP[:eTotalCap_SOLAR][y]*by_rid(y, :etainverter) for y in SOLAR_RSV) - + inputs["pRsv_Req_VRE"]*sum(inputs["pP_Max_Wind"][y,t]*EP[:eTotalCap_WIND][y] for y in WIND_RSV)) + @expression(EP, + eRegReqVreStor[t = 1:T], + inputs["pReg_Req_VRE"] * + sum(inputs["pP_Max_Solar"][y, t] * EP[:eTotalCap_SOLAR][y] * + by_rid(y, :etainverter) for y in SOLAR_REG) + +inputs["pReg_Req_VRE"] * + sum(inputs["pP_Max_Wind"][y, t] * EP[:eTotalCap_WIND][y] for y in WIND_REG)) + @expression(EP, + eRsvReqVreStor[t = 1:T], + inputs["pRsv_Req_VRE"] * + sum(inputs["pP_Max_Solar"][y, t] * EP[:eTotalCap_SOLAR][y] * + by_rid(y, :etainverter) for y in SOLAR_RSV) + +inputs["pRsv_Req_VRE"] * + sum(inputs["pP_Max_Wind"][y, t] * EP[:eTotalCap_WIND][y] for y in WIND_RSV)) if !isempty(VRE_STOR_REG) - @constraint(EP, cRegVreStor[t=1:T], sum(EP[:vREG][y,t] for y in inputs["REG"]) >= EP[:eRegReq][t] + eRegReqVreStor[t]) + @constraint(EP, + cRegVreStor[t = 1:T], + sum(EP[:vREG][y, t] for y in inputs["REG"])>=EP[:eRegReq][t] + + eRegReqVreStor[t]) end if !isempty(VRE_STOR_RSV) - @constraint(EP, cRsvReqVreStor[t=1:T], sum(EP[:vRSV][y,t] for y in inputs["RSV"]) + EP[:vUNMET_RSV][t] >= EP[:eRsvReq][t] + eRsvReqVreStor[t]) + @constraint(EP, + cRsvReqVreStor[t = 1:T], + sum(EP[:vRSV][y, t] for y in inputs["RSV"]) + + EP[:vUNMET_RSV][t]>=EP[:eRsvReq][t] + eRsvReqVreStor[t]) end end diff --git a/src/model/solve_model.jl b/src/model/solve_model.jl index 6519a8c863..e3713bc67b 100644 --- a/src/model/solve_model.jl +++ b/src/model/solve_model.jl @@ -11,28 +11,28 @@ nothing (modifies an existing-solved model in the memory). `solve()` must be run """ function fix_integers(jump_model::Model) - ################################################################################ - ## function fix_integers() - ## - ## inputs: jump_model - a model object containing that has been previously solved. - ## - ## description: fixes the iteger variables ones the model has been solved in order - ## to calculate approximations of dual variables - ## - ## returns: no result since it modifies an existing-solved model in the memory. - ## solve() must be run again to solve and getdual veriables - ## - ################################################################################ - values = Dict(v => value(v) for v in all_variables(jump_model)) - for v in all_variables(jump_model) - if is_integer(v) - fix(v,values[v],force=true) - unset_integer(v) + ################################################################################ + ## function fix_integers() + ## + ## inputs: jump_model - a model object containing that has been previously solved. + ## + ## description: fixes the iteger variables ones the model has been solved in order + ## to calculate approximations of dual variables + ## + ## returns: no result since it modifies an existing-solved model in the memory. + ## solve() must be run again to solve and getdual veriables + ## + ################################################################################ + values = Dict(v => value(v) for v in all_variables(jump_model)) + for v in all_variables(jump_model) + if is_integer(v) + fix(v, values[v], force = true) + unset_integer(v) elseif is_binary(v) - fix(v,values[v],force=true) - unset_binary(v) + fix(v, values[v], force = true) + unset_binary(v) end - end + end end @doc raw""" @@ -48,62 +48,58 @@ Description: Solves and extracts solution variables for later processing - `solver_time::Float64`: time taken to solve the model """ function solve_model(EP::Model, setup::Dict) - ## Start solve timer - solver_start_time = time() - solver_time = time() - - ## Solve Model - optimize!(EP) - - if has_values(EP) - - if has_duals(EP) # fully linear model - println("LP solved for primal") - else - println("MILP solved for primal") - end - - ## Record solver time - solver_time = time() - solver_start_time - elseif setup["ComputeConflicts"]==0 - - @info "No model solution. You can try to set ComputeConflicts to 1 in the genx_settings.yml file to compute conflicting constraints." - - elseif setup["ComputeConflicts"]==1 - - @info "No model solution. Trying to identify conflicting constriants..." + ## Start solve timer + solver_start_time = time() + solver_time = time() + + ## Solve Model + optimize!(EP) + + if has_values(EP) + if has_duals(EP) # fully linear model + println("LP solved for primal") + else + println("MILP solved for primal") + end - try - compute_conflict!(EP) - catch e - if isa(e, JuMP.ArgumentError) - @warn "$(solver_name(EP)) does not support computing conflicting constraints. This is available using either Gurobi or CPLEX." - solver_time = time() - solver_start_time - return EP, solver_time - else - rethrow(e) - end - end + ## Record solver time + solver_time = time() - solver_start_time + elseif setup["ComputeConflicts"] == 0 + @info "No model solution. You can try to set ComputeConflicts to 1 in the genx_settings.yml file to compute conflicting constraints." + + elseif setup["ComputeConflicts"] == 1 + @info "No model solution. Trying to identify conflicting constriants..." + + try + compute_conflict!(EP) + catch e + if isa(e, JuMP.ArgumentError) + @warn "$(solver_name(EP)) does not support computing conflicting constraints. This is available using either Gurobi or CPLEX." + solver_time = time() - solver_start_time + return EP, solver_time + else + rethrow(e) + end + end - list_of_conflicting_constraints = ConstraintRef[] - if get_attribute(EP, MOI.ConflictStatus()) == MOI.CONFLICT_FOUND - for (F, S) in list_of_constraint_types(EP) - for con in all_constraints(EP, F, S) - if get_attribute(con, MOI.ConstraintConflictStatus()) == MOI.IN_CONFLICT - push!(list_of_conflicting_constraints, con) - end - end - end - display(list_of_conflicting_constraints) - solver_time = time() - solver_start_time - return EP, solver_time, list_of_conflicting_constraints - else - @info "Conflicts computation failed." - solver_time = time() - solver_start_time - return EP, solver_time, list_of_conflicting_constraints - end + list_of_conflicting_constraints = ConstraintRef[] + if get_attribute(EP, MOI.ConflictStatus()) == MOI.CONFLICT_FOUND + for (F, S) in list_of_constraint_types(EP) + for con in all_constraints(EP, F, S) + if get_attribute(con, MOI.ConstraintConflictStatus()) == MOI.IN_CONFLICT + push!(list_of_conflicting_constraints, con) + end + end + end + display(list_of_conflicting_constraints) + solver_time = time() - solver_start_time + return EP, solver_time, list_of_conflicting_constraints + else + @info "Conflicts computation failed." + solver_time = time() - solver_start_time + return EP, solver_time, list_of_conflicting_constraints + end + end - end - - return EP, solver_time -end # END solve_model() \ No newline at end of file + return EP, solver_time +end # END solve_model() diff --git a/src/model/utility.jl b/src/model/utility.jl index 22e7329e9b..15e6841958 100644 --- a/src/model/utility.jl +++ b/src/model/utility.jl @@ -11,8 +11,8 @@ For example, if p = 10, 1 hour before t=11 is t=20 """ function hoursbefore(p::Int, t::Int, b::Int)::Int - period = div(t - 1, p) - return period * p + mod1(t - b, p) + period = div(t - 1, p) + return period * p + mod1(t - b, p) end @doc raw""" @@ -23,11 +23,10 @@ to allow for example b=1:3 to fetch a Vector{Int} of the three hours before time index t. """ function hoursbefore(p::Int, t::Int, b::UnitRange{Int})::Vector{Int} - period = div(t - 1, p) - return period * p .+ mod1.(t .- b, p) + period = div(t - 1, p) + return period * p .+ mod1.(t .- b, p) end - @doc raw""" hoursafter(p::Int, t::Int, a::Int) @@ -55,7 +54,6 @@ time index t. function hoursafter(p::Int, t::Int, a::UnitRange{Int})::Vector{Int} period = div(t - 1, p) return period * p .+ mod1.(t .+ a, p) - end @doc raw""" @@ -64,7 +62,7 @@ end This function checks if a column in a dataframe is all zeros. """ function is_nonzero(df::DataFrame, col::Symbol)::BitVector - convert(BitVector, df[!, col] .> 0)::BitVector + convert(BitVector, df[!, col] .> 0)::BitVector end function is_nonzero(rs::Vector{<:AbstractResource}, col::Symbol) @@ -82,4 +80,3 @@ function by_rid_res(rid::Integer, sym::Symbol, rs::Vector{<:AbstractResource}) f = isdefined(GenX, sym) ? getfield(GenX, sym) : x -> getproperty(x, sym) return f(r) end - diff --git a/src/multi_stage/configure_multi_stage_inputs.jl b/src/multi_stage/configure_multi_stage_inputs.jl index 870d98cdca..0bd5d45928 100644 --- a/src/multi_stage/configure_multi_stage_inputs.jl +++ b/src/multi_stage/configure_multi_stage_inputs.jl @@ -21,29 +21,32 @@ NOTE: The inv\_costs\_yr and crp arrays must be the same length; values with the returns: array object containing overnight capital costs, the discounted sum of annual investment costs incured within the model horizon. """ -function compute_overnight_capital_cost(settings_d::Dict,inv_costs_yr::Array,crp::Array, tech_wacc::Array) - - cur_stage = settings_d["CurStage"] # Current model - num_stages = settings_d["NumStages"] # Total number of model stages - stage_lens = settings_d["StageLengths"] - - # 1) For each resource, find the minimum of the capital recovery period and the end of the model horizon - # Total time between the end of the final model stage and the start of the current stage - model_yrs_remaining = sum(stage_lens[cur_stage:end]) - - # We will sum annualized costs through the full capital recovery period or the end of planning horizon, whichever comes first - payment_yrs_remaining = min.(crp, model_yrs_remaining) - - # KEY ASSUMPTION: Investment costs after the planning horizon are fully recoverable, so we don't need to include these costs - # 2) Compute the present value of investment associated with capital recovery period within the model horizon - discounting to year 1 and not year 0 - # (Factor to adjust discounting to year 0 for capital cost is included in the discounting coefficient applied to all terms in the objective function value.) - occ = zeros(length(inv_costs_yr)) - for i in 1:length(occ) - occ[i] = sum(inv_costs_yr[i]/(1+tech_wacc[i]) .^ (p) for p=1:payment_yrs_remaining[i]) - end +function compute_overnight_capital_cost(settings_d::Dict, + inv_costs_yr::Array, + crp::Array, + tech_wacc::Array) + cur_stage = settings_d["CurStage"] # Current model + num_stages = settings_d["NumStages"] # Total number of model stages + stage_lens = settings_d["StageLengths"] + + # 1) For each resource, find the minimum of the capital recovery period and the end of the model horizon + # Total time between the end of the final model stage and the start of the current stage + model_yrs_remaining = sum(stage_lens[cur_stage:end]) + + # We will sum annualized costs through the full capital recovery period or the end of planning horizon, whichever comes first + payment_yrs_remaining = min.(crp, model_yrs_remaining) + + # KEY ASSUMPTION: Investment costs after the planning horizon are fully recoverable, so we don't need to include these costs + # 2) Compute the present value of investment associated with capital recovery period within the model horizon - discounting to year 1 and not year 0 + # (Factor to adjust discounting to year 0 for capital cost is included in the discounting coefficient applied to all terms in the objective function value.) + occ = zeros(length(inv_costs_yr)) + for i in 1:length(occ) + occ[i] = sum(inv_costs_yr[i] / (1 + tech_wacc[i]) .^ (p) + for p in 1:payment_yrs_remaining[i]) + end - # 3) Return the overnight capital cost (discounted sum of annual investment costs incured within the model horizon) - return occ + # 3) Return the overnight capital cost (discounted sum of annual investment costs incured within the model horizon) + return occ end @doc raw""" @@ -67,91 +70,139 @@ inputs: returns: dictionary containing updated model inputs, to be used in the generate\_model() method. """ -function configure_multi_stage_inputs(inputs_d::Dict, settings_d::Dict, NetworkExpansion::Int64) - +function configure_multi_stage_inputs(inputs_d::Dict, + settings_d::Dict, + NetworkExpansion::Int64) gen = inputs_d["RESOURCES"] - # Parameter inputs when multi-year discounting is activated - cur_stage = settings_d["CurStage"] - stage_len = settings_d["StageLengths"][cur_stage] - wacc = settings_d["WACC"] # Interest Rate and also the discount rate unless specified other wise - myopic = settings_d["Myopic"] == 1 # 1 if myopic (only one forward pass), 0 if full DDP - - # Define OPEXMULT here, include in inputs_dict[t] for use in dual_dynamic_programming.jl, transmission_multi_stage.jl, and investment_multi_stage.jl - OPEXMULT = myopic ? 1 : sum([1/(1+wacc)^(i-1) for i in range(1,stop=stage_len)]) - inputs_d["OPEXMULT"] = OPEXMULT - - if !myopic ### Leave myopic costs in annualized form and do not scale OPEX costs - # 1. Convert annualized investment costs incured within the model horizon into overnight capital costs - # NOTE: Although the "yr" suffix is still in use in these parameter names, they no longer represent annualized costs but rather truncated overnight capital costs - gen.inv_cost_per_mwyr = compute_overnight_capital_cost(settings_d, inv_cost_per_mwyr.(gen), capital_recovery_period.(gen), tech_wacc.(gen)) - gen.inv_cost_per_mwhyr = compute_overnight_capital_cost(settings_d, inv_cost_per_mwhyr.(gen), capital_recovery_period.(gen), tech_wacc.(gen)) - gen.inv_cost_charge_per_mwyr = compute_overnight_capital_cost(settings_d, inv_cost_charge_per_mwyr.(gen), capital_recovery_period.(gen), tech_wacc.(gen)) - - # 2. Update fixed O&M costs to account for the possibility of more than 1 year between two model stages - # NOTE: Although the "yr" suffix is still in use in these parameter names, they now represent total costs incured in each stage, which may be multiple years - gen.fixed_om_cost_per_mwyr = fixed_om_cost_per_mwyr.(gen) .* OPEXMULT - gen.fixed_om_cost_per_mwhyr = fixed_om_cost_per_mwhyr.(gen) .* OPEXMULT - gen.fixed_om_cost_charge_per_mwyr = fixed_om_cost_charge_per_mwyr.(gen) .* OPEXMULT - - # Conduct 1. and 2. for any co-located VRE-STOR resources - if !isempty(inputs_d["VRE_STOR"]) - gen_VRE_STOR = gen.VreStorage - gen_VRE_STOR.inv_cost_inverter_per_mwyr = compute_overnight_capital_cost(settings_d, inv_cost_inverter_per_mwyr.(gen_VRE_STOR), capital_recovery_period_dc.(gen_VRE_STOR), tech_wacc_dc.(gen_VRE_STOR)) - gen_VRE_STOR.inv_cost_solar_per_mwyr = compute_overnight_capital_cost(settings_d, inv_cost_solar_per_mwyr.(gen_VRE_STOR), capital_recovery_period_solar.(gen_VRE_STOR), tech_wacc_solar.(gen_VRE_STOR)) - gen_VRE_STOR.inv_cost_wind_per_mwyr = compute_overnight_capital_cost(settings_d, inv_cost_wind_per_mwyr.(gen_VRE_STOR), capital_recovery_period_wind.(gen_VRE_STOR), tech_wacc_wind.(gen_VRE_STOR)) - gen_VRE_STOR.inv_cost_discharge_dc_per_mwyr = compute_overnight_capital_cost(settings_d, inv_cost_discharge_dc_per_mwyr.(gen_VRE_STOR), capital_recovery_period_discharge_dc.(gen_VRE_STOR), tech_wacc_discharge_dc.(gen_VRE_STOR)) - gen_VRE_STOR.inv_cost_charge_dc_per_mwyr = compute_overnight_capital_cost(settings_d, inv_cost_charge_dc_per_mwyr.(gen_VRE_STOR), capital_recovery_period_charge_dc.(gen_VRE_STOR), tech_wacc_charge_dc.(gen_VRE_STOR)) - gen_VRE_STOR.inv_cost_discharge_ac_per_mwyr = compute_overnight_capital_cost(settings_d, inv_cost_discharge_ac_per_mwyr.(gen_VRE_STOR), capital_recovery_period_discharge_ac.(gen_VRE_STOR), tech_wacc_discharge_ac.(gen_VRE_STOR)) - gen_VRE_STOR.inv_cost_charge_ac_per_mwyr = compute_overnight_capital_cost(settings_d, inv_cost_charge_ac_per_mwyr.(gen_VRE_STOR), capital_recovery_period_charge_ac.(gen_VRE_STOR), tech_wacc_charge_ac.(gen_VRE_STOR)) - - gen_VRE_STOR.fixed_om_inverter_cost_per_mwyr = fixed_om_inverter_cost_per_mwyr.(gen_VRE_STOR) .* OPEXMULT - gen_VRE_STOR.fixed_om_solar_cost_per_mwyr = fixed_om_solar_cost_per_mwyr.(gen_VRE_STOR) .* OPEXMULT - gen_VRE_STOR.fixed_om_wind_cost_per_mwyr = fixed_om_wind_cost_per_mwyr.(gen_VRE_STOR) .* OPEXMULT - gen_VRE_STOR.fixed_om_cost_discharge_dc_per_mwyr = fixed_om_cost_discharge_dc_per_mwyr.(gen_VRE_STOR) .* OPEXMULT - gen_VRE_STOR.fixed_om_cost_charge_dc_per_mwyr = fixed_om_cost_charge_dc_per_mwyr.(gen_VRE_STOR) .* OPEXMULT - gen_VRE_STOR.fixed_om_cost_discharge_ac_per_mwyr = fixed_om_cost_discharge_ac_per_mwyr.(gen_VRE_STOR) .* OPEXMULT - gen_VRE_STOR.fixed_om_cost_charge_ac_per_mwyr = fixed_om_cost_charge_ac_per_mwyr.(gen_VRE_STOR) .* OPEXMULT - end - end + # Parameter inputs when multi-year discounting is activated + cur_stage = settings_d["CurStage"] + stage_len = settings_d["StageLengths"][cur_stage] + wacc = settings_d["WACC"] # Interest Rate and also the discount rate unless specified other wise + myopic = settings_d["Myopic"] == 1 # 1 if myopic (only one forward pass), 0 if full DDP + + # Define OPEXMULT here, include in inputs_dict[t] for use in dual_dynamic_programming.jl, transmission_multi_stage.jl, and investment_multi_stage.jl + OPEXMULT = myopic ? 1 : + sum([1 / (1 + wacc)^(i - 1) for i in range(1, stop = stage_len)]) + inputs_d["OPEXMULT"] = OPEXMULT + + if !myopic ### Leave myopic costs in annualized form and do not scale OPEX costs + # 1. Convert annualized investment costs incured within the model horizon into overnight capital costs + # NOTE: Although the "yr" suffix is still in use in these parameter names, they no longer represent annualized costs but rather truncated overnight capital costs + gen.inv_cost_per_mwyr = compute_overnight_capital_cost(settings_d, + inv_cost_per_mwyr.(gen), + capital_recovery_period.(gen), + tech_wacc.(gen)) + gen.inv_cost_per_mwhyr = compute_overnight_capital_cost(settings_d, + inv_cost_per_mwhyr.(gen), + capital_recovery_period.(gen), + tech_wacc.(gen)) + gen.inv_cost_charge_per_mwyr = compute_overnight_capital_cost(settings_d, + inv_cost_charge_per_mwyr.(gen), + capital_recovery_period.(gen), + tech_wacc.(gen)) + + # 2. Update fixed O&M costs to account for the possibility of more than 1 year between two model stages + # NOTE: Although the "yr" suffix is still in use in these parameter names, they now represent total costs incured in each stage, which may be multiple years + gen.fixed_om_cost_per_mwyr = fixed_om_cost_per_mwyr.(gen) .* OPEXMULT + gen.fixed_om_cost_per_mwhyr = fixed_om_cost_per_mwhyr.(gen) .* OPEXMULT + gen.fixed_om_cost_charge_per_mwyr = fixed_om_cost_charge_per_mwyr.(gen) .* OPEXMULT + + # Conduct 1. and 2. for any co-located VRE-STOR resources + if !isempty(inputs_d["VRE_STOR"]) + gen_VRE_STOR = gen.VreStorage + gen_VRE_STOR.inv_cost_inverter_per_mwyr = compute_overnight_capital_cost(settings_d, + inv_cost_inverter_per_mwyr.(gen_VRE_STOR), + capital_recovery_period_dc.(gen_VRE_STOR), + tech_wacc_dc.(gen_VRE_STOR)) + gen_VRE_STOR.inv_cost_solar_per_mwyr = compute_overnight_capital_cost(settings_d, + inv_cost_solar_per_mwyr.(gen_VRE_STOR), + capital_recovery_period_solar.(gen_VRE_STOR), + tech_wacc_solar.(gen_VRE_STOR)) + gen_VRE_STOR.inv_cost_wind_per_mwyr = compute_overnight_capital_cost(settings_d, + inv_cost_wind_per_mwyr.(gen_VRE_STOR), + capital_recovery_period_wind.(gen_VRE_STOR), + tech_wacc_wind.(gen_VRE_STOR)) + gen_VRE_STOR.inv_cost_discharge_dc_per_mwyr = compute_overnight_capital_cost(settings_d, + inv_cost_discharge_dc_per_mwyr.(gen_VRE_STOR), + capital_recovery_period_discharge_dc.(gen_VRE_STOR), + tech_wacc_discharge_dc.(gen_VRE_STOR)) + gen_VRE_STOR.inv_cost_charge_dc_per_mwyr = compute_overnight_capital_cost(settings_d, + inv_cost_charge_dc_per_mwyr.(gen_VRE_STOR), + capital_recovery_period_charge_dc.(gen_VRE_STOR), + tech_wacc_charge_dc.(gen_VRE_STOR)) + gen_VRE_STOR.inv_cost_discharge_ac_per_mwyr = compute_overnight_capital_cost(settings_d, + inv_cost_discharge_ac_per_mwyr.(gen_VRE_STOR), + capital_recovery_period_discharge_ac.(gen_VRE_STOR), + tech_wacc_discharge_ac.(gen_VRE_STOR)) + gen_VRE_STOR.inv_cost_charge_ac_per_mwyr = compute_overnight_capital_cost(settings_d, + inv_cost_charge_ac_per_mwyr.(gen_VRE_STOR), + capital_recovery_period_charge_ac.(gen_VRE_STOR), + tech_wacc_charge_ac.(gen_VRE_STOR)) + + gen_VRE_STOR.fixed_om_inverter_cost_per_mwyr = fixed_om_inverter_cost_per_mwyr.(gen_VRE_STOR) .* + OPEXMULT + gen_VRE_STOR.fixed_om_solar_cost_per_mwyr = fixed_om_solar_cost_per_mwyr.(gen_VRE_STOR) .* + OPEXMULT + gen_VRE_STOR.fixed_om_wind_cost_per_mwyr = fixed_om_wind_cost_per_mwyr.(gen_VRE_STOR) .* + OPEXMULT + gen_VRE_STOR.fixed_om_cost_discharge_dc_per_mwyr = fixed_om_cost_discharge_dc_per_mwyr.(gen_VRE_STOR) .* + OPEXMULT + gen_VRE_STOR.fixed_om_cost_charge_dc_per_mwyr = fixed_om_cost_charge_dc_per_mwyr.(gen_VRE_STOR) .* + OPEXMULT + gen_VRE_STOR.fixed_om_cost_discharge_ac_per_mwyr = fixed_om_cost_discharge_ac_per_mwyr.(gen_VRE_STOR) .* + OPEXMULT + gen_VRE_STOR.fixed_om_cost_charge_ac_per_mwyr = fixed_om_cost_charge_ac_per_mwyr.(gen_VRE_STOR) .* + OPEXMULT + end + end retirable = is_retirable(gen) - # TODO: ask Sam about this + # TODO: ask Sam about this # Set of all resources eligible for capacity retirements - inputs_d["RET_CAP"] = retirable - # Set of all storage resources eligible for energy capacity retirements - inputs_d["RET_CAP_ENERGY"] = intersect(retirable, inputs_d["STOR_ALL"]) - # Set of asymmetric charge/discharge storage resources eligible for charge capacity retirements - inputs_d["RET_CAP_CHARGE"] = intersect(retirable, inputs_d["STOR_ASYMMETRIC"]) - # Set of all co-located resources' components eligible for capacity retirements - if !isempty(inputs_d["VRE_STOR"]) - inputs_d["RET_CAP_DC"] = intersect(retirable, inputs_d["VS_DC"]) - inputs_d["RET_CAP_SOLAR"] = intersect(retirable, inputs_d["VS_SOLAR"]) - inputs_d["RET_CAP_WIND"] = intersect(retirable, inputs_d["VS_WIND"]) - inputs_d["RET_CAP_STOR"] = intersect(retirable, inputs_d["VS_STOR"]) - inputs_d["RET_CAP_DISCHARGE_DC"] = intersect(retirable, inputs_d["VS_ASYM_DC_DISCHARGE"]) - inputs_d["RET_CAP_CHARGE_DC"] = intersect(retirable, inputs_d["VS_ASYM_DC_CHARGE"]) - inputs_d["RET_CAP_DISCHARGE_AC"] = intersect(retirable, inputs_d["VS_ASYM_AC_DISCHARGE"]) - inputs_d["RET_CAP_CHARGE_AC"] = intersect(retirable, inputs_d["VS_ASYM_AC_CHARGE"]) - end - - # Transmission - if NetworkExpansion == 1 && inputs_d["Z"] > 1 - - if !myopic ### Leave myopic costs in annualized form - # 1. Convert annualized tramsmission investment costs incured within the model horizon into overnight capital costs - inputs_d["pC_Line_Reinforcement"] = compute_overnight_capital_cost(settings_d,inputs_d["pC_Line_Reinforcement"],inputs_d["Capital_Recovery_Period_Trans"],inputs_d["transmission_WACC"]) - end - - # Scale max_allowed_reinforcement to allow for possibility of deploying maximum reinforcement in each investment stage - inputs_d["pTrans_Max_Possible"] = inputs_d["pLine_Max_Flow_Possible_MW"] + inputs_d["RET_CAP"] = retirable + # Set of all storage resources eligible for energy capacity retirements + inputs_d["RET_CAP_ENERGY"] = intersect(retirable, inputs_d["STOR_ALL"]) + # Set of asymmetric charge/discharge storage resources eligible for charge capacity retirements + inputs_d["RET_CAP_CHARGE"] = intersect(retirable, inputs_d["STOR_ASYMMETRIC"]) + # Set of all co-located resources' components eligible for capacity retirements + if !isempty(inputs_d["VRE_STOR"]) + inputs_d["RET_CAP_DC"] = intersect(retirable, inputs_d["VS_DC"]) + inputs_d["RET_CAP_SOLAR"] = intersect(retirable, inputs_d["VS_SOLAR"]) + inputs_d["RET_CAP_WIND"] = intersect(retirable, inputs_d["VS_WIND"]) + inputs_d["RET_CAP_STOR"] = intersect(retirable, inputs_d["VS_STOR"]) + inputs_d["RET_CAP_DISCHARGE_DC"] = intersect(retirable, + inputs_d["VS_ASYM_DC_DISCHARGE"]) + inputs_d["RET_CAP_CHARGE_DC"] = intersect(retirable, inputs_d["VS_ASYM_DC_CHARGE"]) + inputs_d["RET_CAP_DISCHARGE_AC"] = intersect(retirable, + inputs_d["VS_ASYM_AC_DISCHARGE"]) + inputs_d["RET_CAP_CHARGE_AC"] = intersect(retirable, inputs_d["VS_ASYM_AC_CHARGE"]) + end + + # Transmission + if NetworkExpansion == 1 && inputs_d["Z"] > 1 + if !myopic ### Leave myopic costs in annualized form + # 1. Convert annualized tramsmission investment costs incured within the model horizon into overnight capital costs + inputs_d["pC_Line_Reinforcement"] = compute_overnight_capital_cost(settings_d, + inputs_d["pC_Line_Reinforcement"], + inputs_d["Capital_Recovery_Period_Trans"], + inputs_d["transmission_WACC"]) + end + + # Scale max_allowed_reinforcement to allow for possibility of deploying maximum reinforcement in each investment stage + inputs_d["pTrans_Max_Possible"] = inputs_d["pLine_Max_Flow_Possible_MW"] # Network lines and zones that are expandable have greater maximum possible line flow than the available capacity of the previous stage as well as available line reinforcement - inputs_d["EXPANSION_LINES"] = findall((inputs_d["pLine_Max_Flow_Possible_MW"] .> inputs_d["pTrans_Max"]) .& (inputs_d["pMax_Line_Reinforcement"] .> 0)) - inputs_d["NO_EXPANSION_LINES"] = findall((inputs_d["pLine_Max_Flow_Possible_MW"] .<= inputs_d["pTrans_Max"]) .| (inputs_d["pMax_Line_Reinforcement"] .<= 0)) - # To-Do: Error Handling - # 1.) Enforce that pLine_Max_Flow_Possible_MW for the first model stage be equal to (for transmission expansion to be disalowed) or greater (to allow transmission expansion) than pTrans_Max in inputs/inputs_p1 + inputs_d["EXPANSION_LINES"] = findall((inputs_d["pLine_Max_Flow_Possible_MW"] .> + inputs_d["pTrans_Max"]) .& + (inputs_d["pMax_Line_Reinforcement"] .> 0)) + inputs_d["NO_EXPANSION_LINES"] = findall((inputs_d["pLine_Max_Flow_Possible_MW"] .<= + inputs_d["pTrans_Max"]) .| + (inputs_d["pMax_Line_Reinforcement"] .<= + 0)) + # To-Do: Error Handling + # 1.) Enforce that pLine_Max_Flow_Possible_MW for the first model stage be equal to (for transmission expansion to be disalowed) or greater (to allow transmission expansion) than pTrans_Max in inputs/inputs_p1 end return inputs_d diff --git a/src/multi_stage/dual_dynamic_programming.jl b/src/multi_stage/dual_dynamic_programming.jl index bb9f14df68..9703784473 100644 --- a/src/multi_stage/dual_dynamic_programming.jl +++ b/src/multi_stage/dual_dynamic_programming.jl @@ -132,7 +132,6 @@ returns: * inputs\_d – Dictionary of inputs for each model stage, generated by the load\_inputs() method, modified by this method. """ function run_ddp(models_d::Dict, setup::Dict, inputs_d::Dict) - settings_d = setup["MultiStageSettingsDict"] num_stages = settings_d["NumStages"] # Total number of investment planning stages EPSILON = settings_d["ConvergenceTolerance"] # Tolerance @@ -150,7 +149,7 @@ function run_ddp(models_d::Dict, setup::Dict, inputs_d::Dict) # Step a.i) Initialize cost-to-go function for t = 1:num_stages for t in 1:num_stages - settings_d["CurStage"] = t; + settings_d["CurStage"] = t models_d[t] = initialize_cost_to_go(settings_d, models_d[t], inputs_d[t]) end @@ -162,7 +161,6 @@ function run_ddp(models_d::Dict, setup::Dict, inputs_d::Dict) println("Solving First Stage Problem") println("***********") - t = 1 # Stage = 1 solve_time_d = Dict() ddp_prev_time = time() # Begin tracking time of each iteration @@ -174,7 +172,6 @@ function run_ddp(models_d::Dict, setup::Dict, inputs_d::Dict) # Step c.ii) If the relative difference between upper and lower bounds are small, break loop while ((z_upper - z_lower) / z_lower > EPSILON) - ic = ic + 1 # Increase iteration counter by 1 if (ic > 10000) @@ -207,21 +204,25 @@ function run_ddp(models_d::Dict, setup::Dict, inputs_d::Dict) end ## Forward pass for t=2:num_stages for t in 2:num_stages - println("***********") println(string("Forward Pass t = ", t)) println("***********") # Step d.i) Fix initial investments for model at time t given optimal solution for time t-1 - models_d[t] = fix_initial_investments(models_d[t-1], models_d[t], start_cap_d, inputs_d[t]) + models_d[t] = fix_initial_investments(models_d[t - 1], + models_d[t], + start_cap_d, + inputs_d[t]) # Step d.ii) Fix capacity tracking variables for endogenous retirements - models_d[t] = fix_capacity_tracking(models_d[t-1], models_d[t], cap_track_d, t) + models_d[t] = fix_capacity_tracking(models_d[t - 1], + models_d[t], + cap_track_d, + t) # Step d.iii) Solve the model at time t models_d[t], solve_time_d[t] = solve_model(models_d[t], setup) inputs_d[t]["solve_time"] = solve_time_d[t] - end ### For the myopic solution, algorithm should terminate here after the first forward pass calculation and then move to Outputs writing. @@ -242,7 +243,8 @@ function run_ddp(models_d::Dict, setup::Dict, inputs_d::Dict) # Step e) Calculate the new upper bound z_upper_temp = 0 for t in 1:num_stages - z_upper_temp = z_upper_temp + (objective_value(models_d[t]) - value(models_d[t][:vALPHA])) + z_upper_temp = z_upper_temp + + (objective_value(models_d[t]) - value(models_d[t][:vALPHA])) end # If the upper bound decreased, set it as the new upper bound @@ -254,17 +256,19 @@ function run_ddp(models_d::Dict, setup::Dict, inputs_d::Dict) # Step f) Backward pass for t = num_stages:2 for t in num_stages:-1:2 - println("***********") println(string("Backward Pass t = ", t)) println("***********") # Step f.i) Add a cut to the previous time step using information from the current time step - models_d[t-1] = add_cut(models_d[t-1], models_d[t], start_cap_d, cap_track_d) + models_d[t - 1] = add_cut(models_d[t - 1], + models_d[t], + start_cap_d, + cap_track_d) # Step f.ii) Solve the model with the additional cut at time t-1 - models_d[t-1], solve_time_d[t-1] = solve_model(models_d[t-1], setup) - inputs_d[t-1]["solve_time"] = solve_time_d[t-1] + models_d[t - 1], solve_time_d[t - 1] = solve_model(models_d[t - 1], setup) + inputs_d[t - 1]["solve_time"] = solve_time_d[t - 1] end # Step g) Recalculate lower bound and go back to c) @@ -283,7 +287,6 @@ function run_ddp(models_d::Dict, setup::Dict, inputs_d::Dict) println(string("Lower Bound = ", z_lower)) println("***********") - ### STEP I) One final forward pass to guarantee convergence # Forward pass for t = 1:num_stages t = 1 # update forward pass solution for the first stage @@ -296,10 +299,13 @@ function run_ddp(models_d::Dict, setup::Dict, inputs_d::Dict) println("***********") # Step d.i) Fix initial investments for model at time t given optimal solution for time t-1 - models_d[t] = fix_initial_investments(models_d[t-1], models_d[t], start_cap_d, inputs_d[t]) + models_d[t] = fix_initial_investments(models_d[t - 1], + models_d[t], + start_cap_d, + inputs_d[t]) # Step d.ii) Fix capacity tracking variables for endogenous retirements - models_d[t] = fix_capacity_tracking(models_d[t-1], models_d[t], cap_track_d, t) + models_d[t] = fix_capacity_tracking(models_d[t - 1], models_d[t], cap_track_d, t) # Step d.iii) Solve the model at time t models_d[t], solve_time_d[t] = solve_model(models_d[t], setup) @@ -325,20 +331,21 @@ inputs: * outpath – String which represents the path to the Results directory. * settings\_d - Dictionary containing settings configured in the GenX settings genx\_settings.yml file as well as the multi-stage settings file multi\_stage\_settings.yml. """ -function write_multi_stage_outputs(stats_d::Dict, outpath::String, settings_d::Dict, inputs_dict::Dict) - +function write_multi_stage_outputs(stats_d::Dict, + outpath::String, + settings_d::Dict, + inputs_dict::Dict) multi_stage_settings_d = settings_d["MultiStageSettingsDict"] write_multi_stage_capacities_discharge(outpath, multi_stage_settings_d) write_multi_stage_capacities_charge(outpath, multi_stage_settings_d) write_multi_stage_capacities_energy(outpath, multi_stage_settings_d) if settings_d["NetworkExpansion"] == 1 - write_multi_stage_network_expansion(outpath, multi_stage_settings_d) + write_multi_stage_network_expansion(outpath, multi_stage_settings_d) end write_multi_stage_costs(outpath, multi_stage_settings_d, inputs_dict) multi_stage_settings_d["Myopic"] == 0 && write_multi_stage_stats(outpath, stats_d) write_multi_stage_settings(outpath, settings_d) - end @doc raw""" @@ -354,22 +361,24 @@ inputs: returns: JuMP model with updated linking constraints. """ -function fix_initial_investments(EP_prev::Model, EP_cur::Model, start_cap_d::Dict, inputs_d::Dict) - - ALL_CAP = union(inputs_d["RET_CAP"],inputs_d["NEW_CAP"]) # Set of all resources subject to inter-stage capacity tracking - +function fix_initial_investments(EP_prev::Model, + EP_cur::Model, + start_cap_d::Dict, + inputs_d::Dict) + ALL_CAP = union(inputs_d["RET_CAP"], inputs_d["NEW_CAP"]) # Set of all resources subject to inter-stage capacity tracking + # start_cap_d dictionary contains the starting capacity expression name (e) as a key, # and the associated linking constraint name (c) as a value for (e, c) in start_cap_d for y in keys(EP_cur[c]) - # Set the right hand side value of the linking initial capacity constraint in the current stage to the value of the available capacity variable solved for in the previous stages - if c == :cExistingTransCap + # Set the right hand side value of the linking initial capacity constraint in the current stage to the value of the available capacity variable solved for in the previous stages + if c == :cExistingTransCap + set_normalized_rhs(EP_cur[c][y], value(EP_prev[e][y])) + else + if y[1] in ALL_CAP # extract resource integer index value from key set_normalized_rhs(EP_cur[c][y], value(EP_prev[e][y])) - else - if y[1] in ALL_CAP # extract resource integer index value from key - set_normalized_rhs(EP_cur[c][y], value(EP_prev[e][y])) - end end + end end end return EP_cur @@ -391,7 +400,10 @@ inputs: returns: JuMP model with updated linking constraints. """ -function fix_capacity_tracking(EP_prev::Model, EP_cur::Model, cap_track_d::Dict, cur_stage::Int) +function fix_capacity_tracking(EP_prev::Model, + EP_cur::Model, + cap_track_d::Dict, + cur_stage::Int) # cap_track_d dictionary contains the endogenous retirement tracking array variable name (v) as a key, # and the associated linking constraint name (c) as a value @@ -407,7 +419,7 @@ function fix_capacity_tracking(EP_prev::Model, EP_cur::Model, cap_track_d::Dict, # For all previous stages, set the right hand side value of the tracking constraint in the current # stage to the value of the tracking constraint observed in the previous stage - for p in 1:(cur_stage-1) + for p in 1:(cur_stage - 1) # Tracking newly buily capacity over all previous stages JuMP.set_normalized_rhs(EP_cur[c][i, p], value(EP_prev[v][i, p])) # Tracking retired capacity over all previous stages @@ -432,7 +444,6 @@ inputs: returns: JuMP expression representing a sum of Benders cuts for linking capacity investment variables to be added to the cost-to-go function. """ function add_cut(EP_cur::Model, EP_next::Model, start_cap_d::Dict, cap_track_d::Dict) - next_obj_value = objective_value(EP_next) # Get the objective function value for the next investment planning stage eRHS = @expression(EP_cur, 0) # Initialize RHS of cut to 0 @@ -480,7 +491,7 @@ function add_cut(EP_cur::Model, EP_next::Model, start_cap_d::Dict, cap_track_d:: end # Add the cut to the model - @constraint(EP_cur, EP_cur[:vALPHA] >= next_obj_value - eRHS) + @constraint(EP_cur, EP_cur[:vALPHA]>=next_obj_value - eRHS) return EP_cur end @@ -505,8 +516,10 @@ inputs: returns: JuMP expression representing a sum of Benders cuts for linking capacity investment variables to be added to the cost-to-go function. """ -function generate_cut_component_track(EP_cur::Model, EP_next::Model, var_name::Symbol, constr_name::Symbol) - +function generate_cut_component_track(EP_cur::Model, + EP_next::Model, + var_name::Symbol, + constr_name::Symbol) next_dual_value = Float64[] cur_inv_value = Float64[] cur_inv_var = [] @@ -520,7 +533,8 @@ function generate_cut_component_track(EP_cur::Model, EP_next::Model, var_name::S push!(cur_inv_var, EP_cur[var_name][y, p]) end - eCutComponent = @expression(EP_cur, dot(next_dual_value, (cur_inv_value .- cur_inv_var))) + eCutComponent = @expression(EP_cur, + dot(next_dual_value, (cur_inv_value .- cur_inv_var))) return eCutComponent end @@ -545,20 +559,22 @@ inputs: returns: JuMP expression representing a sum of Benders cuts for linking capacity investment variables to be added to the cost-to-go function. """ -function generate_cut_component_inv(EP_cur::Model, EP_next::Model, expr_name::Symbol, constr_name::Symbol) - +function generate_cut_component_inv(EP_cur::Model, + EP_next::Model, + expr_name::Symbol, + constr_name::Symbol) next_dual_value = Float64[] cur_inv_value = Float64[] cur_inv_var = [] for y in keys(EP_next[constr_name]) - push!(next_dual_value, dual(EP_next[constr_name][y])) push!(cur_inv_value, value(EP_cur[expr_name][y])) push!(cur_inv_var, EP_cur[expr_name][y]) end - eCutComponent = @expression(EP_cur, dot(next_dual_value, (cur_inv_value .- cur_inv_var))) + eCutComponent = @expression(EP_cur, + dot(next_dual_value, (cur_inv_value .- cur_inv_var))) return eCutComponent end @@ -600,7 +616,6 @@ inputs: returns: JuMP model with updated objective function. """ function initialize_cost_to_go(settings_d::Dict, EP::Model, inputs::Dict) - cur_stage = settings_d["CurStage"] # Current DDP Investment Planning Stage stage_len = settings_d["StageLengths"][cur_stage] wacc = settings_d["WACC"] # Interest Rate and also the discount rate unless specified other wise @@ -616,10 +631,9 @@ function initialize_cost_to_go(settings_d::Dict, EP::Model, inputs::Dict) else DF = 1 / (1 + wacc)^(stage_len * (cur_stage - 1)) # Discount factor applied all to costs in each stage ### # Initialize the cost-to-go variable - @variable(EP, vALPHA >= 0) - @objective(EP, Min, DF * OPEXMULT * EP[:eObj] + vALPHA) + @variable(EP, vALPHA>=0) + @objective(EP, Min, DF * OPEXMULT * EP[:eObj]+vALPHA) end return EP - end diff --git a/src/multi_stage/endogenous_retirement.jl b/src/multi_stage/endogenous_retirement.jl index fca0ebb0bf..b88ac93e1d 100644 --- a/src/multi_stage/endogenous_retirement.jl +++ b/src/multi_stage/endogenous_retirement.jl @@ -12,113 +12,143 @@ inputs: returns: An Int representing the model stage in before which the resource must retire due to endogenous lifetime retirements. """ function get_retirement_stage(cur_stage::Int, lifetime::Int, stage_lens::Array{Int, 1}) - years_from_start = sum(stage_lens[1:cur_stage]) # Years from start from the END of the current stage - ret_years = years_from_start - lifetime # Difference between end of current stage and technology lifetime - ret_stage = 0 # Compute the stage before which all newly built capacity must be retired by the end of the current stage - while (ret_years - stage_lens[ret_stage+1] >= 0) & (ret_stage < cur_stage) - ret_stage += 1 - ret_years -= stage_lens[ret_stage] - end + years_from_start = sum(stage_lens[1:cur_stage]) # Years from start from the END of the current stage + ret_years = years_from_start - lifetime # Difference between end of current stage and technology lifetime + ret_stage = 0 # Compute the stage before which all newly built capacity must be retired by the end of the current stage + while (ret_years - stage_lens[ret_stage + 1] >= 0) & (ret_stage < cur_stage) + ret_stage += 1 + ret_years -= stage_lens[ret_stage] + end return Int(ret_stage) end -function update_cumulative_min_ret!(inputs_d::Dict,t::Int,Resource_Set::String,RetCap::Symbol) - - gen_name = "RESOURCES" - CumRetCap = Symbol("cum_"*String(RetCap)) - # if the getter function exists in GenX then use it, otherwise get the attribute directly - ret_cap_f = isdefined(GenX, RetCap) ? getfield(GenX, RetCap) : r -> getproperty(r, RetCap) - cum_ret_cap_f = isdefined(GenX, CumRetCap) ? getfield(GenX, CumRetCap) : r -> getproperty(r, CumRetCap) - if !isempty(inputs_d[1][Resource_Set]) - gen_t = inputs_d[t][gen_name] - if t==1 - gen_t[CumRetCap] = ret_cap_f.(gen_t) - else - gen_t[CumRetCap] = cum_ret_cap_f.(inputs_d[t-1][gen_name]) + ret_cap_f.(gen_t) - end - end +function update_cumulative_min_ret!(inputs_d::Dict, + t::Int, + Resource_Set::String, + RetCap::Symbol) + gen_name = "RESOURCES" + CumRetCap = Symbol("cum_" * String(RetCap)) + # if the getter function exists in GenX then use it, otherwise get the attribute directly + ret_cap_f = isdefined(GenX, RetCap) ? getfield(GenX, RetCap) : + r -> getproperty(r, RetCap) + cum_ret_cap_f = isdefined(GenX, CumRetCap) ? getfield(GenX, CumRetCap) : + r -> getproperty(r, CumRetCap) + if !isempty(inputs_d[1][Resource_Set]) + gen_t = inputs_d[t][gen_name] + if t == 1 + gen_t[CumRetCap] = ret_cap_f.(gen_t) + else + gen_t[CumRetCap] = cum_ret_cap_f.(inputs_d[t - 1][gen_name]) + ret_cap_f.(gen_t) + end + end end - -function compute_cumulative_min_retirements!(inputs_d::Dict,t::Int) - - mytab =[("G", :min_retired_cap_mw), - ("STOR_ALL", :min_retired_energy_cap_mw), - ("STOR_ASYMMETRIC", :min_retired_charge_cap_mw)]; - - if !isempty(inputs_d[1]["VRE_STOR"]) - append!(mytab,[("VS_STOR", :min_retired_energy_cap_mw), - ("VS_DC", :min_retired_cap_inverter_mw), - ("VS_SOLAR", :min_retired_cap_solar_mw), - ("VS_WIND", :min_retired_cap_wind_mw), - ("VS_ASYM_DC_DISCHARGE", :min_retired_cap_discharge_dc_mw), - ("VS_ASYM_DC_CHARGE", :min_retired_cap_charge_dc_mw), - ("VS_ASYM_AC_DISCHARGE", :min_retired_cap_discharge_ac_mw), - ("VS_ASYM_AC_CHARGE", :min_retired_cap_charge_ac_mw)]) - - end - - for (Resource_Set,RetCap) in mytab - update_cumulative_min_ret!(inputs_d,t,Resource_Set,RetCap) - end - - +function compute_cumulative_min_retirements!(inputs_d::Dict, t::Int) + mytab = [("G", :min_retired_cap_mw), + ("STOR_ALL", :min_retired_energy_cap_mw), + ("STOR_ASYMMETRIC", :min_retired_charge_cap_mw)] + + if !isempty(inputs_d[1]["VRE_STOR"]) + append!(mytab, + [("VS_STOR", :min_retired_energy_cap_mw), + ("VS_DC", :min_retired_cap_inverter_mw), + ("VS_SOLAR", :min_retired_cap_solar_mw), + ("VS_WIND", :min_retired_cap_wind_mw), + ("VS_ASYM_DC_DISCHARGE", :min_retired_cap_discharge_dc_mw), + ("VS_ASYM_DC_CHARGE", :min_retired_cap_charge_dc_mw), + ("VS_ASYM_AC_DISCHARGE", :min_retired_cap_discharge_ac_mw), + ("VS_ASYM_AC_CHARGE", :min_retired_cap_charge_ac_mw)]) + end + + for (Resource_Set, RetCap) in mytab + update_cumulative_min_ret!(inputs_d, t, Resource_Set, RetCap) + end end - function endogenous_retirement!(EP::Model, inputs::Dict, setup::Dict) - multi_stage_settings = setup["MultiStageSettingsDict"] - - println("Endogenous Retirement Module") - - num_stages = multi_stage_settings["NumStages"] - cur_stage = multi_stage_settings["CurStage"] - stage_lens = multi_stage_settings["StageLengths"] - - endogenous_retirement_discharge!(EP, inputs, num_stages, cur_stage, stage_lens) - - if !isempty(inputs["STOR_ALL"]) - endogenous_retirement_energy!(EP, inputs, num_stages, cur_stage, stage_lens) - end - - if !isempty(inputs["STOR_ASYMMETRIC"]) - endogenous_retirement_charge!(EP, inputs, num_stages, cur_stage, stage_lens) - end - - if !isempty(inputs["VRE_STOR"]) - if !isempty(inputs["VS_DC"]) - endogenous_retirement_vre_stor_dc!(EP, inputs, num_stages, cur_stage, stage_lens) - end - - if !isempty(inputs["VS_SOLAR"]) - endogenous_retirement_vre_stor_solar!(EP, inputs, num_stages, cur_stage, stage_lens) - end - - if !isempty(inputs["VS_WIND"]) - endogenous_retirement_vre_stor_wind!(EP, inputs, num_stages, cur_stage, stage_lens) - end - - if !isempty(inputs["VS_STOR"]) - endogenous_retirement_vre_stor_stor!(EP, inputs, num_stages, cur_stage, stage_lens) - end - - if !isempty(inputs["VS_ASYM_DC_DISCHARGE"]) - endogenous_retirement_vre_stor_discharge_dc!(EP, inputs, num_stages, cur_stage, stage_lens) - end - - if !isempty(inputs["VS_ASYM_DC_CHARGE"]) - endogenous_retirement_vre_stor_charge_dc!(EP, inputs, num_stages, cur_stage, stage_lens) - end - - if !isempty(inputs["VS_ASYM_AC_DISCHARGE"]) - endogenous_retirement_vre_stor_discharge_ac!(EP, inputs, num_stages, cur_stage, stage_lens) - end - - if !isempty(inputs["VS_ASYM_AC_CHARGE"]) - endogenous_retirement_vre_stor_charge_ac!(EP, inputs, num_stages, cur_stage, stage_lens) - end - end - + multi_stage_settings = setup["MultiStageSettingsDict"] + + println("Endogenous Retirement Module") + + num_stages = multi_stage_settings["NumStages"] + cur_stage = multi_stage_settings["CurStage"] + stage_lens = multi_stage_settings["StageLengths"] + + endogenous_retirement_discharge!(EP, inputs, num_stages, cur_stage, stage_lens) + + if !isempty(inputs["STOR_ALL"]) + endogenous_retirement_energy!(EP, inputs, num_stages, cur_stage, stage_lens) + end + + if !isempty(inputs["STOR_ASYMMETRIC"]) + endogenous_retirement_charge!(EP, inputs, num_stages, cur_stage, stage_lens) + end + + if !isempty(inputs["VRE_STOR"]) + if !isempty(inputs["VS_DC"]) + endogenous_retirement_vre_stor_dc!(EP, + inputs, + num_stages, + cur_stage, + stage_lens) + end + + if !isempty(inputs["VS_SOLAR"]) + endogenous_retirement_vre_stor_solar!(EP, + inputs, + num_stages, + cur_stage, + stage_lens) + end + + if !isempty(inputs["VS_WIND"]) + endogenous_retirement_vre_stor_wind!(EP, + inputs, + num_stages, + cur_stage, + stage_lens) + end + + if !isempty(inputs["VS_STOR"]) + endogenous_retirement_vre_stor_stor!(EP, + inputs, + num_stages, + cur_stage, + stage_lens) + end + + if !isempty(inputs["VS_ASYM_DC_DISCHARGE"]) + endogenous_retirement_vre_stor_discharge_dc!(EP, + inputs, + num_stages, + cur_stage, + stage_lens) + end + + if !isempty(inputs["VS_ASYM_DC_CHARGE"]) + endogenous_retirement_vre_stor_charge_dc!(EP, + inputs, + num_stages, + cur_stage, + stage_lens) + end + + if !isempty(inputs["VS_ASYM_AC_DISCHARGE"]) + endogenous_retirement_vre_stor_discharge_ac!(EP, + inputs, + num_stages, + cur_stage, + stage_lens) + end + + if !isempty(inputs["VS_ASYM_AC_CHARGE"]) + endogenous_retirement_vre_stor_charge_ac!(EP, + inputs, + num_stages, + cur_stage, + stage_lens) + end + end end @doc raw""" @@ -139,547 +169,753 @@ In other words, it is the largest index $r \in \{1, ..., (p-1)\}$ such that: \end{aligned} ``` """ -function endogenous_retirement_discharge!(EP::Model, inputs::Dict, num_stages::Int, cur_stage::Int, stage_lens::Array{Int, 1}) - - println("Endogenous Retirement (Discharge) Module") - - gen = inputs["RESOURCES"] - - NEW_CAP = inputs["NEW_CAP"] # Set of all resources eligible for new capacity - RET_CAP = inputs["RET_CAP"] # Set of all resources eligible for capacity retirements - COMMIT = inputs["COMMIT"] # Set of all resources eligible for unit commitment - - ### Variables ### - - # Keep track of all new and retired capacity from all stages - @variable(EP, vCAPTRACK[y in RET_CAP,p=1:num_stages] >= 0 ) - @variable(EP, vRETCAPTRACK[y in RET_CAP,p=1:num_stages] >= 0 ) - - ### Expressions ### - - @expression(EP, eNewCap[y in RET_CAP], - if y in NEW_CAP - EP[:vCAP][y] - else - EP[:vZERO] - end - ) - - @expression(EP, eRetCap[y in RET_CAP], - if y in ids_with_all_options_contributing(gen) - EP[:vRETCAP][y] + EP[:vRETROFITCAP][y] - else - EP[:vRETCAP][y] - end - ) - - # Construct and add the endogenous retirement constraint expressions - @expression(EP, eRetCapTrack[y in RET_CAP], sum(EP[:vRETCAPTRACK][y,p] for p=1:cur_stage)) - @expression(EP, eNewCapTrack[y in RET_CAP], sum(EP[:vCAPTRACK][y,p] for p=1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) - @expression(EP, eMinRetCapTrack[y in RET_CAP], - if y in COMMIT - cum_min_retired_cap_mw(gen[y])/cap_size(gen[y]) - else - cum_min_retired_cap_mw(gen[y]) - end - ) - - ### Constraints ### - - # Keep track of newly built capacity from previous stages - @constraint(EP, cCapTrackNew[y in RET_CAP], eNewCap[y] == vCAPTRACK[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cCapTrack[y in RET_CAP,p=1:(cur_stage-1)], vCAPTRACK[y,p] == 0) - - # Keep track of retired capacity from previous stages - @constraint(EP, cRetCapTrackNew[y in RET_CAP], eRetCap[y] == vRETCAPTRACK[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cRetCapTrack[y in RET_CAP,p=1:(cur_stage-1)], vRETCAPTRACK[y,p] == 0) - - # Create a slack variable for each resource that is not contributing to the retired capacity being tracked - # This ensures that the model is able to satisfy the minimum retirement constraint - RETROFIT_WITH_SLACK = ids_with_all_options_not_contributing(gen) - if !isempty(RETROFIT_WITH_SLACK) - @variable(EP, vslack_lifetime[y in RETROFIT_WITH_SLACK] >=0) - @expression(EP, vslack_term, 2*maximum(inv_cost_per_mwyr.(gen))*sum(vslack_lifetime[y] for y in RETROFIT_WITH_SLACK; init=0)) - add_to_expression!(EP[:eObj], vslack_term) - end - - @expression(EP,eLifetimeRetRHS[y in RET_CAP], - if y in RETROFIT_WITH_SLACK - eRetCapTrack[y] + vslack_lifetime[y] - else - eRetCapTrack[y] - end - ) - - @constraint(EP, cLifetimeRet[y in RET_CAP], eNewCapTrack[y] + eMinRetCapTrack[y] <= eLifetimeRetRHS[y]) +function endogenous_retirement_discharge!(EP::Model, + inputs::Dict, + num_stages::Int, + cur_stage::Int, + stage_lens::Array{Int, 1}) + println("Endogenous Retirement (Discharge) Module") + + gen = inputs["RESOURCES"] + + NEW_CAP = inputs["NEW_CAP"] # Set of all resources eligible for new capacity + RET_CAP = inputs["RET_CAP"] # Set of all resources eligible for capacity retirements + COMMIT = inputs["COMMIT"] # Set of all resources eligible for unit commitment + + ### Variables ### + + # Keep track of all new and retired capacity from all stages + @variable(EP, vCAPTRACK[y in RET_CAP, p = 1:num_stages]>=0) + @variable(EP, vRETCAPTRACK[y in RET_CAP, p = 1:num_stages]>=0) + + ### Expressions ### + + @expression(EP, eNewCap[y in RET_CAP], + if y in NEW_CAP + EP[:vCAP][y] + else + EP[:vZERO] + end) + + @expression(EP, eRetCap[y in RET_CAP], + if y in ids_with_all_options_contributing(gen) + EP[:vRETCAP][y] + EP[:vRETROFITCAP][y] + else + EP[:vRETCAP][y] + end) + + # Construct and add the endogenous retirement constraint expressions + @expression(EP, + eRetCapTrack[y in RET_CAP], + sum(EP[:vRETCAPTRACK][y, p] for p in 1:cur_stage)) + @expression(EP, + eNewCapTrack[y in RET_CAP], + sum(EP[:vCAPTRACK][y, p] + for p in 1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) + @expression(EP, eMinRetCapTrack[y in RET_CAP], + if y in COMMIT + cum_min_retired_cap_mw(gen[y]) / cap_size(gen[y]) + else + cum_min_retired_cap_mw(gen[y]) + end) + + ### Constraints ### + + # Keep track of newly built capacity from previous stages + @constraint(EP, cCapTrackNew[y in RET_CAP], eNewCap[y]==vCAPTRACK[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, cCapTrack[y in RET_CAP, p = 1:(cur_stage - 1)], vCAPTRACK[y, p]==0) + + # Keep track of retired capacity from previous stages + @constraint(EP, cRetCapTrackNew[y in RET_CAP], eRetCap[y]==vRETCAPTRACK[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, + cRetCapTrack[y in RET_CAP, p = 1:(cur_stage - 1)], + vRETCAPTRACK[y, p]==0) + + # Create a slack variable for each resource that is not contributing to the retired capacity being tracked + # This ensures that the model is able to satisfy the minimum retirement constraint + RETROFIT_WITH_SLACK = ids_with_all_options_not_contributing(gen) + if !isempty(RETROFIT_WITH_SLACK) + @variable(EP, vslack_lifetime[y in RETROFIT_WITH_SLACK]>=0) + @expression(EP, + vslack_term, + 2*maximum(inv_cost_per_mwyr.(gen))* + sum(vslack_lifetime[y] for y in RETROFIT_WITH_SLACK; init = 0)) + add_to_expression!(EP[:eObj], vslack_term) + end + + @expression(EP, eLifetimeRetRHS[y in RET_CAP], + if y in RETROFIT_WITH_SLACK + eRetCapTrack[y] + vslack_lifetime[y] + else + eRetCapTrack[y] + end) + + @constraint(EP, + cLifetimeRet[y in RET_CAP], + eNewCapTrack[y] + eMinRetCapTrack[y]<=eLifetimeRetRHS[y]) end -function endogenous_retirement_charge!(EP::Model, inputs::Dict, num_stages::Int, cur_stage::Int, stage_lens::Array{Int, 1}) - - println("Endogenous Retirement (Charge) Module") - - gen = inputs["RESOURCES"] - - NEW_CAP_CHARGE = inputs["NEW_CAP_CHARGE"] # Set of asymmetric charge/discharge storage resources eligible for new charge capacity - RET_CAP_CHARGE = inputs["RET_CAP_CHARGE"] # Set of asymmetric charge/discharge storage resources eligible for charge capacity retirements - - ### Variables ### - - # Keep track of all new and retired capacity from all stages - @variable(EP, vCAPTRACKCHARGE[y in RET_CAP_CHARGE,p=1:num_stages] >= 0) - @variable(EP, vRETCAPTRACKCHARGE[y in RET_CAP_CHARGE,p=1:num_stages] >= 0) - - ### Expressions ### - - @expression(EP, eNewCapCharge[y in RET_CAP_CHARGE], - if y in NEW_CAP_CHARGE - EP[:vCAPCHARGE][y] - else - EP[:vZERO] - end - ) - - @expression(EP, eRetCapCharge[y in RET_CAP_CHARGE], EP[:vRETCAPCHARGE][y]) - - # Construct and add the endogenous retirement constraint expressions - @expression(EP, eRetCapTrackCharge[y in RET_CAP_CHARGE], sum(EP[:vRETCAPTRACKCHARGE][y,p] for p=1:cur_stage)) - @expression(EP, eNewCapTrackCharge[y in RET_CAP_CHARGE], sum(EP[:vCAPTRACKCHARGE][y,p] for p=1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) - @expression(EP, eMinRetCapTrackCharge[y in RET_CAP_CHARGE], cum_min_retired_charge_cap_mw(gen[y])) - - ### Constratints ### - - # Keep track of newly built capacity from previous stages - @constraint(EP, cCapTrackChargeNew[y in RET_CAP_CHARGE], eNewCapCharge[y] == vCAPTRACKCHARGE[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cCapTrackCharge[y in RET_CAP_CHARGE,p=1:(cur_stage-1)], vCAPTRACKCHARGE[y,p] == 0) - - # Keep track of retired capacity from previous stages - @constraint(EP, cRetCapTrackChargeNew[y in RET_CAP_CHARGE], eRetCapCharge[y] == vRETCAPTRACKCHARGE[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cRetCapTrackCharge[y in RET_CAP_CHARGE,p=1:(cur_stage-1)], vRETCAPTRACKCHARGE[y,p] == 0) - - @constraint(EP, cLifetimeRetCharge[y in RET_CAP_CHARGE], eNewCapTrackCharge[y] + eMinRetCapTrackCharge[y] <= eRetCapTrackCharge[y]) - +function endogenous_retirement_charge!(EP::Model, + inputs::Dict, + num_stages::Int, + cur_stage::Int, + stage_lens::Array{Int, 1}) + println("Endogenous Retirement (Charge) Module") + + gen = inputs["RESOURCES"] + + NEW_CAP_CHARGE = inputs["NEW_CAP_CHARGE"] # Set of asymmetric charge/discharge storage resources eligible for new charge capacity + RET_CAP_CHARGE = inputs["RET_CAP_CHARGE"] # Set of asymmetric charge/discharge storage resources eligible for charge capacity retirements + + ### Variables ### + + # Keep track of all new and retired capacity from all stages + @variable(EP, vCAPTRACKCHARGE[y in RET_CAP_CHARGE, p = 1:num_stages]>=0) + @variable(EP, vRETCAPTRACKCHARGE[y in RET_CAP_CHARGE, p = 1:num_stages]>=0) + + ### Expressions ### + + @expression(EP, eNewCapCharge[y in RET_CAP_CHARGE], + if y in NEW_CAP_CHARGE + EP[:vCAPCHARGE][y] + else + EP[:vZERO] + end) + + @expression(EP, eRetCapCharge[y in RET_CAP_CHARGE], EP[:vRETCAPCHARGE][y]) + + # Construct and add the endogenous retirement constraint expressions + @expression(EP, + eRetCapTrackCharge[y in RET_CAP_CHARGE], + sum(EP[:vRETCAPTRACKCHARGE][y, p] for p in 1:cur_stage)) + @expression(EP, + eNewCapTrackCharge[y in RET_CAP_CHARGE], + sum(EP[:vCAPTRACKCHARGE][y, p] + for p in 1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) + @expression(EP, + eMinRetCapTrackCharge[y in RET_CAP_CHARGE], + cum_min_retired_charge_cap_mw(gen[y])) + + ### Constratints ### + + # Keep track of newly built capacity from previous stages + @constraint(EP, + cCapTrackChargeNew[y in RET_CAP_CHARGE], + eNewCapCharge[y]==vCAPTRACKCHARGE[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, + cCapTrackCharge[y in RET_CAP_CHARGE, p = 1:(cur_stage - 1)], + vCAPTRACKCHARGE[y, p]==0) + + # Keep track of retired capacity from previous stages + @constraint(EP, + cRetCapTrackChargeNew[y in RET_CAP_CHARGE], + eRetCapCharge[y]==vRETCAPTRACKCHARGE[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, + cRetCapTrackCharge[y in RET_CAP_CHARGE, p = 1:(cur_stage - 1)], + vRETCAPTRACKCHARGE[y, p]==0) + + @constraint(EP, + cLifetimeRetCharge[y in RET_CAP_CHARGE], + eNewCapTrackCharge[y] + eMinRetCapTrackCharge[y]<=eRetCapTrackCharge[y]) end -function endogenous_retirement_energy!(EP::Model, inputs::Dict, num_stages::Int, cur_stage::Int, stage_lens::Array{Int, 1}) - - println("Endogenous Retirement (Energy) Module") - - gen = inputs["RESOURCES"] - - NEW_CAP_ENERGY = inputs["NEW_CAP_ENERGY"] # Set of all storage resources eligible for new energy capacity - RET_CAP_ENERGY = inputs["RET_CAP_ENERGY"] # Set of all storage resources eligible for energy capacity retirements - - ### Variables ### - - # Keep track of all new and retired capacity from all stages - @variable(EP, vCAPTRACKENERGY[y in RET_CAP_ENERGY,p=1:num_stages] >= 0) - @variable(EP, vRETCAPTRACKENERGY[y in RET_CAP_ENERGY,p=1:num_stages] >= 0) - - ### Expressions ### - - @expression(EP, eNewCapEnergy[y in RET_CAP_ENERGY], - if y in NEW_CAP_ENERGY - EP[:vCAPENERGY][y] - else - EP[:vZERO] - end - ) - - @expression(EP, eRetCapEnergy[y in RET_CAP_ENERGY], EP[:vRETCAPENERGY][y]) - - # Construct and add the endogenous retirement constraint expressions - @expression(EP, eRetCapTrackEnergy[y in RET_CAP_ENERGY], sum(EP[:vRETCAPTRACKENERGY][y,p] for p=1:cur_stage)) - @expression(EP, eNewCapTrackEnergy[y in RET_CAP_ENERGY], sum(EP[:vCAPTRACKENERGY][y,p] for p=1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) - @expression(EP, eMinRetCapTrackEnergy[y in RET_CAP_ENERGY], cum_min_retired_energy_cap_mw(gen[y])) - - ### Constratints ### - - # Keep track of newly built capacity from previous stages - @constraint(EP, cCapTrackEnergyNew[y in RET_CAP_ENERGY], eNewCapEnergy[y] == vCAPTRACKENERGY[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cCapTrackEnergy[y in RET_CAP_ENERGY,p=1:(cur_stage-1)], vCAPTRACKENERGY[y,p] == 0) - - # Keep track of retired capacity from previous stages - @constraint(EP, cRetCapTrackEnergyNew[y in RET_CAP_ENERGY], eRetCapEnergy[y] == vRETCAPTRACKENERGY[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cRetCapTrackEnergy[y in RET_CAP_ENERGY,p=1:(cur_stage-1)], vRETCAPTRACKENERGY[y,p] == 0) - - @constraint(EP, cLifetimeRetEnergy[y in RET_CAP_ENERGY], eNewCapTrackEnergy[y] + eMinRetCapTrackEnergy[y] <= eRetCapTrackEnergy[y]) +function endogenous_retirement_energy!(EP::Model, + inputs::Dict, + num_stages::Int, + cur_stage::Int, + stage_lens::Array{Int, 1}) + println("Endogenous Retirement (Energy) Module") + + gen = inputs["RESOURCES"] + + NEW_CAP_ENERGY = inputs["NEW_CAP_ENERGY"] # Set of all storage resources eligible for new energy capacity + RET_CAP_ENERGY = inputs["RET_CAP_ENERGY"] # Set of all storage resources eligible for energy capacity retirements + + ### Variables ### + + # Keep track of all new and retired capacity from all stages + @variable(EP, vCAPTRACKENERGY[y in RET_CAP_ENERGY, p = 1:num_stages]>=0) + @variable(EP, vRETCAPTRACKENERGY[y in RET_CAP_ENERGY, p = 1:num_stages]>=0) + + ### Expressions ### + + @expression(EP, eNewCapEnergy[y in RET_CAP_ENERGY], + if y in NEW_CAP_ENERGY + EP[:vCAPENERGY][y] + else + EP[:vZERO] + end) + + @expression(EP, eRetCapEnergy[y in RET_CAP_ENERGY], EP[:vRETCAPENERGY][y]) + + # Construct and add the endogenous retirement constraint expressions + @expression(EP, + eRetCapTrackEnergy[y in RET_CAP_ENERGY], + sum(EP[:vRETCAPTRACKENERGY][y, p] for p in 1:cur_stage)) + @expression(EP, + eNewCapTrackEnergy[y in RET_CAP_ENERGY], + sum(EP[:vCAPTRACKENERGY][y, p] + for p in 1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) + @expression(EP, + eMinRetCapTrackEnergy[y in RET_CAP_ENERGY], + cum_min_retired_energy_cap_mw(gen[y])) + + ### Constratints ### + + # Keep track of newly built capacity from previous stages + @constraint(EP, + cCapTrackEnergyNew[y in RET_CAP_ENERGY], + eNewCapEnergy[y]==vCAPTRACKENERGY[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, + cCapTrackEnergy[y in RET_CAP_ENERGY, p = 1:(cur_stage - 1)], + vCAPTRACKENERGY[y, p]==0) + + # Keep track of retired capacity from previous stages + @constraint(EP, + cRetCapTrackEnergyNew[y in RET_CAP_ENERGY], + eRetCapEnergy[y]==vRETCAPTRACKENERGY[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, + cRetCapTrackEnergy[y in RET_CAP_ENERGY, p = 1:(cur_stage - 1)], + vRETCAPTRACKENERGY[y, p]==0) + + @constraint(EP, + cLifetimeRetEnergy[y in RET_CAP_ENERGY], + eNewCapTrackEnergy[y] + eMinRetCapTrackEnergy[y]<=eRetCapTrackEnergy[y]) end -function endogenous_retirement_vre_stor_dc!(EP::Model, inputs::Dict, num_stages::Int, cur_stage::Int, stage_lens::Array{Int, 1}) - - println("Endogenous Retirement (VRE-Storage DC) Module") - - gen = inputs["RESOURCES"] - - NEW_CAP_DC = inputs["NEW_CAP_DC"] # Set of all resources eligible for new capacity - RET_CAP_DC = inputs["RET_CAP_DC"] # Set of all resources eligible for capacity retirements - - ### Variables ### - - # Keep track of all new and retired capacity from all stages - @variable(EP, vCAPTRACKDC[y in RET_CAP_DC,p=1:num_stages] >= 0 ) - @variable(EP, vRETCAPTRACKDC[y in RET_CAP_DC,p=1:num_stages] >= 0 ) - - ### Expressions ### - - @expression(EP, eNewCapDC[y in RET_CAP_DC], - if y in NEW_CAP_DC - EP[:vDCCAP][y] - else - EP[:vZERO] - end - ) - - @expression(EP, eRetCapDC[y in RET_CAP_DC], EP[:vRETDCCAP][y]) - - # Construct and add the endogenous retirement constraint expressions - @expression(EP, eRetCapTrackDC[y in RET_CAP_DC], sum(EP[:vRETCAPTRACKDC][y,p] for p=1:cur_stage)) - @expression(EP, eNewCapTrackDC[y in RET_CAP_DC], sum(EP[:vCAPTRACKDC][y,p] for p=1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) - @expression(EP, eMinRetCapTrackDC[y in RET_CAP_DC], cum_min_retired_cap_inverter_mw(gen[y])) - - ### Constraints ### - - # Keep track of newly built capacity from previous stages - @constraint(EP, cCapTrackNewDC[y in RET_CAP_DC], eNewCapDC[y] == vCAPTRACKDC[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cCapTrackDC[y in RET_CAP_DC,p=1:(cur_stage-1)], vCAPTRACKDC[y,p] == 0) - - # Keep track of retired capacity from previous stages - @constraint(EP, cRetCapTrackNewDC[y in RET_CAP_DC], eRetCapDC[y] == vRETCAPTRACKDC[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cRetCapTrackDC[y in RET_CAP_DC,p=1:(cur_stage-1)], vRETCAPTRACKDC[y,p] == 0) - - @constraint(EP, cLifetimeRetDC[y in RET_CAP_DC], eNewCapTrackDC[y] + eMinRetCapTrackDC[y] <= eRetCapTrackDC[y]) +function endogenous_retirement_vre_stor_dc!(EP::Model, + inputs::Dict, + num_stages::Int, + cur_stage::Int, + stage_lens::Array{Int, 1}) + println("Endogenous Retirement (VRE-Storage DC) Module") + + gen = inputs["RESOURCES"] + + NEW_CAP_DC = inputs["NEW_CAP_DC"] # Set of all resources eligible for new capacity + RET_CAP_DC = inputs["RET_CAP_DC"] # Set of all resources eligible for capacity retirements + + ### Variables ### + + # Keep track of all new and retired capacity from all stages + @variable(EP, vCAPTRACKDC[y in RET_CAP_DC, p = 1:num_stages]>=0) + @variable(EP, vRETCAPTRACKDC[y in RET_CAP_DC, p = 1:num_stages]>=0) + + ### Expressions ### + + @expression(EP, eNewCapDC[y in RET_CAP_DC], + if y in NEW_CAP_DC + EP[:vDCCAP][y] + else + EP[:vZERO] + end) + + @expression(EP, eRetCapDC[y in RET_CAP_DC], EP[:vRETDCCAP][y]) + + # Construct and add the endogenous retirement constraint expressions + @expression(EP, + eRetCapTrackDC[y in RET_CAP_DC], + sum(EP[:vRETCAPTRACKDC][y, p] for p in 1:cur_stage)) + @expression(EP, + eNewCapTrackDC[y in RET_CAP_DC], + sum(EP[:vCAPTRACKDC][y, p] + for p in 1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) + @expression(EP, + eMinRetCapTrackDC[y in RET_CAP_DC], + cum_min_retired_cap_inverter_mw(gen[y])) + + ### Constraints ### + + # Keep track of newly built capacity from previous stages + @constraint(EP, + cCapTrackNewDC[y in RET_CAP_DC], + eNewCapDC[y]==vCAPTRACKDC[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, + cCapTrackDC[y in RET_CAP_DC, p = 1:(cur_stage - 1)], + vCAPTRACKDC[y, p]==0) + + # Keep track of retired capacity from previous stages + @constraint(EP, + cRetCapTrackNewDC[y in RET_CAP_DC], + eRetCapDC[y]==vRETCAPTRACKDC[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, + cRetCapTrackDC[y in RET_CAP_DC, p = 1:(cur_stage - 1)], + vRETCAPTRACKDC[y, p]==0) + + @constraint(EP, + cLifetimeRetDC[y in RET_CAP_DC], + eNewCapTrackDC[y] + eMinRetCapTrackDC[y]<=eRetCapTrackDC[y]) end -function endogenous_retirement_vre_stor_solar!(EP::Model, inputs::Dict, num_stages::Int, cur_stage::Int, stage_lens::Array{Int, 1}) - - println("Endogenous Retirement (VRE-Storage Solar) Module") - - gen = inputs["RESOURCES"] - - NEW_CAP_SOLAR = inputs["NEW_CAP_SOLAR"] # Set of all resources eligible for new capacity - RET_CAP_SOLAR = inputs["RET_CAP_SOLAR"] # Set of all resources eligible for capacity retirements - - ### Variables ### - - # Keep track of all new and retired capacity from all stages - @variable(EP, vCAPTRACKSOLAR[y in RET_CAP_SOLAR,p=1:num_stages] >= 0 ) - @variable(EP, vRETCAPTRACKSOLAR[y in RET_CAP_SOLAR,p=1:num_stages] >= 0 ) - - ### Expressions ### - - @expression(EP, eNewCapSolar[y in RET_CAP_SOLAR], - if y in NEW_CAP_SOLAR - EP[:vSOLARCAP][y] - else - EP[:vZERO] - end - ) - - @expression(EP, eRetCapSolar[y in RET_CAP_SOLAR], EP[:vRETSOLARCAP][y]) - - # Construct and add the endogenous retirement constraint expressions - @expression(EP, eRetCapTrackSolar[y in RET_CAP_SOLAR], sum(EP[:vRETCAPTRACKSOLAR][y,p] for p=1:cur_stage)) - @expression(EP, eNewCapTrackSolar[y in RET_CAP_SOLAR], sum(EP[:vCAPTRACKSOLAR][y,p] for p=1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) - @expression(EP, eMinRetCapTrackSolar[y in RET_CAP_SOLAR], cum_min_retired_cap_solar_mw(gen[y])) - - ### Constraints ### - - # Keep track of newly built capacity from previous stages - @constraint(EP, cCapTrackNewSolar[y in RET_CAP_SOLAR], eNewCapSolar[y] == vCAPTRACKSOLAR[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cCapTrackSolar[y in RET_CAP_SOLAR,p=1:(cur_stage-1)], vCAPTRACKSOLAR[y,p] == 0) - - # Keep track of retired capacity from previous stages - @constraint(EP, cRetCapTrackNewSolar[y in RET_CAP_SOLAR], eRetCapSolar[y] == vRETCAPTRACKSOLAR[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cRetCapTrackSolar[y in RET_CAP_SOLAR,p=1:(cur_stage-1)], vRETCAPTRACKSOLAR[y,p] == 0) - - @constraint(EP, cLifetimeRetSolar[y in RET_CAP_SOLAR], eNewCapTrackSolar[y] + eMinRetCapTrackSolar[y] <= eRetCapTrackSolar[y]) +function endogenous_retirement_vre_stor_solar!(EP::Model, + inputs::Dict, + num_stages::Int, + cur_stage::Int, + stage_lens::Array{Int, 1}) + println("Endogenous Retirement (VRE-Storage Solar) Module") + + gen = inputs["RESOURCES"] + + NEW_CAP_SOLAR = inputs["NEW_CAP_SOLAR"] # Set of all resources eligible for new capacity + RET_CAP_SOLAR = inputs["RET_CAP_SOLAR"] # Set of all resources eligible for capacity retirements + + ### Variables ### + + # Keep track of all new and retired capacity from all stages + @variable(EP, vCAPTRACKSOLAR[y in RET_CAP_SOLAR, p = 1:num_stages]>=0) + @variable(EP, vRETCAPTRACKSOLAR[y in RET_CAP_SOLAR, p = 1:num_stages]>=0) + + ### Expressions ### + + @expression(EP, eNewCapSolar[y in RET_CAP_SOLAR], + if y in NEW_CAP_SOLAR + EP[:vSOLARCAP][y] + else + EP[:vZERO] + end) + + @expression(EP, eRetCapSolar[y in RET_CAP_SOLAR], EP[:vRETSOLARCAP][y]) + + # Construct and add the endogenous retirement constraint expressions + @expression(EP, + eRetCapTrackSolar[y in RET_CAP_SOLAR], + sum(EP[:vRETCAPTRACKSOLAR][y, p] for p in 1:cur_stage)) + @expression(EP, + eNewCapTrackSolar[y in RET_CAP_SOLAR], + sum(EP[:vCAPTRACKSOLAR][y, p] + for p in 1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) + @expression(EP, + eMinRetCapTrackSolar[y in RET_CAP_SOLAR], + cum_min_retired_cap_solar_mw(gen[y])) + + ### Constraints ### + + # Keep track of newly built capacity from previous stages + @constraint(EP, + cCapTrackNewSolar[y in RET_CAP_SOLAR], + eNewCapSolar[y]==vCAPTRACKSOLAR[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, + cCapTrackSolar[y in RET_CAP_SOLAR, p = 1:(cur_stage - 1)], + vCAPTRACKSOLAR[y, p]==0) + + # Keep track of retired capacity from previous stages + @constraint(EP, + cRetCapTrackNewSolar[y in RET_CAP_SOLAR], + eRetCapSolar[y]==vRETCAPTRACKSOLAR[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, + cRetCapTrackSolar[y in RET_CAP_SOLAR, p = 1:(cur_stage - 1)], + vRETCAPTRACKSOLAR[y, p]==0) + + @constraint(EP, + cLifetimeRetSolar[y in RET_CAP_SOLAR], + eNewCapTrackSolar[y] + eMinRetCapTrackSolar[y]<=eRetCapTrackSolar[y]) end -function endogenous_retirement_vre_stor_wind!(EP::Model, inputs::Dict, num_stages::Int, cur_stage::Int, stage_lens::Array{Int, 1}) - - println("Endogenous Retirement (VRE-Storage Wind) Module") - - gen = inputs["RESOURCES"] - - NEW_CAP_WIND = inputs["NEW_CAP_WIND"] # Set of all resources eligible for new capacity - RET_CAP_WIND = inputs["RET_CAP_WIND"] # Set of all resources eligible for capacity retirements - - ### Variables ### - - # Keep track of all new and retired capacity from all stages - @variable(EP, vCAPTRACKWIND[y in RET_CAP_WIND,p=1:num_stages] >= 0 ) - @variable(EP, vRETCAPTRACKWIND[y in RET_CAP_WIND,p=1:num_stages] >= 0 ) - - ### Expressions ### - - @expression(EP, eNewCapWind[y in RET_CAP_WIND], - if y in NEW_CAP_WIND - EP[:vWINDCAP][y] - else - EP[:vZERO] - end - ) - - @expression(EP, eRetCapWind[y in RET_CAP_WIND], EP[:vRETWINDCAP][y]) - - # Construct and add the endogenous retirement constraint expressions - @expression(EP, eRetCapTrackWind[y in RET_CAP_WIND], sum(EP[:vRETCAPTRACKWIND][y,p] for p=1:cur_stage)) - @expression(EP, eNewCapTrackWind[y in RET_CAP_WIND], sum(EP[:vCAPTRACKWIND][y,p] for p=1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) - @expression(EP, eMinRetCapTrackWind[y in RET_CAP_WIND], cum_min_retired_cap_wind_mw(gen[y])) - - ### Constraints ### - - # Keep track of newly built capacity from previous stages - @constraint(EP, cCapTrackNewWind[y in RET_CAP_WIND], eNewCapWind[y] == vCAPTRACKWIND[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cCapTrackWind[y in RET_CAP_WIND,p=1:(cur_stage-1)], vCAPTRACKWIND[y,p] == 0) - - # Keep track of retired capacity from previous stages - @constraint(EP, cRetCapTrackNewWind[y in RET_CAP_WIND], eRetCapWind[y] == vRETCAPTRACKWIND[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cRetCapTrackWind[y in RET_CAP_WIND,p=1:(cur_stage-1)], vRETCAPTRACKWIND[y,p] == 0) - - @constraint(EP, cLifetimeRetWind[y in RET_CAP_WIND], eNewCapTrackWind[y] + eMinRetCapTrackWind[y] <= eRetCapTrackWind[y]) +function endogenous_retirement_vre_stor_wind!(EP::Model, + inputs::Dict, + num_stages::Int, + cur_stage::Int, + stage_lens::Array{Int, 1}) + println("Endogenous Retirement (VRE-Storage Wind) Module") + + gen = inputs["RESOURCES"] + + NEW_CAP_WIND = inputs["NEW_CAP_WIND"] # Set of all resources eligible for new capacity + RET_CAP_WIND = inputs["RET_CAP_WIND"] # Set of all resources eligible for capacity retirements + + ### Variables ### + + # Keep track of all new and retired capacity from all stages + @variable(EP, vCAPTRACKWIND[y in RET_CAP_WIND, p = 1:num_stages]>=0) + @variable(EP, vRETCAPTRACKWIND[y in RET_CAP_WIND, p = 1:num_stages]>=0) + + ### Expressions ### + + @expression(EP, eNewCapWind[y in RET_CAP_WIND], + if y in NEW_CAP_WIND + EP[:vWINDCAP][y] + else + EP[:vZERO] + end) + + @expression(EP, eRetCapWind[y in RET_CAP_WIND], EP[:vRETWINDCAP][y]) + + # Construct and add the endogenous retirement constraint expressions + @expression(EP, + eRetCapTrackWind[y in RET_CAP_WIND], + sum(EP[:vRETCAPTRACKWIND][y, p] for p in 1:cur_stage)) + @expression(EP, + eNewCapTrackWind[y in RET_CAP_WIND], + sum(EP[:vCAPTRACKWIND][y, p] + for p in 1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) + @expression(EP, + eMinRetCapTrackWind[y in RET_CAP_WIND], + cum_min_retired_cap_wind_mw(gen[y])) + + ### Constraints ### + + # Keep track of newly built capacity from previous stages + @constraint(EP, + cCapTrackNewWind[y in RET_CAP_WIND], + eNewCapWind[y]==vCAPTRACKWIND[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, + cCapTrackWind[y in RET_CAP_WIND, p = 1:(cur_stage - 1)], + vCAPTRACKWIND[y, p]==0) + + # Keep track of retired capacity from previous stages + @constraint(EP, + cRetCapTrackNewWind[y in RET_CAP_WIND], + eRetCapWind[y]==vRETCAPTRACKWIND[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, + cRetCapTrackWind[y in RET_CAP_WIND, p = 1:(cur_stage - 1)], + vRETCAPTRACKWIND[y, p]==0) + + @constraint(EP, + cLifetimeRetWind[y in RET_CAP_WIND], + eNewCapTrackWind[y] + eMinRetCapTrackWind[y]<=eRetCapTrackWind[y]) end -function endogenous_retirement_vre_stor_stor!(EP::Model, inputs::Dict, num_stages::Int, cur_stage::Int, stage_lens::Array{Int, 1}) - - println("Endogenous Retirement (VRE-Storage Storage) Module") - - gen = inputs["RESOURCES"] - - NEW_CAP_STOR = inputs["NEW_CAP_STOR"] # Set of all resources eligible for new capacity - RET_CAP_STOR = inputs["RET_CAP_STOR"] # Set of all resources eligible for capacity retirements - - ### Variables ### - - # Keep track of all new and retired capacity from all stages - @variable(EP, vCAPTRACKENERGY_VS[y in RET_CAP_STOR,p=1:num_stages] >= 0) - @variable(EP, vRETCAPTRACKENERGY_VS[y in RET_CAP_STOR,p=1:num_stages] >= 0) - - ### Expressions ### - - @expression(EP, eNewCapEnergy_VS[y in RET_CAP_STOR], - if y in NEW_CAP_STOR - EP[:vCAPENERGY_VS][y] - else - EP[:vZERO] - end - ) - - @expression(EP, eRetCapEnergy_VS[y in RET_CAP_STOR], EP[:vRETCAPENERGY_VS][y]) - - # Construct and add the endogenous retirement constraint expressions - @expression(EP, eRetCapTrackEnergy_VS[y in RET_CAP_STOR], sum(EP[:vRETCAPTRACKENERGY_VS][y,p] for p=1:cur_stage)) - @expression(EP, eNewCapTrackEnergy_VS[y in RET_CAP_STOR], sum(EP[:vCAPTRACKENERGY_VS][y,p] for p=1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) - @expression(EP, eMinRetCapTrackEnergy_VS[y in RET_CAP_STOR], cum_min_retired_energy_cap_mw(gen[y])) - - ### Constratints ### - - # Keep track of newly built capacity from previous stages - @constraint(EP, cCapTrackEnergyNew_VS[y in RET_CAP_STOR], eNewCapEnergy_VS[y] == vCAPTRACKENERGY_VS[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cCapTrackEnergy_VS[y in RET_CAP_STOR,p=1:(cur_stage-1)], vCAPTRACKENERGY_VS[y,p] == 0) - - # Keep track of retired capacity from previous stages - @constraint(EP, cRetCapTrackEnergyNew_VS[y in RET_CAP_STOR], eRetCapEnergy_VS[y] == vRETCAPTRACKENERGY_VS[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cRetCapTrackEnergy_VS[y in RET_CAP_STOR,p=1:(cur_stage-1)], vRETCAPTRACKENERGY_VS[y,p] == 0) - - @constraint(EP, cLifetimeRetEnergy_VS[y in RET_CAP_STOR], eNewCapTrackEnergy_VS[y] + eMinRetCapTrackEnergy_VS[y] <= eRetCapTrackEnergy_VS[y]) +function endogenous_retirement_vre_stor_stor!(EP::Model, + inputs::Dict, + num_stages::Int, + cur_stage::Int, + stage_lens::Array{Int, 1}) + println("Endogenous Retirement (VRE-Storage Storage) Module") + + gen = inputs["RESOURCES"] + + NEW_CAP_STOR = inputs["NEW_CAP_STOR"] # Set of all resources eligible for new capacity + RET_CAP_STOR = inputs["RET_CAP_STOR"] # Set of all resources eligible for capacity retirements + + ### Variables ### + + # Keep track of all new and retired capacity from all stages + @variable(EP, vCAPTRACKENERGY_VS[y in RET_CAP_STOR, p = 1:num_stages]>=0) + @variable(EP, vRETCAPTRACKENERGY_VS[y in RET_CAP_STOR, p = 1:num_stages]>=0) + + ### Expressions ### + + @expression(EP, eNewCapEnergy_VS[y in RET_CAP_STOR], + if y in NEW_CAP_STOR + EP[:vCAPENERGY_VS][y] + else + EP[:vZERO] + end) + + @expression(EP, eRetCapEnergy_VS[y in RET_CAP_STOR], EP[:vRETCAPENERGY_VS][y]) + + # Construct and add the endogenous retirement constraint expressions + @expression(EP, + eRetCapTrackEnergy_VS[y in RET_CAP_STOR], + sum(EP[:vRETCAPTRACKENERGY_VS][y, p] for p in 1:cur_stage)) + @expression(EP, + eNewCapTrackEnergy_VS[y in RET_CAP_STOR], + sum(EP[:vCAPTRACKENERGY_VS][y, p] + for p in 1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) + @expression(EP, + eMinRetCapTrackEnergy_VS[y in RET_CAP_STOR], + cum_min_retired_energy_cap_mw(gen[y])) + + ### Constratints ### + + # Keep track of newly built capacity from previous stages + @constraint(EP, + cCapTrackEnergyNew_VS[y in RET_CAP_STOR], + eNewCapEnergy_VS[y]==vCAPTRACKENERGY_VS[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, + cCapTrackEnergy_VS[y in RET_CAP_STOR, p = 1:(cur_stage - 1)], + vCAPTRACKENERGY_VS[y, p]==0) + + # Keep track of retired capacity from previous stages + @constraint(EP, + cRetCapTrackEnergyNew_VS[y in RET_CAP_STOR], + eRetCapEnergy_VS[y]==vRETCAPTRACKENERGY_VS[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, + cRetCapTrackEnergy_VS[y in RET_CAP_STOR, p = 1:(cur_stage - 1)], + vRETCAPTRACKENERGY_VS[y, p]==0) + + @constraint(EP, + cLifetimeRetEnergy_VS[y in RET_CAP_STOR], + eNewCapTrackEnergy_VS[y] + eMinRetCapTrackEnergy_VS[y]<=eRetCapTrackEnergy_VS[y]) end -function endogenous_retirement_vre_stor_discharge_dc!(EP::Model, inputs::Dict, num_stages::Int, cur_stage::Int, stage_lens::Array{Int, 1}) - - println("Endogenous Retirement (VRE-Storage Discharge DC) Module") - - gen = inputs["RESOURCES"] - - NEW_CAP_DISCHARGE_DC = inputs["NEW_CAP_DISCHARGE_DC"] # Set of all resources eligible for new capacity - RET_CAP_DISCHARGE_DC = inputs["RET_CAP_DISCHARGE_DC"] # Set of all resources eligible for capacity retirements - - ### Variables ### - - # Keep track of all new and retired capacity from all stages - @variable(EP, vCAPTRACKDISCHARGEDC[y in RET_CAP_DISCHARGE_DC,p=1:num_stages] >= 0 ) - @variable(EP, vRETCAPTRACKDISCHARGEDC[y in RET_CAP_DISCHARGE_DC,p=1:num_stages] >= 0 ) - - ### Expressions ### - - @expression(EP, eNewCapDischargeDC[y in RET_CAP_DISCHARGE_DC], - if y in NEW_CAP_DISCHARGE_DC - EP[:vCAPDISCHARGE_DC][y] - else - EP[:vZERO] - end - ) - - @expression(EP, eRetCapDischargeDC[y in RET_CAP_DISCHARGE_DC], EP[:vRETCAPDISCHARGE_DC][y]) - - # Construct and add the endogenous retirement constraint expressions - @expression(EP, eRetCapTrackDischargeDC[y in RET_CAP_DISCHARGE_DC], sum(EP[:vRETCAPTRACKDISCHARGEDC][y,p] for p=1:cur_stage)) - @expression(EP, eNewCapTrackDischargeDC[y in RET_CAP_DISCHARGE_DC], sum(EP[:vCAPTRACKDISCHARGEDC][y,p] for p=1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) - @expression(EP, eMinRetCapTrackDischargeDC[y in RET_CAP_DISCHARGE_DC], cum_min_retired_cap_discharge_dc_mw(gen[y])) - - ### Constraints ### - - # Keep track of newly built capacity from previous stages - @constraint(EP, cCapTrackNewDischargeDC[y in RET_CAP_DISCHARGE_DC], eNewCapDischargeDC[y] == vCAPTRACKDISCHARGEDC[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cCapTrackDischargeDC[y in RET_CAP_DISCHARGE_DC,p=1:(cur_stage-1)], vCAPTRACKDISCHARGEDC[y,p] == 0) - - # Keep track of retired capacity from previous stages - @constraint(EP, cRetCapTrackNewDischargeDC[y in RET_CAP_DISCHARGE_DC], eRetCapTrackDischargeDC[y] == vRETCAPTRACKDISCHARGEDC[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cRetCapTrackDischargeDC[y in RET_CAP_DISCHARGE_DC,p=1:(cur_stage-1)], vRETCAPTRACKDISCHARGEDC[y,p] == 0) - - @constraint(EP, cLifetimeRetDischargeDC[y in RET_CAP_DISCHARGE_DC], eNewCapTrackDischargeDC[y] + eMinRetCapTrackDischargeDC[y] <= eRetCapTrackDischargeDC[y]) +function endogenous_retirement_vre_stor_discharge_dc!(EP::Model, + inputs::Dict, + num_stages::Int, + cur_stage::Int, + stage_lens::Array{Int, 1}) + println("Endogenous Retirement (VRE-Storage Discharge DC) Module") + + gen = inputs["RESOURCES"] + + NEW_CAP_DISCHARGE_DC = inputs["NEW_CAP_DISCHARGE_DC"] # Set of all resources eligible for new capacity + RET_CAP_DISCHARGE_DC = inputs["RET_CAP_DISCHARGE_DC"] # Set of all resources eligible for capacity retirements + + ### Variables ### + + # Keep track of all new and retired capacity from all stages + @variable(EP, vCAPTRACKDISCHARGEDC[y in RET_CAP_DISCHARGE_DC, p = 1:num_stages]>=0) + @variable(EP, vRETCAPTRACKDISCHARGEDC[y in RET_CAP_DISCHARGE_DC, p = 1:num_stages]>=0) + + ### Expressions ### + + @expression(EP, eNewCapDischargeDC[y in RET_CAP_DISCHARGE_DC], + if y in NEW_CAP_DISCHARGE_DC + EP[:vCAPDISCHARGE_DC][y] + else + EP[:vZERO] + end) + + @expression(EP, + eRetCapDischargeDC[y in RET_CAP_DISCHARGE_DC], + EP[:vRETCAPDISCHARGE_DC][y]) + + # Construct and add the endogenous retirement constraint expressions + @expression(EP, + eRetCapTrackDischargeDC[y in RET_CAP_DISCHARGE_DC], + sum(EP[:vRETCAPTRACKDISCHARGEDC][y, p] for p in 1:cur_stage)) + @expression(EP, + eNewCapTrackDischargeDC[y in RET_CAP_DISCHARGE_DC], + sum(EP[:vCAPTRACKDISCHARGEDC][y, p] + for p in 1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) + @expression(EP, + eMinRetCapTrackDischargeDC[y in RET_CAP_DISCHARGE_DC], + cum_min_retired_cap_discharge_dc_mw(gen[y])) + + ### Constraints ### + + # Keep track of newly built capacity from previous stages + @constraint(EP, + cCapTrackNewDischargeDC[y in RET_CAP_DISCHARGE_DC], + eNewCapDischargeDC[y]==vCAPTRACKDISCHARGEDC[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, + cCapTrackDischargeDC[y in RET_CAP_DISCHARGE_DC, p = 1:(cur_stage - 1)], + vCAPTRACKDISCHARGEDC[y, p]==0) + + # Keep track of retired capacity from previous stages + @constraint(EP, + cRetCapTrackNewDischargeDC[y in RET_CAP_DISCHARGE_DC], + eRetCapTrackDischargeDC[y]==vRETCAPTRACKDISCHARGEDC[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, + cRetCapTrackDischargeDC[y in RET_CAP_DISCHARGE_DC, p = 1:(cur_stage - 1)], + vRETCAPTRACKDISCHARGEDC[y, p]==0) + + @constraint(EP, + cLifetimeRetDischargeDC[y in RET_CAP_DISCHARGE_DC], + eNewCapTrackDischargeDC[y] + + eMinRetCapTrackDischargeDC[y]<=eRetCapTrackDischargeDC[y]) end -function endogenous_retirement_vre_stor_charge_dc!(EP::Model, inputs::Dict, num_stages::Int, cur_stage::Int, stage_lens::Array{Int, 1}) - - println("Endogenous Retirement (VRE-Storage Charge DC) Module") - - gen = inputs["RESOURCES"] - NEW_CAP_CHARGE_DC = inputs["NEW_CAP_CHARGE_DC"] # Set of all resources eligible for new capacity - RET_CAP_CHARGE_DC = inputs["RET_CAP_CHARGE_DC"] # Set of all resources eligible for capacity retirements - - ### Variables ### - - # Keep track of all new and retired capacity from all stages - @variable(EP, vCAPTRACKCHARGEDC[y in RET_CAP_CHARGE_DC,p=1:num_stages] >= 0 ) - @variable(EP, vRETCAPTRACKCHARGEDC[y in RET_CAP_CHARGE_DC,p=1:num_stages] >= 0 ) - - ### Expressions ### - - @expression(EP, eNewCapChargeDC[y in RET_CAP_CHARGE_DC], - if y in NEW_CAP_CHARGE_DC - EP[:vCAPCHARGE_DC][y] - else - EP[:vZERO] - end - ) - - @expression(EP, eRetCapChargeDC[y in RET_CAP_CHARGE_DC], EP[:vRETCAPCHARGE_DC][y]) - - # Construct and add the endogenous retirement constraint expressions - @expression(EP, eRetCapTrackChargeDC[y in RET_CAP_CHARGE_DC], sum(EP[:vRETCAPTRACKCHARGEDC][y,p] for p=1:cur_stage)) - @expression(EP, eNewCapTrackChargeDC[y in RET_CAP_CHARGE_DC], sum(EP[:vCAPTRACKCHARGEDC][y,p] for p=1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) - @expression(EP, eMinRetCapTrackChargeDC[y in RET_CAP_CHARGE_DC], cum_min_retired_cap_charge_dc_mw(gen[y])) - - ### Constraints ### - - # Keep track of newly built capacity from previous stages - @constraint(EP, cCapTrackNewChargeDC[y in RET_CAP_CHARGE_DC], eNewCapChargeDC[y] == vCAPTRACKCHARGEDC[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cCapTrackChargeDC[y in RET_CAP_CHARGE_DC,p=1:(cur_stage-1)], vCAPTRACKCHARGEDC[y,p] == 0) - - # Keep track of retired capacity from previous stages - @constraint(EP, cRetCapTrackNewChargeDC[y in RET_CAP_CHARGE_DC], eRetCapTrackChargeDC[y] == vRETCAPTRACKCHARGEDC[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cRetCapTrackChargeDC[y in RET_CAP_CHARGE_DC,p=1:(cur_stage-1)], vRETCAPTRACKCHARGEDC[y,p] == 0) - - @constraint(EP, cLifetimeRetChargeDC[y in RET_CAP_CHARGE_DC], eNewCapTrackChargeDC[y] + eMinRetCapTrackChargeDC[y] <= eRetCapTrackChargeDC[y]) +function endogenous_retirement_vre_stor_charge_dc!(EP::Model, + inputs::Dict, + num_stages::Int, + cur_stage::Int, + stage_lens::Array{Int, 1}) + println("Endogenous Retirement (VRE-Storage Charge DC) Module") + + gen = inputs["RESOURCES"] + NEW_CAP_CHARGE_DC = inputs["NEW_CAP_CHARGE_DC"] # Set of all resources eligible for new capacity + RET_CAP_CHARGE_DC = inputs["RET_CAP_CHARGE_DC"] # Set of all resources eligible for capacity retirements + + ### Variables ### + + # Keep track of all new and retired capacity from all stages + @variable(EP, vCAPTRACKCHARGEDC[y in RET_CAP_CHARGE_DC, p = 1:num_stages]>=0) + @variable(EP, vRETCAPTRACKCHARGEDC[y in RET_CAP_CHARGE_DC, p = 1:num_stages]>=0) + + ### Expressions ### + + @expression(EP, eNewCapChargeDC[y in RET_CAP_CHARGE_DC], + if y in NEW_CAP_CHARGE_DC + EP[:vCAPCHARGE_DC][y] + else + EP[:vZERO] + end) + + @expression(EP, eRetCapChargeDC[y in RET_CAP_CHARGE_DC], EP[:vRETCAPCHARGE_DC][y]) + + # Construct and add the endogenous retirement constraint expressions + @expression(EP, + eRetCapTrackChargeDC[y in RET_CAP_CHARGE_DC], + sum(EP[:vRETCAPTRACKCHARGEDC][y, p] for p in 1:cur_stage)) + @expression(EP, + eNewCapTrackChargeDC[y in RET_CAP_CHARGE_DC], + sum(EP[:vCAPTRACKCHARGEDC][y, p] + for p in 1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) + @expression(EP, + eMinRetCapTrackChargeDC[y in RET_CAP_CHARGE_DC], + cum_min_retired_cap_charge_dc_mw(gen[y])) + + ### Constraints ### + + # Keep track of newly built capacity from previous stages + @constraint(EP, + cCapTrackNewChargeDC[y in RET_CAP_CHARGE_DC], + eNewCapChargeDC[y]==vCAPTRACKCHARGEDC[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, + cCapTrackChargeDC[y in RET_CAP_CHARGE_DC, p = 1:(cur_stage - 1)], + vCAPTRACKCHARGEDC[y, p]==0) + + # Keep track of retired capacity from previous stages + @constraint(EP, + cRetCapTrackNewChargeDC[y in RET_CAP_CHARGE_DC], + eRetCapTrackChargeDC[y]==vRETCAPTRACKCHARGEDC[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, + cRetCapTrackChargeDC[y in RET_CAP_CHARGE_DC, p = 1:(cur_stage - 1)], + vRETCAPTRACKCHARGEDC[y, p]==0) + + @constraint(EP, + cLifetimeRetChargeDC[y in RET_CAP_CHARGE_DC], + eNewCapTrackChargeDC[y] + eMinRetCapTrackChargeDC[y]<=eRetCapTrackChargeDC[y]) end -function endogenous_retirement_vre_stor_discharge_ac!(EP::Model, inputs::Dict, num_stages::Int, cur_stage::Int, stage_lens::Array{Int, 1}) - - println("Endogenous Retirement (VRE-Storage Discharge AC) Module") - - gen = inputs["RESOURCES"] - NEW_CAP_DISCHARGE_AC = inputs["NEW_CAP_DISCHARGE_AC"] # Set of all resources eligible for new capacity - RET_CAP_DISCHARGE_AC = inputs["RET_CAP_DISCHARGE_AC"] # Set of all resources eligible for capacity retirements - - ### Variables ### - - # Keep track of all new and retired capacity from all stages - @variable(EP, vCAPTRACKDISCHARGEAC[y in RET_CAP_DISCHARGE_AC,p=1:num_stages] >= 0 ) - @variable(EP, vRETCAPTRACKDISCHARGEAC[y in RET_CAP_DISCHARGE_AC,p=1:num_stages] >= 0 ) - - ### Expressions ### - - @expression(EP, eNewCapDischargeAC[y in RET_CAP_DISCHARGE_AC], - if y in NEW_CAP_DISCHARGE_AC - EP[:vCAPDISCHARGE_AC][y] - else - EP[:vZERO] - end - ) - - @expression(EP, eRetCapDischargeAC[y in RET_CAP_DISCHARGE_AC], EP[:vRETCAPDISCHARGE_AC][y]) - - # Construct and add the endogenous retirement constraint expressions - @expression(EP, eRetCapTrackDischargeAC[y in RET_CAP_DISCHARGE_AC], sum(EP[:vRETCAPTRACKDISCHARGEAC][y,p] for p=1:cur_stage)) - @expression(EP, eNewCapTrackDischargeAC[y in RET_CAP_DISCHARGE_AC], sum(EP[:vCAPTRACKDISCHARGEAC][y,p] for p=1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) - @expression(EP, eMinRetCapTrackDischargeAC[y in RET_CAP_DISCHARGE_AC], cum_min_retired_cap_discharge_ac_mw(gen[y])) - - ### Constraints ### - - # Keep track of newly built capacity from previous stages - @constraint(EP, cCapTrackNewDischargeAC[y in RET_CAP_DISCHARGE_AC], eNewCapDischargeAC[y] == vCAPTRACKDISCHARGEAC[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cCapTrackDischargeAC[y in RET_CAP_DISCHARGE_AC,p=1:(cur_stage-1)], vCAPTRACKDISCHARGEAC[y,p] == 0) - - # Keep track of retired capacity from previous stages - @constraint(EP, cRetCapTrackNewDischargeAC[y in RET_CAP_DISCHARGE_AC], eRetCapTrackDischargeAC[y] == vRETCAPTRACKDISCHARGEAC[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cRetCapTrackDischargeAC[y in RET_CAP_DISCHARGE_AC,p=1:(cur_stage-1)], vRETCAPTRACKDISCHARGEAC[y,p] == 0) - - @constraint(EP, cLifetimeRetDischargeAC[y in RET_CAP_DISCHARGE_AC], eNewCapTrackDischargeAC[y] + eMinRetCapTrackDischargeAC[y] <= eRetCapTrackDischargeAC[y]) +function endogenous_retirement_vre_stor_discharge_ac!(EP::Model, + inputs::Dict, + num_stages::Int, + cur_stage::Int, + stage_lens::Array{Int, 1}) + println("Endogenous Retirement (VRE-Storage Discharge AC) Module") + + gen = inputs["RESOURCES"] + NEW_CAP_DISCHARGE_AC = inputs["NEW_CAP_DISCHARGE_AC"] # Set of all resources eligible for new capacity + RET_CAP_DISCHARGE_AC = inputs["RET_CAP_DISCHARGE_AC"] # Set of all resources eligible for capacity retirements + + ### Variables ### + + # Keep track of all new and retired capacity from all stages + @variable(EP, vCAPTRACKDISCHARGEAC[y in RET_CAP_DISCHARGE_AC, p = 1:num_stages]>=0) + @variable(EP, vRETCAPTRACKDISCHARGEAC[y in RET_CAP_DISCHARGE_AC, p = 1:num_stages]>=0) + + ### Expressions ### + + @expression(EP, eNewCapDischargeAC[y in RET_CAP_DISCHARGE_AC], + if y in NEW_CAP_DISCHARGE_AC + EP[:vCAPDISCHARGE_AC][y] + else + EP[:vZERO] + end) + + @expression(EP, + eRetCapDischargeAC[y in RET_CAP_DISCHARGE_AC], + EP[:vRETCAPDISCHARGE_AC][y]) + + # Construct and add the endogenous retirement constraint expressions + @expression(EP, + eRetCapTrackDischargeAC[y in RET_CAP_DISCHARGE_AC], + sum(EP[:vRETCAPTRACKDISCHARGEAC][y, p] for p in 1:cur_stage)) + @expression(EP, + eNewCapTrackDischargeAC[y in RET_CAP_DISCHARGE_AC], + sum(EP[:vCAPTRACKDISCHARGEAC][y, p] + for p in 1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) + @expression(EP, + eMinRetCapTrackDischargeAC[y in RET_CAP_DISCHARGE_AC], + cum_min_retired_cap_discharge_ac_mw(gen[y])) + + ### Constraints ### + + # Keep track of newly built capacity from previous stages + @constraint(EP, + cCapTrackNewDischargeAC[y in RET_CAP_DISCHARGE_AC], + eNewCapDischargeAC[y]==vCAPTRACKDISCHARGEAC[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, + cCapTrackDischargeAC[y in RET_CAP_DISCHARGE_AC, p = 1:(cur_stage - 1)], + vCAPTRACKDISCHARGEAC[y, p]==0) + + # Keep track of retired capacity from previous stages + @constraint(EP, + cRetCapTrackNewDischargeAC[y in RET_CAP_DISCHARGE_AC], + eRetCapTrackDischargeAC[y]==vRETCAPTRACKDISCHARGEAC[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, + cRetCapTrackDischargeAC[y in RET_CAP_DISCHARGE_AC, p = 1:(cur_stage - 1)], + vRETCAPTRACKDISCHARGEAC[y, p]==0) + + @constraint(EP, + cLifetimeRetDischargeAC[y in RET_CAP_DISCHARGE_AC], + eNewCapTrackDischargeAC[y] + + eMinRetCapTrackDischargeAC[y]<=eRetCapTrackDischargeAC[y]) end -function endogenous_retirement_vre_stor_charge_ac!(EP::Model, inputs::Dict, num_stages::Int, cur_stage::Int, stage_lens::Array{Int, 1}) - - println("Endogenous Retirement (VRE-Storage Charge AC) Module") - - gen = inputs["RESOURCES"] - NEW_CAP_CHARGE_AC = inputs["NEW_CAP_CHARGE_AC"] # Set of all resources eligible for new capacity - RET_CAP_CHARGE_AC = inputs["RET_CAP_CHARGE_AC"] # Set of all resources eligible for capacity retirements - - ### Variables ### - - # Keep track of all new and retired capacity from all stages - @variable(EP, vCAPTRACKCHARGEAC[y in RET_CAP_CHARGE_AC,p=1:num_stages] >= 0 ) - @variable(EP, vRETCAPTRACKCHARGEAC[y in RET_CAP_CHARGE_AC,p=1:num_stages] >= 0 ) - - ### Expressions ### - - @expression(EP, eNewCapChargeAC[y in RET_CAP_CHARGE_AC], - if y in NEW_CAP_CHARGE_AC - EP[:vCAPCHARGE_AC][y] - else - EP[:vZERO] - end - ) - - @expression(EP, eRetCapChargeAC[y in RET_CAP_CHARGE_AC], EP[:vRETCAPCHARGE_AC][y]) - - # Construct and add the endogenous retirement constraint expressions - @expression(EP, eRetCapTrackChargeAC[y in RET_CAP_CHARGE_AC], sum(EP[:vRETCAPTRACKCHARGEAC][y,p] for p=1:cur_stage)) - @expression(EP, eNewCapTrackChargeAC[y in RET_CAP_CHARGE_AC], sum(EP[:vCAPTRACKCHARGEAC][y,p] for p=1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) - @expression(EP, eMinRetCapTrackChargeAC[y in RET_CAP_CHARGE_AC], cum_min_retired_cap_charge_ac_mw(gen[y])) - - ### Constraints ### - - # Keep track of newly built capacity from previous stages - @constraint(EP, cCapTrackNewChargeAC[y in RET_CAP_CHARGE_AC], eNewCapChargeAC[y] == vCAPTRACKCHARGEAC[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cCapTrackChargeAC[y in RET_CAP_CHARGE_AC,p=1:(cur_stage-1)], vCAPTRACKCHARGEAC[y,p] == 0) - - # Keep track of retired capacity from previous stages - @constraint(EP, cRetCapTrackNewChargeAC[y in RET_CAP_CHARGE_AC], eRetCapTrackChargeAC[y] == vRETCAPTRACKCHARGEAC[y,cur_stage]) - # The RHS of this constraint will be updated in the forward pass - @constraint(EP, cRetCapTrackChargeAC[y in RET_CAP_CHARGE_AC,p=1:(cur_stage-1)], vRETCAPTRACKCHARGEAC[y,p] == 0) - - @constraint(EP, cLifetimeRetChargeAC[y in RET_CAP_CHARGE_AC], eNewCapTrackChargeAC[y] + eMinRetCapTrackChargeAC[y] <= eRetCapTrackChargeAC[y]) +function endogenous_retirement_vre_stor_charge_ac!(EP::Model, + inputs::Dict, + num_stages::Int, + cur_stage::Int, + stage_lens::Array{Int, 1}) + println("Endogenous Retirement (VRE-Storage Charge AC) Module") + + gen = inputs["RESOURCES"] + NEW_CAP_CHARGE_AC = inputs["NEW_CAP_CHARGE_AC"] # Set of all resources eligible for new capacity + RET_CAP_CHARGE_AC = inputs["RET_CAP_CHARGE_AC"] # Set of all resources eligible for capacity retirements + + ### Variables ### + + # Keep track of all new and retired capacity from all stages + @variable(EP, vCAPTRACKCHARGEAC[y in RET_CAP_CHARGE_AC, p = 1:num_stages]>=0) + @variable(EP, vRETCAPTRACKCHARGEAC[y in RET_CAP_CHARGE_AC, p = 1:num_stages]>=0) + + ### Expressions ### + + @expression(EP, eNewCapChargeAC[y in RET_CAP_CHARGE_AC], + if y in NEW_CAP_CHARGE_AC + EP[:vCAPCHARGE_AC][y] + else + EP[:vZERO] + end) + + @expression(EP, eRetCapChargeAC[y in RET_CAP_CHARGE_AC], EP[:vRETCAPCHARGE_AC][y]) + + # Construct and add the endogenous retirement constraint expressions + @expression(EP, + eRetCapTrackChargeAC[y in RET_CAP_CHARGE_AC], + sum(EP[:vRETCAPTRACKCHARGEAC][y, p] for p in 1:cur_stage)) + @expression(EP, + eNewCapTrackChargeAC[y in RET_CAP_CHARGE_AC], + sum(EP[:vCAPTRACKCHARGEAC][y, p] + for p in 1:get_retirement_stage(cur_stage, lifetime(gen[y]), stage_lens))) + @expression(EP, + eMinRetCapTrackChargeAC[y in RET_CAP_CHARGE_AC], + cum_min_retired_cap_charge_ac_mw(gen[y])) + + ### Constraints ### + + # Keep track of newly built capacity from previous stages + @constraint(EP, + cCapTrackNewChargeAC[y in RET_CAP_CHARGE_AC], + eNewCapChargeAC[y]==vCAPTRACKCHARGEAC[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, + cCapTrackChargeAC[y in RET_CAP_CHARGE_AC, p = 1:(cur_stage - 1)], + vCAPTRACKCHARGEAC[y, p]==0) + + # Keep track of retired capacity from previous stages + @constraint(EP, + cRetCapTrackNewChargeAC[y in RET_CAP_CHARGE_AC], + eRetCapTrackChargeAC[y]==vRETCAPTRACKCHARGEAC[y, cur_stage]) + # The RHS of this constraint will be updated in the forward pass + @constraint(EP, + cRetCapTrackChargeAC[y in RET_CAP_CHARGE_AC, p = 1:(cur_stage - 1)], + vRETCAPTRACKCHARGEAC[y, p]==0) + + @constraint(EP, + cLifetimeRetChargeAC[y in RET_CAP_CHARGE_AC], + eNewCapTrackChargeAC[y] + eMinRetCapTrackChargeAC[y]<=eRetCapTrackChargeAC[y]) end diff --git a/src/multi_stage/write_multi_stage_capacities_charge.jl b/src/multi_stage/write_multi_stage_capacities_charge.jl index b098cae598..a9d7f4cf11 100644 --- a/src/multi_stage/write_multi_stage_capacities_charge.jl +++ b/src/multi_stage/write_multi_stage_capacities_charge.jl @@ -9,7 +9,6 @@ inputs: * settings\_d - Dictionary containing settings dictionary configured in the multi-stage settings file multi\_stage\_settings.yml. """ function write_multi_stage_capacities_charge(outpath::String, settings_d::Dict) - num_stages = settings_d["NumStages"] # Total number of investment planning stages capacities_d = Dict() @@ -19,7 +18,8 @@ function write_multi_stage_capacities_charge(outpath::String, settings_d::Dict) end # Set first column of DataFrame as resource names from the first stage - df_cap = DataFrame(Resource=capacities_d[1][!, :Resource], Zone=capacities_d[1][!, :Zone]) + df_cap = DataFrame(Resource = capacities_d[1][!, :Resource], + Zone = capacities_d[1][!, :Zone]) # Store starting capacities from the first stage df_cap[!, Symbol("StartChargeCap_p1")] = capacities_d[1][!, :StartChargeCap] @@ -30,5 +30,4 @@ function write_multi_stage_capacities_charge(outpath::String, settings_d::Dict) end CSV.write(joinpath(outpath, "capacities_charge_multi_stage.csv"), df_cap) - end diff --git a/src/multi_stage/write_multi_stage_capacities_discharge.jl b/src/multi_stage/write_multi_stage_capacities_discharge.jl index b4a84f433f..0da02b7002 100644 --- a/src/multi_stage/write_multi_stage_capacities_discharge.jl +++ b/src/multi_stage/write_multi_stage_capacities_discharge.jl @@ -9,7 +9,6 @@ inputs: * settings\_d - Dictionary containing settings dictionary configured in the multi-stage settings file multi\_stage\_settings.yml. """ function write_multi_stage_capacities_discharge(outpath::String, settings_d::Dict) - num_stages = settings_d["NumStages"] # Total number of investment planning stages capacities_d = Dict() @@ -19,7 +18,8 @@ function write_multi_stage_capacities_discharge(outpath::String, settings_d::Dic end # Set first column of DataFrame as resource names from the first stage - df_cap = DataFrame(Resource=capacities_d[1][!, :Resource], Zone=capacities_d[1][!, :Zone]) + df_cap = DataFrame(Resource = capacities_d[1][!, :Resource], + Zone = capacities_d[1][!, :Zone]) # Store starting capacities from the first stage df_cap[!, Symbol("StartCap_p1")] = capacities_d[1][!, :StartCap] @@ -30,5 +30,4 @@ function write_multi_stage_capacities_discharge(outpath::String, settings_d::Dic end CSV.write(joinpath(outpath, "capacities_multi_stage.csv"), df_cap) - end diff --git a/src/multi_stage/write_multi_stage_capacities_energy.jl b/src/multi_stage/write_multi_stage_capacities_energy.jl index b9d2d81849..9c7a5c1567 100644 --- a/src/multi_stage/write_multi_stage_capacities_energy.jl +++ b/src/multi_stage/write_multi_stage_capacities_energy.jl @@ -9,7 +9,6 @@ inputs: * settings\_d - Dictionary containing settings dictionary configured in the multi-stage settings file multi\_stage\_settings.yml. """ function write_multi_stage_capacities_energy(outpath::String, settings_d::Dict) - num_stages = settings_d["NumStages"] # Total number of investment planning stages capacities_d = Dict() @@ -19,7 +18,8 @@ function write_multi_stage_capacities_energy(outpath::String, settings_d::Dict) end # Set first column of DataFrame as resource names from the first stage - df_cap = DataFrame(Resource=capacities_d[1][!, :Resource], Zone=capacities_d[1][!, :Zone]) + df_cap = DataFrame(Resource = capacities_d[1][!, :Resource], + Zone = capacities_d[1][!, :Zone]) # Store starting capacities from the first stage df_cap[!, Symbol("StartEnergyCap_p1")] = capacities_d[1][!, :StartEnergyCap] @@ -30,5 +30,4 @@ function write_multi_stage_capacities_energy(outpath::String, settings_d::Dict) end CSV.write(joinpath(outpath, "capacities_energy_multi_stage.csv"), df_cap) - end diff --git a/src/multi_stage/write_multi_stage_costs.jl b/src/multi_stage/write_multi_stage_costs.jl index dcc5533f27..0c229a8d35 100644 --- a/src/multi_stage/write_multi_stage_costs.jl +++ b/src/multi_stage/write_multi_stage_costs.jl @@ -9,7 +9,6 @@ inputs: * settings\_d - Dictionary containing settings dictionary configured in the multi-stage settings file multi\_stage\_settings.yml. """ function write_multi_stage_costs(outpath::String, settings_d::Dict, inputs_dict::Dict) - num_stages = settings_d["NumStages"] # Total number of DDP stages wacc = settings_d["WACC"] # Interest Rate and also the discount rate unless specified other wise stage_lens = settings_d["StageLengths"] @@ -24,7 +23,7 @@ function write_multi_stage_costs(outpath::String, settings_d::Dict, inputs_dict: OPEXMULTS = [inputs_dict[j]["OPEXMULT"] for j in 1:num_stages] # Stage-wise OPEX multipliers to count multiple years between two model stages # Set first column of DataFrame as resource names from the first stage - df_costs = DataFrame(Costs=costs_d[1][!, :Costs]) + df_costs = DataFrame(Costs = costs_d[1][!, :Costs]) # Store discounted total costs for each stage in a data frame for p in 1:num_stages @@ -39,13 +38,14 @@ function write_multi_stage_costs(outpath::String, settings_d::Dict, inputs_dict: # For OPEX costs, apply additional discounting for cost in ["cVar", "cNSE", "cStart", "cUnmetRsv"] if cost in df_costs[!, :Costs] - df_costs[df_costs[!, :Costs].==cost, 2:end] = transpose(OPEXMULTS) .* df_costs[df_costs[!, :Costs].==cost, 2:end] + df_costs[df_costs[!, :Costs] .== cost, 2:end] = transpose(OPEXMULTS) .* + df_costs[df_costs[!, :Costs] .== cost, + 2:end] end end # Remove "cTotal" from results (as this includes Cost-to-Go) - df_costs = df_costs[df_costs[!, :Costs].!="cTotal", :] + df_costs = df_costs[df_costs[!, :Costs] .!= "cTotal", :] CSV.write(joinpath(outpath, "costs_multi_stage.csv"), df_costs) - end diff --git a/src/multi_stage/write_multi_stage_network_expansion.jl b/src/multi_stage/write_multi_stage_network_expansion.jl index 3b9808d29f..1a6ddc7015 100644 --- a/src/multi_stage/write_multi_stage_network_expansion.jl +++ b/src/multi_stage/write_multi_stage_network_expansion.jl @@ -19,11 +19,12 @@ function write_multi_stage_network_expansion(outpath::String, settings_d::Dict) end # Set first column of output DataFrame as line IDs - df_trans_cap = DataFrame(Line=trans_capacities_d[1][!, :Line]) + df_trans_cap = DataFrame(Line = trans_capacities_d[1][!, :Line]) # Store new transmission capacities for all stages for p in 1:num_stages - df_trans_cap[!, Symbol("New_Trans_Capacity_p$p")] = trans_capacities_d[p][!, :New_Trans_Capacity] + df_trans_cap[!, Symbol("New_Trans_Capacity_p$p")] = trans_capacities_d[p][!, + :New_Trans_Capacity] end CSV.write(joinpath(outpath, "network_expansion_multi_stage.csv"), df_trans_cap) diff --git a/src/multi_stage/write_multi_stage_stats.jl b/src/multi_stage/write_multi_stage_stats.jl index 75919d067f..b0c089a9d0 100644 --- a/src/multi_stage/write_multi_stage_stats.jl +++ b/src/multi_stage/write_multi_stage_stats.jl @@ -9,7 +9,6 @@ inputs: * stats\_d – Dictionary which contains the run time, upper bound, and lower bound of each DDP iteration. """ function write_multi_stage_stats(outpath::String, stats_d::Dict) - times_a = stats_d["TIMES"] # Time (seconds) of each iteration upper_bounds_a = stats_d["UPPER_BOUNDS"] # Upper bound of each iteration lower_bounds_a = stats_d["LOWER_BOUNDS"] # Lower bound of each iteration @@ -20,12 +19,11 @@ function write_multi_stage_stats(outpath::String, stats_d::Dict) realtive_gap_a = (upper_bounds_a .- lower_bounds_a) ./ lower_bounds_a # Construct dataframe where first column is iteration number, second is iteration time - df_stats = DataFrame(Iteration_Number=iteration_count_a, - Seconds=times_a, - Upper_Bound=upper_bounds_a, - Lower_Bound=lower_bounds_a, - Relative_Gap=realtive_gap_a) + df_stats = DataFrame(Iteration_Number = iteration_count_a, + Seconds = times_a, + Upper_Bound = upper_bounds_a, + Lower_Bound = lower_bounds_a, + Relative_Gap = realtive_gap_a) CSV.write(joinpath(outpath, "stats_multi_stage.csv"), df_stats) - end diff --git a/src/time_domain_reduction/precluster.jl b/src/time_domain_reduction/precluster.jl index b4ddb4df76..1d7352b8d4 100644 --- a/src/time_domain_reduction/precluster.jl +++ b/src/time_domain_reduction/precluster.jl @@ -45,4 +45,4 @@ function run_timedomainreduction_multistage!(case::AbstractString) end return -end \ No newline at end of file +end diff --git a/src/time_domain_reduction/time_domain_reduction.jl b/src/time_domain_reduction/time_domain_reduction.jl index ca6b25ac6b..71b39419b3 100644 --- a/src/time_domain_reduction/time_domain_reduction.jl +++ b/src/time_domain_reduction/time_domain_reduction.jl @@ -18,7 +18,6 @@ using Distances using CSV using GenX - const SEED = 1234 @doc raw""" @@ -51,8 +50,9 @@ function parse_data(myinputs) ZONES = myinputs["R_ZONES"] # DEMAND - Demand_data.csv - demand_profiles = [ myinputs["pD"][:,l] for l in 1:size(myinputs["pD"],2) ] - demand_col_names = [DEMAND_COLUMN_PREFIX()*string(l) for l in 1:size(demand_profiles)[1]] + demand_profiles = [myinputs["pD"][:, l] for l in 1:size(myinputs["pD"], 2)] + demand_col_names = [DEMAND_COLUMN_PREFIX() * string(l) + for l in 1:size(demand_profiles)[1]] demand_zones = [l for l in 1:size(demand_profiles)[1]] col_to_zone_map = Dict(demand_col_names .=> 1:length(demand_col_names)) @@ -64,15 +64,18 @@ function parse_data(myinputs) wind_col_names = [] var_col_names = [] for r in 1:length(RESOURCE_ZONES) - if occursin("PV", RESOURCE_ZONES[r]) || occursin("pv", RESOURCE_ZONES[r]) || occursin("Pv", RESOURCE_ZONES[r]) || occursin("Solar", RESOURCE_ZONES[r]) || occursin("SOLAR", RESOURCE_ZONES[r]) || occursin("solar", RESOURCE_ZONES[r]) + if occursin("PV", RESOURCE_ZONES[r]) || occursin("pv", RESOURCE_ZONES[r]) || + occursin("Pv", RESOURCE_ZONES[r]) || occursin("Solar", RESOURCE_ZONES[r]) || + occursin("SOLAR", RESOURCE_ZONES[r]) || occursin("solar", RESOURCE_ZONES[r]) push!(solar_col_names, RESOURCE_ZONES[r]) - push!(solar_profiles, myinputs["pP_Max"][r,:]) - elseif occursin("Wind", RESOURCE_ZONES[r]) || occursin("WIND", RESOURCE_ZONES[r]) || occursin("wind", RESOURCE_ZONES[r]) + push!(solar_profiles, myinputs["pP_Max"][r, :]) + elseif occursin("Wind", RESOURCE_ZONES[r]) || occursin("WIND", RESOURCE_ZONES[r]) || + occursin("wind", RESOURCE_ZONES[r]) push!(wind_col_names, RESOURCE_ZONES[r]) - push!(wind_profiles, myinputs["pP_Max"][r,:]) + push!(wind_profiles, myinputs["pP_Max"][r, :]) end push!(var_col_names, RESOURCE_ZONES[r]) - push!(var_profiles, myinputs["pP_Max"][r,:]) + push!(var_profiles, myinputs["pP_Max"][r, :]) col_to_zone_map[RESOURCE_ZONES[r]] = ZONES[r] end @@ -82,15 +85,18 @@ function parse_data(myinputs) AllFuelsConst = true for f in 1:length(fuel_col_names) push!(fuel_profiles, myinputs["fuel_costs"][fuel_col_names[f]]) - if AllFuelsConst && (minimum(myinputs["fuel_costs"][fuel_col_names[f]]) != maximum(myinputs["fuel_costs"][fuel_col_names[f]])) + if AllFuelsConst && (minimum(myinputs["fuel_costs"][fuel_col_names[f]]) != + maximum(myinputs["fuel_costs"][fuel_col_names[f]])) AllFuelsConst = false end end all_col_names = [demand_col_names; var_col_names; fuel_col_names] all_profiles = [demand_profiles..., var_profiles..., fuel_profiles...] - return demand_col_names, var_col_names, solar_col_names, wind_col_names, fuel_col_names, all_col_names, - demand_profiles, var_profiles, solar_profiles, wind_profiles, fuel_profiles, all_profiles, - col_to_zone_map, AllFuelsConst + return demand_col_names, var_col_names, solar_col_names, wind_col_names, fuel_col_names, + all_col_names, + demand_profiles, var_profiles, solar_profiles, wind_profiles, fuel_profiles, + all_profiles, + col_to_zone_map, AllFuelsConst end @doc raw""" @@ -113,39 +119,46 @@ function parse_multi_stage_data(inputs_dict) # [ REPLACE THIS with multi_stage_settings.yml StageLengths ] # In case not all stages have the same length, check relative lengths - stage_lengths = [ size(inputs_dict[t]["pD"][:,1],1) for t in 1:length(keys(inputs_dict)) ] + stage_lengths = [size(inputs_dict[t]["pD"][:, 1], 1) + for t in 1:length(keys(inputs_dict))] total_length = sum(stage_lengths) - relative_lengths = stage_lengths/total_length + relative_lengths = stage_lengths / total_length # DEMAND - Demand_data.csv - stage_demand_profiles = [ inputs_dict[t]["pD"][:,l] for t in 1:length(keys(inputs_dict)), l in 1:size(inputs_dict[1]["pD"],2) ] - vector_lps = [stage_demand_profiles[:,l] for l in 1:size(inputs_dict[1]["pD"],2)] - demand_profiles = [reduce(vcat,vector_lps[l]) for l in 1:size(inputs_dict[1]["pD"],2)] - demand_col_names = [DEMAND_COLUMN_PREFIX()*string(l) for l in 1:size(demand_profiles)[1]] + stage_demand_profiles = [inputs_dict[t]["pD"][:, l] + for t in 1:length(keys(inputs_dict)), + l in 1:size(inputs_dict[1]["pD"], 2)] + vector_lps = [stage_demand_profiles[:, l] for l in 1:size(inputs_dict[1]["pD"], 2)] + demand_profiles = [reduce(vcat, vector_lps[l]) for l in 1:size(inputs_dict[1]["pD"], 2)] + demand_col_names = [DEMAND_COLUMN_PREFIX() * string(l) + for l in 1:size(demand_profiles)[1]] demand_zones = [l for l in 1:size(demand_profiles)[1]] col_to_zone_map = Dict(demand_col_names .=> 1:length(demand_col_names)) # CAPACITY FACTORS - Generators_variability.csv for r in 1:length(RESOURCE_ZONES) - if occursin("PV", RESOURCE_ZONES[r]) || occursin("pv", RESOURCE_ZONES[r]) || occursin("Pv", RESOURCE_ZONES[r]) || occursin("Solar", RESOURCE_ZONES[r]) || occursin("SOLAR", RESOURCE_ZONES[r]) || occursin("solar", RESOURCE_ZONES[r]) + if occursin("PV", RESOURCE_ZONES[r]) || occursin("pv", RESOURCE_ZONES[r]) || + occursin("Pv", RESOURCE_ZONES[r]) || occursin("Solar", RESOURCE_ZONES[r]) || + occursin("SOLAR", RESOURCE_ZONES[r]) || occursin("solar", RESOURCE_ZONES[r]) push!(solar_col_names, RESOURCE_ZONES[r]) pv_all_stages = [] for t in 1:length(keys(inputs_dict)) - pv_all_stages = vcat(pv_all_stages, inputs_dict[t]["pP_Max"][r,:]) + pv_all_stages = vcat(pv_all_stages, inputs_dict[t]["pP_Max"][r, :]) end push!(solar_profiles, pv_all_stages) - elseif occursin("Wind", RESOURCE_ZONES[r]) || occursin("WIND", RESOURCE_ZONES[r]) || occursin("wind", RESOURCE_ZONES[r]) + elseif occursin("Wind", RESOURCE_ZONES[r]) || occursin("WIND", RESOURCE_ZONES[r]) || + occursin("wind", RESOURCE_ZONES[r]) push!(wind_col_names, RESOURCE_ZONES[r]) wind_all_stages = [] for t in 1:length(keys(inputs_dict)) - wind_all_stages = vcat(wind_all_stages, inputs_dict[t]["pP_Max"][r,:]) + wind_all_stages = vcat(wind_all_stages, inputs_dict[t]["pP_Max"][r, :]) end push!(wind_profiles, wind_all_stages) end push!(var_col_names, RESOURCE_ZONES[r]) var_all_stages = [] for t in 1:length(keys(inputs_dict)) - var_all_stages = vcat(var_all_stages, inputs_dict[t]["pP_Max"][r,:]) + var_all_stages = vcat(var_all_stages, inputs_dict[t]["pP_Max"][r, :]) end push!(var_profiles, var_all_stages) col_to_zone_map[RESOURCE_ZONES[r]] = ZONES[r] @@ -158,8 +171,10 @@ function parse_multi_stage_data(inputs_dict) for f in 1:length(fuel_col_names) fuel_all_stages = [] for t in 1:length(keys(inputs_dict)) - fuel_all_stages = vcat(fuel_all_stages, inputs_dict[t]["fuel_costs"][fuel_col_names[f]]) - if AllFuelsConst && (minimum(inputs_dict[t]["fuel_costs"][fuel_col_names[f]]) != maximum(inputs_dict[t]["fuel_costs"][fuel_col_names[f]])) + fuel_all_stages = vcat(fuel_all_stages, + inputs_dict[t]["fuel_costs"][fuel_col_names[f]]) + if AllFuelsConst && (minimum(inputs_dict[t]["fuel_costs"][fuel_col_names[f]]) != + maximum(inputs_dict[t]["fuel_costs"][fuel_col_names[f]])) AllFuelsConst = false end end @@ -168,9 +183,11 @@ function parse_multi_stage_data(inputs_dict) all_col_names = [demand_col_names; var_col_names; fuel_col_names] all_profiles = [demand_profiles..., var_profiles..., fuel_profiles...] - return demand_col_names, var_col_names, solar_col_names, wind_col_names, fuel_col_names, all_col_names, - demand_profiles, var_profiles, solar_profiles, wind_profiles, fuel_profiles, all_profiles, - col_to_zone_map, AllFuelsConst, stage_lengths, total_length, relative_lengths + return demand_col_names, var_col_names, solar_col_names, wind_col_names, fuel_col_names, + all_col_names, + demand_profiles, var_profiles, solar_profiles, wind_profiles, fuel_profiles, + all_profiles, + col_to_zone_map, AllFuelsConst, stage_lengths, total_length, relative_lengths end @doc raw""" @@ -184,13 +201,16 @@ representation is within a given proportion of the "maximum" possible deviation. """ function check_condition(Threshold, R, OldColNames, ScalingMethod, TimestepsPerRepPeriod) if ScalingMethod == "N" - return maximum(R.costs)/(length(OldColNames)*TimestepsPerRepPeriod) < Threshold + return maximum(R.costs) / (length(OldColNames) * TimestepsPerRepPeriod) < Threshold elseif ScalingMethod == "S" - return maximum(R.costs)/(length(OldColNames)*TimestepsPerRepPeriod*4) < Threshold + return maximum(R.costs) / (length(OldColNames) * TimestepsPerRepPeriod * 4) < + Threshold else - println("INVALID Scaling Method ", ScalingMethod, " / Choose N for Normalization or S for Standardization. Proceeding with N.") + println("INVALID Scaling Method ", + ScalingMethod, + " / Choose N for Normalization or S for Standardization. Proceeding with N.") end - return maximum(R.costs)/(length(OldColNames)*TimestepsPerRepPeriod) < Threshold + return maximum(R.costs) / (length(OldColNames) * TimestepsPerRepPeriod) < Threshold end @doc raw""" @@ -213,20 +233,28 @@ K-Means: [https://juliastats.org/Clustering.jl/dev/kmeans.html](https://juliasta K-Medoids: [https://juliastats.org/Clustering.jl/stable/kmedoids.html](https://juliastats.org/Clustering.jl/stable/kmedoids.html) """ -function cluster(ClusterMethod, ClusteringInputDF, NClusters, nIters, v=false, random=true) +function cluster(ClusterMethod, + ClusteringInputDF, + NClusters, + nIters, + v = false, + random = true) if ClusterMethod == "kmeans" - DistMatrix = pairwise(Euclidean(), Matrix(ClusteringInputDF), dims=2) - R = kmeans(Matrix(ClusteringInputDF), NClusters, init=:kmcen) + DistMatrix = pairwise(Euclidean(), Matrix(ClusteringInputDF), dims = 2) + R = kmeans(Matrix(ClusteringInputDF), NClusters, init = :kmcen) for i in 1:nIters - if !random; Random.seed!(SEED); end + if !random + Random.seed!(SEED) + end R_i = kmeans(Matrix(ClusteringInputDF), NClusters) if R_i.totalcost < R.totalcost R = R_i end - if v && (i % (nIters/10) == 0) - println(string(i) * " : " * string(round(R_i.totalcost, digits=3)) * " " * string(round(R.totalcost, digits=3)) ) + if v && (i % (nIters / 10) == 0) + println(string(i) * " : " * string(round(R_i.totalcost, digits = 3)) * " " * + string(round(R.totalcost, digits = 3))) end end @@ -236,22 +264,26 @@ function cluster(ClusterMethod, ClusteringInputDF, NClusters, nIters, v=false, r M = [] for i in 1:NClusters - dists = [euclidean(Centers[:,i], ClusteringInputDF[!, j]) for j in 1:size(ClusteringInputDF, 2)] - push!(M,argmin(dists)) + dists = [euclidean(Centers[:, i], ClusteringInputDF[!, j]) + for j in 1:size(ClusteringInputDF, 2)] + push!(M, argmin(dists)) end elseif ClusterMethod == "kmedoids" - DistMatrix = pairwise(Euclidean(), Matrix(ClusteringInputDF), dims=2) - R = kmedoids(DistMatrix, NClusters, init=:kmcen) + DistMatrix = pairwise(Euclidean(), Matrix(ClusteringInputDF), dims = 2) + R = kmedoids(DistMatrix, NClusters, init = :kmcen) for i in 1:nIters - if !random; Random.seed!(SEED); end + if !random + Random.seed!(SEED) + end R_i = kmedoids(DistMatrix, NClusters) if R_i.totalcost < R.totalcost R = R_i end - if v && (i % (nIters/10) == 0) - println(string(i) * " : " * string(round(R_i.totalcost, digits=3)) * " " * string(round(R.totalcost, digits=3)) ) + if v && (i % (nIters / 10) == 0) + println(string(i) * " : " * string(round(R_i.totalcost, digits = 3)) * " " * + string(round(R.totalcost, digits = 3))) end end @@ -271,14 +303,16 @@ end Remove and store the columns that do not vary during the period. """ -function RemoveConstCols(all_profiles, all_col_names, v=false) +function RemoveConstCols(all_profiles, all_col_names, v = false) ConstData = [] ConstIdx = [] ConstCols = [] for c in 1:length(all_col_names) Const = minimum(all_profiles[c]) == maximum(all_profiles[c]) if Const - if v println("Removing constant col: ", all_col_names[c]) end + if v + println("Removing constant col: ", all_col_names[c]) + end push!(ConstData, all_profiles[c]) push!(ConstCols, all_col_names[c]) push!(ConstIdx, c) @@ -304,37 +338,59 @@ system to be included among the extreme periods. They would select """ function get_extreme_period(DF, GDF, profKey, typeKey, statKey, - ConstCols, demand_col_names, solar_col_names, wind_col_names, v=false) - if v println(profKey," ", typeKey," ", statKey) end + ConstCols, demand_col_names, solar_col_names, wind_col_names, v = false) + if v + println(profKey, " ", typeKey, " ", statKey) + end if typeKey == "Integral" if profKey == "Demand" - (stat, group_idx) = get_integral_extreme(GDF, statKey, demand_col_names, ConstCols) + (stat, group_idx) = get_integral_extreme(GDF, + statKey, + demand_col_names, + ConstCols) elseif profKey == "PV" - (stat, group_idx) = get_integral_extreme(GDF, statKey, solar_col_names, ConstCols) + (stat, group_idx) = get_integral_extreme(GDF, + statKey, + solar_col_names, + ConstCols) elseif profKey == "Wind" - (stat, group_idx) = get_integral_extreme(GDF, statKey, wind_col_names, ConstCols) + (stat, group_idx) = get_integral_extreme(GDF, + statKey, + wind_col_names, + ConstCols) else - println("Error: Profile Key ", profKey, " is invalid. Choose `Demand', `PV' or `Wind'.") + println("Error: Profile Key ", + profKey, + " is invalid. Choose `Demand', `PV' or `Wind'.") end elseif typeKey == "Absolute" if profKey == "Demand" - (stat, group_idx) = get_absolute_extreme(DF, statKey, demand_col_names, ConstCols) + (stat, group_idx) = get_absolute_extreme(DF, + statKey, + demand_col_names, + ConstCols) elseif profKey == "PV" - (stat, group_idx) = get_absolute_extreme(DF, statKey, solar_col_names, ConstCols) + (stat, group_idx) = get_absolute_extreme(DF, + statKey, + solar_col_names, + ConstCols) elseif profKey == "Wind" (stat, group_idx) = get_absolute_extreme(DF, statKey, wind_col_names, ConstCols) else - println("Error: Profile Key ", profKey, " is invalid. Choose `Demand', `PV' or `Wind'.") + println("Error: Profile Key ", + profKey, + " is invalid. Choose `Demand', `PV' or `Wind'.") end - else - println("Error: Type Key ", typeKey, " is invalid. Choose `Absolute' or `Integral'.") - stat = 0 - group_idx = 0 - end + else + println("Error: Type Key ", + typeKey, + " is invalid. Choose `Absolute' or `Integral'.") + stat = 0 + group_idx = 0 + end return (stat, group_idx) end - @doc raw""" get_integral_extreme(GDF, statKey, col_names, ConstCols) @@ -345,9 +401,11 @@ summed over the period. """ function get_integral_extreme(GDF, statKey, col_names, ConstCols) if statKey == "Max" - (stat, stat_idx) = findmax( sum([GDF[!, Symbol(c)] for c in setdiff(col_names, ConstCols) ]) ) + (stat, stat_idx) = findmax(sum([GDF[!, Symbol(c)] + for c in setdiff(col_names, ConstCols)])) elseif statKey == "Min" - (stat, stat_idx) = findmin( sum([GDF[!, Symbol(c)] for c in setdiff(col_names, ConstCols) ]) ) + (stat, stat_idx) = findmin(sum([GDF[!, Symbol(c)] + for c in setdiff(col_names, ConstCols)])) else println("Error: Statistic Key ", statKey, " is invalid. Choose `Max' or `Min'.") end @@ -363,10 +421,12 @@ Get the period index of the single timestep with the minimum or maximum demand o """ function get_absolute_extreme(DF, statKey, col_names, ConstCols) if statKey == "Max" - (stat, stat_idx) = findmax( sum([DF[!, Symbol(c)] for c in setdiff(col_names, ConstCols) ]) ) + (stat, stat_idx) = findmax(sum([DF[!, Symbol(c)] + for c in setdiff(col_names, ConstCols)])) group_idx = DF.Group[stat_idx] elseif statKey == "Min" - (stat, stat_idx) = findmin( sum([DF[!, Symbol(c)] for c in setdiff(col_names, ConstCols) ]) ) + (stat, stat_idx) = findmin(sum([DF[!, Symbol(c)] + for c in setdiff(col_names, ConstCols)])) group_idx = DF.Group[stat_idx] else println("Error: Statistic Key ", statKey, " is invalid. Choose `Max' or `Min'.") @@ -374,7 +434,6 @@ function get_absolute_extreme(DF, statKey, col_names, ConstCols) return (stat, group_idx) end - @doc raw""" scale_weights(W, H) @@ -386,9 +445,11 @@ w_j \leftarrow H \cdot \frac{w_j}{\sum_i w_i} \: \: \: \forall w_j \in W ``` """ -function scale_weights(W, H, v=false) - if v println("Weights before scaling: ", W) end - W = [ float(w)/sum(W) * H for w in W] # Scale to number of hours in input data +function scale_weights(W, H, v = false) + if v + println("Weights before scaling: ", W) + end + W = [float(w) / sum(W) * H for w in W] # Scale to number of hours in input data if v println("Weights after scaling: ", W) println("Sum of Updated Cluster Weights: ", sum(W)) @@ -396,7 +457,6 @@ function scale_weights(W, H, v=false) return W end - @doc raw""" get_demand_multipliers(ClusterOutputData, ModifiedData, M, W, DemandCols, TimestepsPerRepPeriod, NewColNames, NClusters, Ncols) @@ -416,7 +476,16 @@ demand in timestep $i$ for representative period $m$ in zone $z$, $w_m$ is the w hours that one hour in representative period $m$ represents in the original profile, and $k_z$ is the zonal demand multiplier returned by the function. """ -function get_demand_multipliers(ClusterOutputData, InputData, M, W, DemandCols, TimestepsPerRepPeriod, NewColNames, NClusters, Ncols, v=false) +function get_demand_multipliers(ClusterOutputData, + InputData, + M, + W, + DemandCols, + TimestepsPerRepPeriod, + NewColNames, + NClusters, + Ncols, + v = false) # Compute original zonal total demands zone_sums = Dict() for demandcol in DemandCols @@ -426,7 +495,9 @@ function get_demand_multipliers(ClusterOutputData, InputData, M, W, DemandCols, # Compute zonal demands per representative period cluster_zone_sums = Dict() for m in 1:NClusters - clustered_lp_DF = DataFrame( Dict( NewColNames[i] => ClusterOutputData[!,m][TimestepsPerRepPeriod*(i-1)+1 : TimestepsPerRepPeriod*i] for i in 1:Ncols if (Symbol(NewColNames[i]) in DemandCols)) ) + clustered_lp_DF = DataFrame(Dict(NewColNames[i] => ClusterOutputData[!, m][(TimestepsPerRepPeriod * (i - 1) + 1):(TimestepsPerRepPeriod * i)] + for i in 1:Ncols + if (Symbol(NewColNames[i]) in DemandCols))) cluster_zone_sums[m] = Dict() for demandcol in DemandCols cluster_zone_sums[m][demandcol] = sum(clustered_lp_DF[:, demandcol]) @@ -439,10 +510,20 @@ function get_demand_multipliers(ClusterOutputData, InputData, M, W, DemandCols, demand_mults = Dict() for demandcol in DemandCols for m in 1:NClusters - weighted_cluster_zone_sums[demandcol] += (W[m]/(TimestepsPerRepPeriod))*cluster_zone_sums[m][demandcol] + weighted_cluster_zone_sums[demandcol] += (W[m] / (TimestepsPerRepPeriod)) * + cluster_zone_sums[m][demandcol] + end + demand_mults[demandcol] = zone_sums[demandcol] / + weighted_cluster_zone_sums[demandcol] + if v + println(demandcol, + ": ", + weighted_cluster_zone_sums[demandcol], + " vs. ", + zone_sums[demandcol], + " => ", + demand_mults[demandcol]) end - demand_mults[demandcol] = zone_sums[demandcol]/weighted_cluster_zone_sums[demandcol] - if v println(demandcol, ": ", weighted_cluster_zone_sums[demandcol], " vs. ", zone_sums[demandcol], " => ", demand_mults[demandcol]) end end # Zone-wise validation that scaled clustered demand equals original demand (Don't actually scale demand in this function) @@ -453,20 +534,34 @@ function get_demand_multipliers(ClusterOutputData, InputData, M, W, DemandCols, if (NewColNames[i] in DemandCols) # Uncomment this line if we decide to scale demand here instead of later. (Also remove "demand_mults[NewColNames[i]]*" term from new_zone_sums computation) #ClusterOutputData[!,m][TimestepsPerRepPeriod*(i-1)+1 : TimestepsPerRepPeriod*i] *= demand_mults[NewColNames[i]] - println(" Scaling ", M[m], " (", NewColNames[i], ") : ", cluster_zone_sums[m][NewColNames[i]], " => ", demand_mults[NewColNames[i]]*sum(ClusterOutputData[!,m][TimestepsPerRepPeriod*(i-1)+1 : TimestepsPerRepPeriod*i])) - new_zone_sums[NewColNames[i]] += (W[m]/(TimestepsPerRepPeriod))*demand_mults[NewColNames[i]]*sum(ClusterOutputData[!,m][TimestepsPerRepPeriod*(i-1)+1 : TimestepsPerRepPeriod*i]) + println(" Scaling ", + M[m], + " (", + NewColNames[i], + ") : ", + cluster_zone_sums[m][NewColNames[i]], + " => ", + demand_mults[NewColNames[i]] * + sum(ClusterOutputData[!, m][(TimestepsPerRepPeriod * (i - 1) + 1):(TimestepsPerRepPeriod * i)])) + new_zone_sums[NewColNames[i]] += (W[m] / (TimestepsPerRepPeriod)) * + demand_mults[NewColNames[i]] * + sum(ClusterOutputData[!, m][(TimestepsPerRepPeriod * (i - 1) + 1):(TimestepsPerRepPeriod * i)]) end end end for demandcol in DemandCols - println(demandcol, ": ", new_zone_sums[demandcol], " =?= ", zone_sums[demandcol]) + println(demandcol, + ": ", + new_zone_sums[demandcol], + " =?= ", + zone_sums[demandcol]) end end return demand_mults end -function update_deprecated_tdr_inputs!(setup::Dict{Any,Any}) +function update_deprecated_tdr_inputs!(setup::Dict{Any, Any}) if "LoadWeight" in keys(setup) setup["DemandWeight"] = setup["LoadWeight"] delete!(setup, "LoadWeight") @@ -479,14 +574,13 @@ function update_deprecated_tdr_inputs!(setup::Dict{Any,Any}) extr_dict = setup[extreme_periods] if "Load" in keys(extr_dict) - extr_dict["Demand"] = extr_dict["Load"] + extr_dict["Demand"] = extr_dict["Load"] delete!(extr_dict, "Load") - @info "In time_domain_reduction_settings file the key Load is deprecated. Prefer Demand." - end + @info "In time_domain_reduction_settings file the key Load is deprecated. Prefer Demand." + end end end - @doc raw""" cluster_inputs(inpath, settings_path, mysetup, stage_id=-99, v=false; random=true) @@ -541,13 +635,21 @@ to separate Vre_and_stor_solar_variability.csv and Vre_and_stor_wind_variability and wind profiles for co-located resources will be separated into different CSV files to be read by loading the inputs after the clustering of the inputs has occurred. """ -function cluster_inputs(inpath, settings_path, mysetup, stage_id=-99, v=false; random=true) - if v println(now()) end +function cluster_inputs(inpath, + settings_path, + mysetup, + stage_id = -99, + v = false; + random = true) + if v + println(now()) + end ##### Step 0: Load in settings and data # Read time domain reduction settings file time_domain_reduction_settings.yml - myTDRsetup = YAML.load(open(joinpath(settings_path,"time_domain_reduction_settings.yml"))) + myTDRsetup = YAML.load(open(joinpath(settings_path, + "time_domain_reduction_settings.yml"))) update_deprecated_tdr_inputs!(myTDRsetup) # Accept model parameters from the settings file time_domain_reduction_settings.yml @@ -582,46 +684,55 @@ function cluster_inputs(inpath, settings_path, mysetup, stage_id=-99, v=false; r # Define a local version of the setup so that you can modify the mysetup["ParameterScale"] value to be zero in case it is 1 mysetup_local = copy(mysetup) # If ParameterScale =1 then make it zero, since clustered inputs will be scaled prior to generating model - mysetup_local["ParameterScale"]=0 # Performing cluster and report outputs in user-provided units + mysetup_local["ParameterScale"] = 0 # Performing cluster and report outputs in user-provided units # Define another local version of setup such that Multi-Stage Non-Concatentation TDR can iteratively read in the raw data mysetup_MS = copy(mysetup) - mysetup_MS["TimeDomainReduction"]=0 - mysetup_MS["DoNotReadPeriodMap"]=1 - mysetup_MS["ParameterScale"]=0 + mysetup_MS["TimeDomainReduction"] = 0 + mysetup_MS["DoNotReadPeriodMap"] = 1 + mysetup_MS["ParameterScale"] = 0 if MultiStage == 1 - model_dict=Dict() - inputs_dict=Dict() + model_dict = Dict() + inputs_dict = Dict() for t in 1:NumStages - # Step 0) Set Model Year - mysetup["MultiStageSettingsDict"]["CurStage"] = t + # Step 0) Set Model Year + mysetup["MultiStageSettingsDict"]["CurStage"] = t - # Step 1) Load Inputs - global inpath_sub = string("$inpath/inputs/inputs_p",t) + # Step 1) Load Inputs + global inpath_sub = string("$inpath/inputs/inputs_p", t) # this prevents doubled time domain reduction in stages past # the first, even if the first stage is okay. - prevent_doubled_timedomainreduction(joinpath(inpath_sub, mysetup["SystemFolder"])) + prevent_doubled_timedomainreduction(joinpath(inpath_sub, + mysetup["SystemFolder"])) - inputs_dict[t] = load_inputs(mysetup_MS, inpath_sub) + inputs_dict[t] = load_inputs(mysetup_MS, inpath_sub) - inputs_dict[t] = configure_multi_stage_inputs(inputs_dict[t],mysetup["MultiStageSettingsDict"],mysetup["NetworkExpansion"]) + inputs_dict[t] = configure_multi_stage_inputs(inputs_dict[t], + mysetup["MultiStageSettingsDict"], + mysetup["NetworkExpansion"]) end if MultiStageConcatenate == 1 - if v println("MultiStage with Concatenation") end + if v + println("MultiStage with Concatenation") + end RESOURCE_ZONES = inputs_dict[1]["RESOURCE_ZONES"] RESOURCES = inputs_dict[1]["RESOURCE_NAMES"] ZONES = inputs_dict[1]["R_ZONES"] # Parse input data into useful structures divided by type (demand, wind, solar, fuel, groupings thereof, etc.) # TO DO LATER: Replace these with collections of col_names, profiles, zones demand_col_names, var_col_names, solar_col_names, wind_col_names, fuel_col_names, all_col_names, - demand_profiles, var_profiles, solar_profiles, wind_profiles, fuel_profiles, all_profiles, - col_to_zone_map, AllFuelsConst, stage_lengths, total_length, relative_lengths = parse_multi_stage_data(inputs_dict) + demand_profiles, var_profiles, solar_profiles, wind_profiles, fuel_profiles, all_profiles, + col_to_zone_map, AllFuelsConst, stage_lengths, total_length, relative_lengths = parse_multi_stage_data(inputs_dict) else # TDR each period individually - if v println("MultiStage without Concatenation") end - if v println("---> STAGE ", stage_id) end + if v + println("MultiStage without Concatenation") + end + if v + println("---> STAGE ", stage_id) + end myinputs = inputs_dict[stage_id] RESOURCE_ZONES = myinputs["RESOURCE_ZONES"] RESOURCES = myinputs["RESOURCE_NAMES"] @@ -629,32 +740,41 @@ function cluster_inputs(inpath, settings_path, mysetup, stage_id=-99, v=false; r # Parse input data into useful structures divided by type (demand, wind, solar, fuel, groupings thereof, etc.) # TO DO LATER: Replace these with collections of col_names, profiles, zones demand_col_names, var_col_names, solar_col_names, wind_col_names, fuel_col_names, all_col_names, - demand_profiles, var_profiles, solar_profiles, wind_profiles, fuel_profiles, all_profiles, - col_to_zone_map, AllFuelsConst = parse_data(myinputs) + demand_profiles, var_profiles, solar_profiles, wind_profiles, fuel_profiles, all_profiles, + col_to_zone_map, AllFuelsConst = parse_data(myinputs) end else - if v println("Not MultiStage") end - myinputs = load_inputs(mysetup_local,inpath) + if v + println("Not MultiStage") + end + myinputs = load_inputs(mysetup_local, inpath) RESOURCE_ZONES = myinputs["RESOURCE_ZONES"] RESOURCES = myinputs["RESOURCE_NAMES"] ZONES = myinputs["R_ZONES"] # Parse input data into useful structures divided by type (demand, wind, solar, fuel, groupings thereof, etc.) # TO DO LATER: Replace these with collections of col_names, profiles, zones demand_col_names, var_col_names, solar_col_names, wind_col_names, fuel_col_names, all_col_names, - demand_profiles, var_profiles, solar_profiles, wind_profiles, fuel_profiles, all_profiles, - col_to_zone_map, AllFuelsConst = parse_data(myinputs) + demand_profiles, var_profiles, solar_profiles, wind_profiles, fuel_profiles, all_profiles, + col_to_zone_map, AllFuelsConst = parse_data(myinputs) + end + if v + println() end - if v println() end # Remove Constant Columns - Add back later in final output - all_profiles, all_col_names, ConstData, ConstCols, ConstIdx = RemoveConstCols(all_profiles, all_col_names, v) + all_profiles, all_col_names, ConstData, ConstCols, ConstIdx = RemoveConstCols(all_profiles, + all_col_names, + v) # Determine whether or not to time domain reduce fuel profiles as well based on user choice and file structure (i.e., variable fuels in Fuels_data.csv) IncludeFuel = true - if (ClusterFuelPrices != 1) || (AllFuelsConst) IncludeFuel = false end + if (ClusterFuelPrices != 1) || (AllFuelsConst) + IncludeFuel = false + end # Put it together! - InputData = DataFrame( Dict( all_col_names[c]=>all_profiles[c] for c in 1:length(all_col_names) ) ) + InputData = DataFrame(Dict(all_col_names[c] => all_profiles[c] + for c in 1:length(all_col_names))) InputData = convert.(Float64, InputData) if v println("Demand (MW) and Capacity Factor Profiles: ") @@ -666,27 +786,37 @@ function cluster_inputs(inpath, settings_path, mysetup, stage_id=-99, v=false; r Nhours = nrow(InputData) # Timesteps Ncols = length(NewColNames) - 1 - ##### Step 1: Normalize or standardize all demand, renewables, and fuel data / optionally scale with DemandWeight # Normalize/standardize data based on user-provided method if ScalingMethod == "N" - normProfiles = [ StatsBase.transform(fit(UnitRangeTransform, InputData[:,c]; dims=1, unit=true), InputData[:,c]) for c in 1:length(OldColNames) ] + normProfiles = [StatsBase.transform(fit(UnitRangeTransform, + InputData[:, c]; + dims = 1, + unit = true), + InputData[:, c]) for c in 1:length(OldColNames)] elseif ScalingMethod == "S" - normProfiles = [ StatsBase.transform(fit(ZScoreTransform, InputData[:,c]; dims=1), InputData[:,c]) for c in 1:length(OldColNames) ] + normProfiles = [StatsBase.transform(fit(ZScoreTransform, InputData[:, c]; dims = 1), + InputData[:, c]) for c in 1:length(OldColNames)] else println("ERROR InvalidScalingMethod: Use N for Normalization or S for Standardization.") println("CONTINUING using 0->1 normalization...") - normProfiles = [ StatsBase.transform(fit(UnitRangeTransform, InputData[:,c]; dims=1, unit=true), InputData[:,c]) for c in 1:length(OldColNames) ] + normProfiles = [StatsBase.transform(fit(UnitRangeTransform, + InputData[:, c]; + dims = 1, + unit = true), + InputData[:, c]) for c in 1:length(OldColNames)] end # Compile newly normalized/standardized profiles - AnnualTSeriesNormalized = DataFrame(Dict( OldColNames[c] => normProfiles[c] for c in 1:length(OldColNames) )) + AnnualTSeriesNormalized = DataFrame(Dict(OldColNames[c] => normProfiles[c] + for c in 1:length(OldColNames))) # Optional pre-scaling of demand in order to give it more preference in clutering algorithm if DemandWeight != 1 # If we want to value demand more/less than capacity factors. Assume nonnegative. LW=1 means no scaling. for c in demand_col_names - AnnualTSeriesNormalized[!, Symbol(c)] .= AnnualTSeriesNormalized[!, Symbol(c)] .* DemandWeight + AnnualTSeriesNormalized[!, Symbol(c)] .= AnnualTSeriesNormalized[!, + Symbol(c)] .* DemandWeight end end @@ -696,121 +826,193 @@ function cluster_inputs(inpath, settings_path, mysetup, stage_id=-99, v=false; r println() end - ##### STEP 2: Identify extreme periods in the model, Reshape data for clustering # Total number of subperiods available in the dataset, where each subperiod length = TimestepsPerRepPeriod - NumDataPoints = Nhours÷TimestepsPerRepPeriod # 364 weeks in 7 years - if v println("Total Subperiods in the data set: ", NumDataPoints) end - InputData[:, :Group] .= (1:Nhours) .÷ (TimestepsPerRepPeriod+0.0001) .+ 1 # Group col identifies the subperiod ID of each hour (e.g., all hours in week 2 have Group=2 if using TimestepsPerRepPeriod=168) + NumDataPoints = Nhours ÷ TimestepsPerRepPeriod # 364 weeks in 7 years + if v + println("Total Subperiods in the data set: ", NumDataPoints) + end + InputData[:, :Group] .= (1:Nhours) .÷ (TimestepsPerRepPeriod + 0.0001) .+ 1 # Group col identifies the subperiod ID of each hour (e.g., all hours in week 2 have Group=2 if using TimestepsPerRepPeriod=168) # Group by period (e.g., week) cgdf = combine(groupby(InputData, :Group), [c .=> sum for c in OldColNames]) - cgdf = cgdf[setdiff(1:end, NumDataPoints+1), :] + cgdf = cgdf[setdiff(1:end, NumDataPoints + 1), :] rename!(cgdf, [:Group; Symbol.(OldColNames)]) # Extreme period identification based on user selection in time_domain_reduction_settings.yml DemandExtremePeriod = false # Used when deciding whether or not to scale demand curves to equal original total demand ExtremeWksList = [] if UseExtremePeriods == 1 - for profKey in keys(ExtPeriodSelections) - for geoKey in keys(ExtPeriodSelections[profKey]) - for typeKey in keys(ExtPeriodSelections[profKey][geoKey]) - for statKey in keys(ExtPeriodSelections[profKey][geoKey][typeKey]) - if ExtPeriodSelections[profKey][geoKey][typeKey][statKey] == 1 - if profKey == "Demand" - DemandExtremePeriod = true - end - if geoKey == "System" - if v print(geoKey, " ") end - (stat, group_idx) = get_extreme_period(InputData, cgdf, profKey, typeKey, statKey, ConstCols, demand_col_names, solar_col_names, wind_col_names, v) - push!(ExtremeWksList, floor(Int, group_idx)) - if v println(group_idx, " : ", stat) end - elseif geoKey == "Zone" - for z in sort(unique(ZONES)) - z_cols = [k for (k,v) in col_to_zone_map if v==z] - if profKey == "Demand" z_cols_type = intersect(z_cols, demand_col_names) - elseif profKey == "PV" z_cols_type = intersect(z_cols, solar_col_names) - elseif profKey == "Wind" z_cols_type = intersect(z_cols, wind_col_names) - else z_cols_type = [] - end - z_cols_type = setdiff(z_cols_type, ConstCols) - if length(z_cols_type) > 0 - if v print(geoKey, " ") end - (stat, group_idx) = get_extreme_period(select(InputData, [:Group; Symbol.(z_cols_type)]), select(cgdf, [:Group; Symbol.(z_cols_type)]), profKey, typeKey, statKey, ConstCols, z_cols_type, z_cols_type, z_cols_type, v) - push!(ExtremeWksList, floor(Int, group_idx)) - if v println(group_idx, " : ", stat, "(", z, ")") end - else - if v println("Zone ", z, " has no time series profiles of type ", profKey) end - end - end - else - println("Error: Geography Key ", geoKey, " is invalid. Select `System' or `Zone'.") - end - end - end - end - end - end - if v println(ExtremeWksList) end - sort!(unique!(ExtremeWksList)) - if v println("Reduced to ", ExtremeWksList) end + for profKey in keys(ExtPeriodSelections) + for geoKey in keys(ExtPeriodSelections[profKey]) + for typeKey in keys(ExtPeriodSelections[profKey][geoKey]) + for statKey in keys(ExtPeriodSelections[profKey][geoKey][typeKey]) + if ExtPeriodSelections[profKey][geoKey][typeKey][statKey] == 1 + if profKey == "Demand" + DemandExtremePeriod = true + end + if geoKey == "System" + if v + print(geoKey, " ") + end + (stat, group_idx) = get_extreme_period(InputData, + cgdf, + profKey, + typeKey, + statKey, + ConstCols, + demand_col_names, + solar_col_names, + wind_col_names, + v) + push!(ExtremeWksList, floor(Int, group_idx)) + if v + println(group_idx, " : ", stat) + end + elseif geoKey == "Zone" + for z in sort(unique(ZONES)) + z_cols = [k for (k, v) in col_to_zone_map if v == z] + if profKey == "Demand" + z_cols_type = intersect(z_cols, demand_col_names) + elseif profKey == "PV" + z_cols_type = intersect(z_cols, solar_col_names) + elseif profKey == "Wind" + z_cols_type = intersect(z_cols, wind_col_names) + else + z_cols_type = [] + end + z_cols_type = setdiff(z_cols_type, ConstCols) + if length(z_cols_type) > 0 + if v + print(geoKey, " ") + end + (stat, group_idx) = get_extreme_period(select(InputData, + [:Group; Symbol.(z_cols_type)]), + select(cgdf, [:Group; Symbol.(z_cols_type)]), + profKey, + typeKey, + statKey, + ConstCols, + z_cols_type, + z_cols_type, + z_cols_type, + v) + push!(ExtremeWksList, floor(Int, group_idx)) + if v + println(group_idx, " : ", stat, "(", z, ")") + end + else + if v + println("Zone ", + z, + " has no time series profiles of type ", + profKey) + end + end + end + else + println("Error: Geography Key ", + geoKey, + " is invalid. Select `System' or `Zone'.") + end + end + end + end + end + end + if v + println(ExtremeWksList) + end + sort!(unique!(ExtremeWksList)) + if v + println("Reduced to ", ExtremeWksList) + end end ### DATA MODIFICATION - Shifting InputData and Normalized InputData # from 8760 (# hours) by n (# profiles) DF to # 168*n (n period-stacked profiles) by 52 (# periods) DF - DFsToConcat = [stack(InputData[isequal.(InputData.Group,w),:], OldColNames)[!,:value] for w in 1:NumDataPoints if w <= NumDataPoints ] + DFsToConcat = [stack(InputData[isequal.(InputData.Group, w), :], OldColNames)[!, + :value] for w in 1:NumDataPoints if w <= NumDataPoints] ModifiedData = DataFrame(Dict(Symbol(i) => DFsToConcat[i] for i in 1:NumDataPoints)) - AnnualTSeriesNormalized[:, :Group] .= (1:Nhours) .÷ (TimestepsPerRepPeriod+0.0001) .+ 1 - DFsToConcatNorm = [stack(AnnualTSeriesNormalized[isequal.(AnnualTSeriesNormalized.Group,w),:], OldColNames)[!,:value] for w in 1:NumDataPoints if w <= NumDataPoints ] - ModifiedDataNormalized = DataFrame(Dict(Symbol(i) => DFsToConcatNorm[i] for i in 1:NumDataPoints)) + AnnualTSeriesNormalized[:, :Group] .= (1:Nhours) .÷ (TimestepsPerRepPeriod + 0.0001) .+ + 1 + DFsToConcatNorm = [stack(AnnualTSeriesNormalized[isequal.(AnnualTSeriesNormalized.Group, + w), + :], + OldColNames)[!, + :value] for w in 1:NumDataPoints if w <= NumDataPoints] + ModifiedDataNormalized = DataFrame(Dict(Symbol(i) => DFsToConcatNorm[i] + for i in 1:NumDataPoints)) # Remove extreme periods from normalized data before clustering NClusters = MinPeriods if UseExtremePeriods == 1 - if v println("Pre-removal: ", names(ModifiedDataNormalized)) end - if v println("Extreme Periods: ", string.(ExtremeWksList)) end + if v + println("Pre-removal: ", names(ModifiedDataNormalized)) + end + if v + println("Extreme Periods: ", string.(ExtremeWksList)) + end ClusteringInputDF = select(ModifiedDataNormalized, Not(string.(ExtremeWksList))) - if v println("Post-removal: ", names(ClusteringInputDF)) end + if v + println("Post-removal: ", names(ClusteringInputDF)) + end NClusters -= length(ExtremeWksList) else ClusteringInputDF = ModifiedDataNormalized end - ##### STEP 3: Clustering cluster_results = [] # Cluster once regardless of iteration decisions - push!(cluster_results, cluster(ClusterMethod, ClusteringInputDF, NClusters, nReps, v, random)) + push!(cluster_results, + cluster(ClusterMethod, ClusteringInputDF, NClusters, nReps, v, random)) # Iteratively add worst periods as extreme periods OR increment number of clusters k # until threshold is met or maximum periods are added (If chosen in inputs) if (Iterate == 1) - while (!check_condition(Threshold, last(cluster_results)[1], OldColNames, ScalingMethod, TimestepsPerRepPeriod)) & ((length(ExtremeWksList)+NClusters) < MaxPeriods) + while (!check_condition(Threshold, + last(cluster_results)[1], + OldColNames, + ScalingMethod, + TimestepsPerRepPeriod)) & ((length(ExtremeWksList) + NClusters) < MaxPeriods) if IterateMethod == "cluster" - if v println("Adding a new Cluster! ") end + if v + println("Adding a new Cluster! ") + end NClusters += 1 - push!(cluster_results, cluster(ClusterMethod, ClusteringInputDF, NClusters, nReps, v, random)) + push!(cluster_results, + cluster(ClusterMethod, ClusteringInputDF, NClusters, nReps, v, random)) elseif (IterateMethod == "extreme") & (UseExtremePeriods == 1) - if v println("Adding a new Extreme Period! ") end + if v + println("Adding a new Extreme Period! ") + end worst_period_idx = get_worst_period_idx(last(cluster_results)[1]) removed_period = string(names(ClusteringInputDF)[worst_period_idx]) select!(ClusteringInputDF, Not(worst_period_idx)) push!(ExtremeWksList, parse(Int, removed_period)) - if v println(worst_period_idx, " (", removed_period, ") ", ExtremeWksList) end - push!(cluster_results, cluster(ClusterMethod, ClusteringInputDF, NClusters, nReps, v, random)) + if v + println(worst_period_idx, " (", removed_period, ") ", ExtremeWksList) + end + push!(cluster_results, + cluster(ClusterMethod, ClusteringInputDF, NClusters, nReps, v, random)) elseif IterateMethod == "extreme" - println("INVALID IterateMethod ", IterateMethod, " because UseExtremePeriods is off. Set to 1 if you wish to add extreme periods.") + println("INVALID IterateMethod ", + IterateMethod, + " because UseExtremePeriods is off. Set to 1 if you wish to add extreme periods.") break else - println("INVALID IterateMethod ", IterateMethod, ". Choose 'cluster' or 'extreme'.") + println("INVALID IterateMethod ", + IterateMethod, + ". Choose 'cluster' or 'extreme'.") break end end - if v && (length(ExtremeWksList)+NClusters == MaxPeriods) + if v && (length(ExtremeWksList) + NClusters == MaxPeriods) println("Stopped iterating by hitting the maximum number of periods.") elseif v println("Stopped by meeting the accuracy threshold.") @@ -842,7 +1044,9 @@ function cluster_inputs(inpath, settings_path, mysetup, stage_id=-99, v=false; r # ClusterInputDF Reframing of Centers/Medoids (i.e., alphabetical as opposed to indices, same order) M = [parse(Int64, string(names(ClusteringInputDF)[i])) for i in M] - if v println("Fixed M: ", M) end + if v + println("Fixed M: ", M) + end # ClusterInputDF Ordering of All Periods (i.e., alphabetical as opposed to indices) A_Dict = Dict() # States index of representative period within M for each period a in A @@ -855,7 +1059,9 @@ function cluster_inputs(inpath, settings_path, mysetup, stage_id=-99, v=false; r # Add extreme periods into the clustering result with # of occurences = 1 for each ExtremeWksList = sort(ExtremeWksList) if UseExtremePeriods == 1 - if v println("Extreme Periods: ", ExtremeWksList) end + if v + println("Extreme Periods: ", ExtremeWksList) + end M = [M; ExtremeWksList] A_idx = NClusters + 1 for w in ExtremeWksList @@ -868,7 +1074,7 @@ function cluster_inputs(inpath, settings_path, mysetup, stage_id=-99, v=false; r end # Recreate A in numeric order (as opposed to ClusterInputDF order) - A = [A_Dict[i] for i in 1:(length(A)+length(ExtremeWksList))] + A = [A_Dict[i] for i in 1:(length(A) + length(ExtremeWksList))] N = W # Keep cluster version of weights stored as N, number of periods represented by RP @@ -879,32 +1085,40 @@ function cluster_inputs(inpath, settings_path, mysetup, stage_id=-99, v=false; r # SORT A W M in conjunction, chronologically by M, before handling them elsewhere to be consistent # A points to an index of M. We need it to point to a new index of sorted M. Hence, AssignMap. old_M = M - df_sort = DataFrame( Weights = W, NumPeriodsRepresented = N, Rep_Period = M) + df_sort = DataFrame(Weights = W, NumPeriodsRepresented = N, Rep_Period = M) sort!(df_sort, [:Rep_Period]) W = df_sort[!, :Weights] N = df_sort[!, :NumPeriodsRepresented] M = df_sort[!, :Rep_Period] - AssignMap = Dict( i => findall(x->x==old_M[i], M)[1] for i in 1:length(M)) + AssignMap = Dict(i => findall(x -> x == old_M[i], M)[1] for i in 1:length(M)) A = [AssignMap[a] for a in A] # Make PeriodMap, maps each period to its representative period PeriodMap = DataFrame(Period_Index = 1:length(A), - Rep_Period = [M[a] for a in A], - Rep_Period_Index = [a for a in A]) + Rep_Period = [M[a] for a in A], + Rep_Period_Index = [a for a in A]) # Get Symbol-version of column names by type for later analysis DemandCols = Symbol.(demand_col_names) - VarCols = [Symbol(var_col_names[i]) for i in 1:length(var_col_names) ] - FuelCols = [Symbol(fuel_col_names[i]) for i in 1:length(fuel_col_names) ] - ConstCol_Syms = [Symbol(ConstCols[i]) for i in 1:length(ConstCols) ] + VarCols = [Symbol(var_col_names[i]) for i in 1:length(var_col_names)] + FuelCols = [Symbol(fuel_col_names[i]) for i in 1:length(fuel_col_names)] + ConstCol_Syms = [Symbol(ConstCols[i]) for i in 1:length(ConstCols)] # Cluster Ouput: The original data at the medoids/centers - ClusterOutputData = ModifiedData[:,Symbol.(M)] + ClusterOutputData = ModifiedData[:, Symbol.(M)] # Get zone-wise demand multipliers for later scaling in order for weighted-representative-total-zonal demand to equal original total-zonal demand # (Only if we don't have demand-related extreme periods because we don't want to change peak demand periods) if !DemandExtremePeriod - demand_mults = get_demand_multipliers(ClusterOutputData, InputData, M, W, DemandCols, TimestepsPerRepPeriod, NewColNames, NClusters, Ncols) + demand_mults = get_demand_multipliers(ClusterOutputData, + InputData, + M, + W, + DemandCols, + TimestepsPerRepPeriod, + NewColNames, + NClusters, + Ncols) end # Reorganize Data by Demand, Solar, Wind, Fuel, and GrpWeight by Hour, Add Constant Data Back In @@ -914,37 +1128,47 @@ function cluster_inputs(inpath, settings_path, mysetup, stage_id=-99, v=false; r fpDFs = [] # Fuel Profile DataFrames - Just Fuel Profiles for m in 1:NClusters - rpDF = DataFrame( Dict( NewColNames[i] => ClusterOutputData[!,m][TimestepsPerRepPeriod*(i-1)+1 : TimestepsPerRepPeriod*i] for i in 1:Ncols) ) - gvDF = DataFrame( Dict( NewColNames[i] => ClusterOutputData[!,m][TimestepsPerRepPeriod*(i-1)+1 : TimestepsPerRepPeriod*i] for i in 1:Ncols if (Symbol(NewColNames[i]) in VarCols)) ) - dmDF = DataFrame( Dict( NewColNames[i] => ClusterOutputData[!,m][TimestepsPerRepPeriod*(i-1)+1 : TimestepsPerRepPeriod*i] for i in 1:Ncols if (Symbol(NewColNames[i]) in DemandCols)) ) - if IncludeFuel fpDF = DataFrame( Dict( NewColNames[i] => ClusterOutputData[!,m][TimestepsPerRepPeriod*(i-1)+1 : TimestepsPerRepPeriod*i] for i in 1:Ncols if (Symbol(NewColNames[i]) in FuelCols)) ) end - if !IncludeFuel fpDF = DataFrame(Placeholder = 1:TimestepsPerRepPeriod) end + rpDF = DataFrame(Dict(NewColNames[i] => ClusterOutputData[!, m][(TimestepsPerRepPeriod * (i - 1) + 1):(TimestepsPerRepPeriod * i)] + for i in 1:Ncols)) + gvDF = DataFrame(Dict(NewColNames[i] => ClusterOutputData[!, m][(TimestepsPerRepPeriod * (i - 1) + 1):(TimestepsPerRepPeriod * i)] + for i in 1:Ncols if (Symbol(NewColNames[i]) in VarCols))) + dmDF = DataFrame(Dict(NewColNames[i] => ClusterOutputData[!, m][(TimestepsPerRepPeriod * (i - 1) + 1):(TimestepsPerRepPeriod * i)] + for i in 1:Ncols if (Symbol(NewColNames[i]) in DemandCols))) + if IncludeFuel + fpDF = DataFrame(Dict(NewColNames[i] => ClusterOutputData[!, m][(TimestepsPerRepPeriod * (i - 1) + 1):(TimestepsPerRepPeriod * i)] + for i in 1:Ncols if (Symbol(NewColNames[i]) in FuelCols))) + end + if !IncludeFuel + fpDF = DataFrame(Placeholder = 1:TimestepsPerRepPeriod) + end # Add Constant Columns back in for c in 1:length(ConstCols) - rpDF[!,Symbol(ConstCols[c])] .= ConstData[c][1] + rpDF[!, Symbol(ConstCols[c])] .= ConstData[c][1] if Symbol(ConstCols[c]) in VarCols - gvDF[!,Symbol(ConstCols[c])] .= ConstData[c][1] + gvDF[!, Symbol(ConstCols[c])] .= ConstData[c][1] elseif Symbol(ConstCols[c]) in FuelCols - fpDF[!,Symbol(ConstCols[c])] .= ConstData[c][1] + fpDF[!, Symbol(ConstCols[c])] .= ConstData[c][1] elseif Symbol(ConstCols[c]) in DemandCols - dmDF[!,Symbol(ConstCols[c])] .= ConstData[c][1] + dmDF[!, Symbol(ConstCols[c])] .= ConstData[c][1] end end - if !IncludeFuel select!(fpDF, Not(:Placeholder)) end + if !IncludeFuel + select!(fpDF, Not(:Placeholder)) + end # Scale Demand using previously identified multipliers # Scale dmDF but not rpDF which compares to input data but is not written to file. for demandcol in DemandCols if demandcol ∉ ConstCol_Syms if !DemandExtremePeriod - dmDF[!,demandcol] .*= demand_mults[demandcol] + dmDF[!, demandcol] .*= demand_mults[demandcol] end end end - rpDF[!,:GrpWeight] .= W[m] - rpDF[!,:Cluster] .= M[m] + rpDF[!, :GrpWeight] .= W[m] + rpDF[!, :Cluster] .= M[m] push!(rpDFs, rpDF) push!(gvDFs, gvDF) push!(dmDFs, dmDF) @@ -955,35 +1179,54 @@ function cluster_inputs(inpath, settings_path, mysetup, stage_id=-99, v=false; r DMOutputData = vcat(dmDFs...) # Demand Profiles FPOutputData = vcat(fpDFs...) # Fuel Profiles - ##### Step 5: Evaluation - InputDataTest = InputData[(InputData.Group .<= NumDataPoints*1.0), :] + InputDataTest = InputData[(InputData.Group .<= NumDataPoints * 1.0), :] ClusterDataTest = vcat([rpDFs[a] for a in A]...) # To compare fairly, demand is not scaled here - RMSE = Dict( c => rmse_score(InputDataTest[:, c], ClusterDataTest[:, c]) for c in OldColNames) + RMSE = Dict(c => rmse_score(InputDataTest[:, c], ClusterDataTest[:, c]) + for c in OldColNames) ##### Step 6: Print to File if MultiStage == 1 - if v print("Outputs: MultiStage") end + if v + print("Outputs: MultiStage") + end if MultiStageConcatenate == 1 - if v println(" with Concatenation") end - groups_per_stage = round.(Int, size(A,1)*relative_lengths) - group_ranges = [if i == 1 1:groups_per_stage[1] else sum(groups_per_stage[1:i-1])+1:sum(groups_per_stage[1:i]) end for i in 1:size(relative_lengths,1)] + if v + println(" with Concatenation") + end + groups_per_stage = round.(Int, size(A, 1) * relative_lengths) + group_ranges = [if i == 1 + 1:groups_per_stage[1] + else + (sum(groups_per_stage[1:(i - 1)]) + 1):sum(groups_per_stage[1:i]) + end + for i in 1:size(relative_lengths, 1)] Stage_Weights = Dict() Stage_PeriodMaps = Dict() Stage_Outfiles = Dict() - SolarVar_Outfile = joinpath(TimeDomainReductionFolder, "Vre_and_stor_solar_variability.csv") - WindVar_Outfile = joinpath(TimeDomainReductionFolder, "Vre_and_stor_wind_variability.csv") + SolarVar_Outfile = joinpath(TimeDomainReductionFolder, + "Vre_and_stor_solar_variability.csv") + WindVar_Outfile = joinpath(TimeDomainReductionFolder, + "Vre_and_stor_wind_variability.csv") for per in 1:NumStages # Iterate over multi-stages - mkpath(joinpath(inpath,"inputs","inputs_p$per", TimeDomainReductionFolder)) + mkpath(joinpath(inpath, + "inputs", + "inputs_p$per", + TimeDomainReductionFolder)) # Stage-specific weights and mappings cmap = countmap(A[group_ranges[per]]) # Count number of each rep. period in the planning stage - weight_props = [ if i in keys(cmap) cmap[i]/N[i] else 0 end for i in 1:size(M,1) ] # Proportions of each rep. period associated with each planning stage - Stage_Weights[per] = weight_props.*W # Total hours that each rep. period represents within the planning stage - Stage_PeriodMaps[per] = PeriodMap[group_ranges[per],:] - Stage_PeriodMaps[per][!,:Period_Index] = 1:(group_ranges[per][end]-group_ranges[per][1]+1) + weight_props = [if i in keys(cmap) + cmap[i] / N[i] + else + 0 + end + for i in 1:size(M, 1)] # Proportions of each rep. period associated with each planning stage + Stage_Weights[per] = weight_props .* W # Total hours that each rep. period represents within the planning stage + Stage_PeriodMaps[per] = PeriodMap[group_ranges[per], :] + Stage_PeriodMaps[per][!, :Period_Index] = 1:(group_ranges[per][end] - group_ranges[per][1] + 1) # Outfiles Stage_Outfiles[per] = Dict() Stage_Outfiles[per]["Demand"] = joinpath("inputs_p$per", Demand_Outfile) @@ -992,239 +1235,349 @@ function cluster_inputs(inpath, settings_path, mysetup, stage_id=-99, v=false; r Stage_Outfiles[per]["PMap"] = joinpath("inputs_p$per", PMap_Outfile) Stage_Outfiles[per]["YAML"] = joinpath("inputs_p$per", YAML_Outfile) if !isempty(inputs_dict[per]["VRE_STOR"]) - Stage_Outfiles[per]["GSolar"] = joinpath("inputs_p$per", SolarVar_Outfile) + Stage_Outfiles[per]["GSolar"] = joinpath("inputs_p$per", + SolarVar_Outfile) Stage_Outfiles[per]["GWind"] = joinpath("inputs_p$per", WindVar_Outfile) end # Save output data to stage-specific locations ### TDR_Results/Demand_data_clustered.csv - demand_in = get_demand_dataframe(joinpath(inpath, "inputs", "inputs_p$per"), mysetup["SystemFolder"]) - demand_in[!,:Sub_Weights] = demand_in[!,:Sub_Weights] * 1. - demand_in[1:length(Stage_Weights[per]),:Sub_Weights] .= Stage_Weights[per] - demand_in[!,:Rep_Periods][1] = length(Stage_Weights[per]) - demand_in[!,:Timesteps_per_Rep_Period][1] = TimestepsPerRepPeriod + demand_in = get_demand_dataframe(joinpath(inpath, "inputs", "inputs_p$per"), + mysetup["SystemFolder"]) + demand_in[!, :Sub_Weights] = demand_in[!, :Sub_Weights] * 1.0 + demand_in[1:length(Stage_Weights[per]), :Sub_Weights] .= Stage_Weights[per] + demand_in[!, :Rep_Periods][1] = length(Stage_Weights[per]) + demand_in[!, :Timesteps_per_Rep_Period][1] = TimestepsPerRepPeriod select!(demand_in, Not(DemandCols)) select!(demand_in, Not(:Time_Index)) - Time_Index_M = Union{Int64, Missings.Missing}[missing for i in 1:size(demand_in,1)] - Time_Index_M[1:size(DMOutputData,1)] = 1:size(DMOutputData,1) - demand_in[!,:Time_Index] .= Time_Index_M + Time_Index_M = Union{Int64, Missings.Missing}[missing + for i in 1:size(demand_in, 1)] + Time_Index_M[1:size(DMOutputData, 1)] = 1:size(DMOutputData, 1) + demand_in[!, :Time_Index] .= Time_Index_M for c in DemandCols - new_col = Union{Float64, Missings.Missing}[missing for i in 1:size(demand_in,1)] - new_col[1:size(DMOutputData,1)] = DMOutputData[!,c] - demand_in[!,c] .= new_col + new_col = Union{Float64, Missings.Missing}[missing + for i in 1:size(demand_in, 1)] + new_col[1:size(DMOutputData, 1)] = DMOutputData[!, c] + demand_in[!, c] .= new_col end - demand_in = demand_in[1:size(DMOutputData,1),:] + demand_in = demand_in[1:size(DMOutputData, 1), :] - if v println("Writing demand file...") end - CSV.write(joinpath(inpath, "inputs", Stage_Outfiles[per]["Demand"]), demand_in) + if v + println("Writing demand file...") + end + CSV.write(joinpath(inpath, "inputs", Stage_Outfiles[per]["Demand"]), + demand_in) ### TDR_Results/Generators_variability.csv # Reset column ordering, add time index, and solve duplicate column name trouble with CSV.write's header kwarg - GVColMap = Dict(RESOURCE_ZONES[i] => RESOURCES[i] for i in 1:length(inputs_dict[1]["RESOURCE_NAMES"])) + GVColMap = Dict(RESOURCE_ZONES[i] => RESOURCES[i] + for i in 1:length(inputs_dict[1]["RESOURCE_NAMES"])) GVColMap["Time_Index"] = "Time_Index" GVOutputData = GVOutputData[!, Symbol.(RESOURCE_ZONES)] - insertcols!(GVOutputData, 1, :Time_Index => 1:size(GVOutputData,1)) + insertcols!(GVOutputData, 1, :Time_Index => 1:size(GVOutputData, 1)) NewGVColNames = [GVColMap[string(c)] for c in names(GVOutputData)] - if v println("Writing resource file...") end - CSV.write(joinpath(inpath, "inputs", Stage_Outfiles[per]["GVar"]), GVOutputData, header=NewGVColNames) + if v + println("Writing resource file...") + end + CSV.write(joinpath(inpath, "inputs", Stage_Outfiles[per]["GVar"]), + GVOutputData, + header = NewGVColNames) if !isempty(inputs_dict[per]["VRE_STOR"]) - gen_var = load_dataframe(joinpath(inpath, "inputs", Stage_Outfiles[per]["GVar"])) - + gen_var = load_dataframe(joinpath(inpath, + "inputs", + Stage_Outfiles[per]["GVar"])) + # Find which indexes have solar PV/wind names RESOURCE_ZONES_VRE_STOR = NewGVColNames solar_col_names = [] wind_col_names = [] for r in 1:length(RESOURCE_ZONES_VRE_STOR) - if occursin("PV", RESOURCE_ZONES_VRE_STOR[r]) || occursin("pv", RESOURCE_ZONES_VRE_STOR[r]) || occursin("Pv", RESOURCE_ZONES_VRE_STOR[r]) || occursin("Solar", RESOURCE_ZONES_VRE_STOR[r]) || occursin("SOLAR", RESOURCE_ZONES_VRE_STOR[r]) || occursin("solar", RESOURCE_ZONES_VRE_STOR[r]) || occursin("Time", RESOURCE_ZONES_VRE_STOR[r]) - push!(solar_col_names,r) + if occursin("PV", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("pv", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("Pv", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("Solar", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("SOLAR", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("solar", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("Time", RESOURCE_ZONES_VRE_STOR[r]) + push!(solar_col_names, r) end - if occursin("Wind", RESOURCE_ZONES_VRE_STOR[r]) || occursin("WIND", RESOURCE_ZONES_VRE_STOR[r]) || occursin("wind", RESOURCE_ZONES_VRE_STOR[r]) || occursin("Time", RESOURCE_ZONES_VRE_STOR[r]) + if occursin("Wind", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("WIND", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("wind", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("Time", RESOURCE_ZONES_VRE_STOR[r]) push!(wind_col_names, r) end end - + # Index into dataframe and output them solar_var = gen_var[!, solar_col_names] - solar_var[!, :Time_Index] = 1:size(solar_var,1) + solar_var[!, :Time_Index] = 1:size(solar_var, 1) wind_var = gen_var[!, wind_col_names] - wind_var[!, :Time_Index] = 1:size(wind_var,1) - - CSV.write(joinpath(inpath, "inputs", Stage_Outfiles[per]["GSolar"]), solar_var) - CSV.write(joinpath(inpath, "inputs", Stage_Outfiles[per]["GWind"]), wind_var) + wind_var[!, :Time_Index] = 1:size(wind_var, 1) + + CSV.write(joinpath(inpath, "inputs", Stage_Outfiles[per]["GSolar"]), + solar_var) + CSV.write(joinpath(inpath, "inputs", Stage_Outfiles[per]["GWind"]), + wind_var) end ### TDR_Results/Fuels_data.csv - fuel_in = load_dataframe(joinpath(inpath, "inputs", "inputs_p$per", mysetup["SystemFolder"], "Fuels_data.csv")) + fuel_in = load_dataframe(joinpath(inpath, + "inputs", + "inputs_p$per", + mysetup["SystemFolder"], + "Fuels_data.csv")) select!(fuel_in, Not(:Time_Index)) SepFirstRow = DataFrame(fuel_in[1, :]) NewFuelOutput = vcat(SepFirstRow, FPOutputData) rename!(NewFuelOutput, FuelCols) - insertcols!(NewFuelOutput, 1, :Time_Index => 0:size(NewFuelOutput,1)-1) - if v println("Writing fuel profiles...") end - CSV.write(joinpath(inpath, "inputs", Stage_Outfiles[per]["Fuel"]), NewFuelOutput) + insertcols!(NewFuelOutput, 1, :Time_Index => 0:(size(NewFuelOutput, 1) - 1)) + if v + println("Writing fuel profiles...") + end + CSV.write(joinpath(inpath, "inputs", Stage_Outfiles[per]["Fuel"]), + NewFuelOutput) ### TDR_Results/Period_map.csv - if v println("Writing period map...") end - CSV.write(joinpath(inpath, "inputs", Stage_Outfiles[per]["PMap"]), Stage_PeriodMaps[per]) + if v + println("Writing period map...") + end + CSV.write(joinpath(inpath, "inputs", Stage_Outfiles[per]["PMap"]), + Stage_PeriodMaps[per]) ### TDR_Results/time_domain_reduction_settings.yml - if v println("Writing .yml settings...") end - YAML.write_file(joinpath(inpath, "inputs", Stage_Outfiles[per]["YAML"]), myTDRsetup) - + if v + println("Writing .yml settings...") + end + YAML.write_file(joinpath(inpath, "inputs", Stage_Outfiles[per]["YAML"]), + myTDRsetup) end else - if v print("without Concatenation has not yet been fully implemented. ") end - if v println("( STAGE ", stage_id, " )") end - input_stage_directory = "inputs_p"*string(stage_id) - mkpath(joinpath(inpath,"inputs",input_stage_directory, TimeDomainReductionFolder)) + if v + print("without Concatenation has not yet been fully implemented. ") + end + if v + println("( STAGE ", stage_id, " )") + end + input_stage_directory = "inputs_p" * string(stage_id) + mkpath(joinpath(inpath, + "inputs", + input_stage_directory, + TimeDomainReductionFolder)) ### TDR_Results/Demand_data.csv - demand_in = get_demand_dataframe(joinpath(inpath, "inputs", input_stage_directory, mysetup["SystemFolder"])) - demand_in[!,:Sub_Weights] = demand_in[!,:Sub_Weights] * 1. - demand_in[1:length(W),:Sub_Weights] .= W - demand_in[!,:Rep_Periods][1] = length(W) - demand_in[!,:Timesteps_per_Rep_Period][1] = TimestepsPerRepPeriod + demand_in = get_demand_dataframe(joinpath(inpath, + "inputs", + input_stage_directory, + mysetup["SystemFolder"])) + demand_in[!, :Sub_Weights] = demand_in[!, :Sub_Weights] * 1.0 + demand_in[1:length(W), :Sub_Weights] .= W + demand_in[!, :Rep_Periods][1] = length(W) + demand_in[!, :Timesteps_per_Rep_Period][1] = TimestepsPerRepPeriod select!(demand_in, Not(DemandCols)) select!(demand_in, Not(:Time_Index)) - Time_Index_M = Union{Int64, Missings.Missing}[missing for i in 1:size(demand_in,1)] - Time_Index_M[1:size(DMOutputData,1)] = 1:size(DMOutputData,1) - demand_in[!,:Time_Index] .= Time_Index_M + Time_Index_M = Union{Int64, Missings.Missing}[missing + for i in 1:size(demand_in, 1)] + Time_Index_M[1:size(DMOutputData, 1)] = 1:size(DMOutputData, 1) + demand_in[!, :Time_Index] .= Time_Index_M for c in DemandCols - new_col = Union{Float64, Missings.Missing}[missing for i in 1:size(demand_in,1)] - new_col[1:size(DMOutputData,1)] = DMOutputData[!,c] - demand_in[!,c] .= new_col + new_col = Union{Float64, Missings.Missing}[missing + for i in 1:size(demand_in, 1)] + new_col[1:size(DMOutputData, 1)] = DMOutputData[!, c] + demand_in[!, c] .= new_col end - demand_in = demand_in[1:size(DMOutputData,1),:] + demand_in = demand_in[1:size(DMOutputData, 1), :] - if v println("Writing demand file...") end - CSV.write(joinpath(inpath,"inputs",input_stage_directory,Demand_Outfile), demand_in) + if v + println("Writing demand file...") + end + CSV.write(joinpath(inpath, "inputs", input_stage_directory, Demand_Outfile), + demand_in) ### TDR_Results/Generators_variability.csv # Reset column ordering, add time index, and solve duplicate column name trouble with CSV.write's header kwarg - GVColMap = Dict(RESOURCE_ZONES[i] => RESOURCES[i] for i in 1:length(myinputs["RESOURCE_NAMES"])) + GVColMap = Dict(RESOURCE_ZONES[i] => RESOURCES[i] + for i in 1:length(myinputs["RESOURCE_NAMES"])) GVColMap["Time_Index"] = "Time_Index" GVOutputData = GVOutputData[!, Symbol.(RESOURCE_ZONES)] - insertcols!(GVOutputData, 1, :Time_Index => 1:size(GVOutputData,1)) + insertcols!(GVOutputData, 1, :Time_Index => 1:size(GVOutputData, 1)) NewGVColNames = [GVColMap[string(c)] for c in names(GVOutputData)] - if v println("Writing resource file...") end - CSV.write(joinpath(inpath,"inputs",input_stage_directory,GVar_Outfile), GVOutputData, header=NewGVColNames) + if v + println("Writing resource file...") + end + CSV.write(joinpath(inpath, "inputs", input_stage_directory, GVar_Outfile), + GVOutputData, + header = NewGVColNames) # Break up VRE-storage components if needed if !isempty(myinputs["VRE_STOR"]) - gen_var = load_dataframe(joinpath(inpath,"inputs",input_stage_directory,GVar_Outfile)) + gen_var = load_dataframe(joinpath(inpath, + "inputs", + input_stage_directory, + GVar_Outfile)) # Find which indexes have solar PV/wind names RESOURCE_ZONES_VRE_STOR = NewGVColNames solar_col_names = [] wind_col_names = [] for r in 1:length(RESOURCE_ZONES_VRE_STOR) - if occursin("PV", RESOURCE_ZONES_VRE_STOR[r]) || occursin("pv", RESOURCE_ZONES_VRE_STOR[r]) || occursin("Pv", RESOURCE_ZONES_VRE_STOR[r]) || occursin("Solar", RESOURCE_ZONES_VRE_STOR[r]) || occursin("SOLAR", RESOURCE_ZONES_VRE_STOR[r]) || occursin("solar", RESOURCE_ZONES_VRE_STOR[r]) || occursin("Time", RESOURCE_ZONES_VRE_STOR[r]) - push!(solar_col_names,r) + if occursin("PV", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("pv", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("Pv", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("Solar", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("SOLAR", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("solar", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("Time", RESOURCE_ZONES_VRE_STOR[r]) + push!(solar_col_names, r) end - if occursin("Wind", RESOURCE_ZONES_VRE_STOR[r]) || occursin("WIND", RESOURCE_ZONES_VRE_STOR[r]) || occursin("wind", RESOURCE_ZONES_VRE_STOR[r]) || occursin("Time", RESOURCE_ZONES_VRE_STOR[r]) + if occursin("Wind", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("WIND", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("wind", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("Time", RESOURCE_ZONES_VRE_STOR[r]) push!(wind_col_names, r) end end # Index into dataframe and output them solar_var = gen_var[!, solar_col_names] - solar_var[!, :Time_Index] = 1:size(solar_var,1) + solar_var[!, :Time_Index] = 1:size(solar_var, 1) wind_var = gen_var[!, wind_col_names] - wind_var[!, :Time_Index] = 1:size(wind_var,1) - - SolarVar_Outfile = joinpath(TimeDomainReductionFolder, "Vre_and_stor_solar_variability.csv") - WindVar_Outfile = joinpath(TimeDomainReductionFolder, "Vre_and_stor_wind_variability.csv") - CSV.write(joinpath(inpath,"inputs",input_stage_directory,SolarVar_Outfile), solar_var) - CSV.write(joinpath(inpath,"inputs",input_stage_directory, WindVar_Outfile), wind_var) + wind_var[!, :Time_Index] = 1:size(wind_var, 1) + + SolarVar_Outfile = joinpath(TimeDomainReductionFolder, + "Vre_and_stor_solar_variability.csv") + WindVar_Outfile = joinpath(TimeDomainReductionFolder, + "Vre_and_stor_wind_variability.csv") + CSV.write(joinpath(inpath, + "inputs", + input_stage_directory, + SolarVar_Outfile), + solar_var) + CSV.write(joinpath(inpath, + "inputs", + input_stage_directory, + WindVar_Outfile), + wind_var) end ### TDR_Results/Fuels_data.csv - fuel_in = load_dataframe(joinpath(inpath, "inputs", input_stage_directory, mysetup["SystemFolder"], "Fuels_data.csv")) + fuel_in = load_dataframe(joinpath(inpath, + "inputs", + input_stage_directory, + mysetup["SystemFolder"], + "Fuels_data.csv")) select!(fuel_in, Not(:Time_Index)) SepFirstRow = DataFrame(fuel_in[1, :]) NewFuelOutput = vcat(SepFirstRow, FPOutputData) rename!(NewFuelOutput, FuelCols) - insertcols!(NewFuelOutput, 1, :Time_Index => 0:size(NewFuelOutput,1)-1) - if v println("Writing fuel profiles...") end - CSV.write(joinpath(inpath,"inputs",input_stage_directory,Fuel_Outfile), NewFuelOutput) + insertcols!(NewFuelOutput, 1, :Time_Index => 0:(size(NewFuelOutput, 1) - 1)) + if v + println("Writing fuel profiles...") + end + CSV.write(joinpath(inpath, "inputs", input_stage_directory, Fuel_Outfile), + NewFuelOutput) ### Period_map.csv - if v println("Writing period map...") end - CSV.write(joinpath(inpath,"inputs",input_stage_directory,PMap_Outfile), PeriodMap) + if v + println("Writing period map...") + end + CSV.write(joinpath(inpath, "inputs", input_stage_directory, PMap_Outfile), + PeriodMap) ### time_domain_reduction_settings.yml - if v println("Writing .yml settings...") end - YAML.write_file(joinpath(inpath,"inputs",input_stage_directory,YAML_Outfile), myTDRsetup) + if v + println("Writing .yml settings...") + end + YAML.write_file(joinpath(inpath, "inputs", input_stage_directory, YAML_Outfile), + myTDRsetup) end else - if v println("Outputs: Single-Stage") end + if v + println("Outputs: Single-Stage") + end mkpath(joinpath(inpath, TimeDomainReductionFolder)) ### TDR_Results/Demand_data.csv system_path = joinpath(inpath, mysetup["SystemFolder"]) demand_in = get_demand_dataframe(system_path) - demand_in[!,:Sub_Weights] = demand_in[!,:Sub_Weights] * 1. - demand_in[1:length(W),:Sub_Weights] .= W - demand_in[!,:Rep_Periods][1] = length(W) - demand_in[!,:Timesteps_per_Rep_Period][1] = TimestepsPerRepPeriod + demand_in[!, :Sub_Weights] = demand_in[!, :Sub_Weights] * 1.0 + demand_in[1:length(W), :Sub_Weights] .= W + demand_in[!, :Rep_Periods][1] = length(W) + demand_in[!, :Timesteps_per_Rep_Period][1] = TimestepsPerRepPeriod select!(demand_in, Not(DemandCols)) select!(demand_in, Not(:Time_Index)) - Time_Index_M = Union{Int64, Missings.Missing}[missing for i in 1:size(demand_in,1)] - Time_Index_M[1:size(DMOutputData,1)] = 1:size(DMOutputData,1) - demand_in[!,:Time_Index] .= Time_Index_M + Time_Index_M = Union{Int64, Missings.Missing}[missing for i in 1:size(demand_in, 1)] + Time_Index_M[1:size(DMOutputData, 1)] = 1:size(DMOutputData, 1) + demand_in[!, :Time_Index] .= Time_Index_M for c in DemandCols - new_col = Union{Float64, Missings.Missing}[missing for i in 1:size(demand_in,1)] - new_col[1:size(DMOutputData,1)] = DMOutputData[!,c] - demand_in[!,c] .= new_col + new_col = Union{Float64, Missings.Missing}[missing for i in 1:size(demand_in, 1)] + new_col[1:size(DMOutputData, 1)] = DMOutputData[!, c] + demand_in[!, c] .= new_col end - demand_in = demand_in[1:size(DMOutputData,1),:] + demand_in = demand_in[1:size(DMOutputData, 1), :] - if v println("Writing demand file...") end + if v + println("Writing demand file...") + end CSV.write(joinpath(inpath, Demand_Outfile), demand_in) ### TDR_Results/Generators_variability.csv # Reset column ordering, add time index, and solve duplicate column name trouble with CSV.write's header kwarg - GVColMap = Dict(RESOURCE_ZONES[i] => RESOURCES[i] for i in 1:length(myinputs["RESOURCE_NAMES"])) + GVColMap = Dict(RESOURCE_ZONES[i] => RESOURCES[i] + for i in 1:length(myinputs["RESOURCE_NAMES"])) GVColMap["Time_Index"] = "Time_Index" GVOutputData = GVOutputData[!, Symbol.(RESOURCE_ZONES)] - insertcols!(GVOutputData, 1, :Time_Index => 1:size(GVOutputData,1)) + insertcols!(GVOutputData, 1, :Time_Index => 1:size(GVOutputData, 1)) NewGVColNames = [GVColMap[string(c)] for c in names(GVOutputData)] - if v println("Writing resource file...") end - CSV.write(joinpath(inpath, GVar_Outfile), GVOutputData, header=NewGVColNames) + if v + println("Writing resource file...") + end + CSV.write(joinpath(inpath, GVar_Outfile), GVOutputData, header = NewGVColNames) # Break up VRE-storage components if needed if !isempty(myinputs["VRE_STOR"]) - gen_var = load_dataframe(joinpath(inpath,GVar_Outfile)) + gen_var = load_dataframe(joinpath(inpath, GVar_Outfile)) # Find which indexes have solar PV/wind names RESOURCE_ZONES_VRE_STOR = NewGVColNames solar_col_names = [] wind_col_names = [] for r in 1:length(RESOURCE_ZONES_VRE_STOR) - if occursin("PV", RESOURCE_ZONES_VRE_STOR[r]) || occursin("pv", RESOURCE_ZONES_VRE_STOR[r]) || occursin("Pv", RESOURCE_ZONES_VRE_STOR[r]) || occursin("Solar", RESOURCE_ZONES_VRE_STOR[r]) || occursin("SOLAR", RESOURCE_ZONES_VRE_STOR[r]) || occursin("solar", RESOURCE_ZONES_VRE_STOR[r]) || occursin("Time", RESOURCE_ZONES_VRE_STOR[r]) - push!(solar_col_names,r) + if occursin("PV", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("pv", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("Pv", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("Solar", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("SOLAR", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("solar", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("Time", RESOURCE_ZONES_VRE_STOR[r]) + push!(solar_col_names, r) end - if occursin("Wind", RESOURCE_ZONES_VRE_STOR[r]) || occursin("WIND", RESOURCE_ZONES_VRE_STOR[r]) || occursin("wind", RESOURCE_ZONES_VRE_STOR[r]) || occursin("Time", RESOURCE_ZONES_VRE_STOR[r]) + if occursin("Wind", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("WIND", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("wind", RESOURCE_ZONES_VRE_STOR[r]) || + occursin("Time", RESOURCE_ZONES_VRE_STOR[r]) push!(wind_col_names, r) end end # Index into dataframe and output them solar_var = gen_var[!, solar_col_names] - solar_var[!, :Time_Index] = 1:size(solar_var,1) + solar_var[!, :Time_Index] = 1:size(solar_var, 1) wind_var = gen_var[!, wind_col_names] - wind_var[!, :Time_Index] = 1:size(wind_var,1) + wind_var[!, :Time_Index] = 1:size(wind_var, 1) - SolarVar_Outfile = joinpath(TimeDomainReductionFolder, "Vre_and_stor_solar_variability.csv") - WindVar_Outfile = joinpath(TimeDomainReductionFolder, "Vre_and_stor_wind_variability.csv") + SolarVar_Outfile = joinpath(TimeDomainReductionFolder, + "Vre_and_stor_solar_variability.csv") + WindVar_Outfile = joinpath(TimeDomainReductionFolder, + "Vre_and_stor_wind_variability.csv") CSV.write(joinpath(inpath, SolarVar_Outfile), solar_var) CSV.write(joinpath(inpath, WindVar_Outfile), wind_var) end @@ -1236,26 +1589,32 @@ function cluster_inputs(inpath, settings_path, mysetup, stage_id=-99, v=false; r SepFirstRow = DataFrame(fuel_in[1, :]) NewFuelOutput = vcat(SepFirstRow, FPOutputData) rename!(NewFuelOutput, FuelCols) - insertcols!(NewFuelOutput, 1, :Time_Index => 0:size(NewFuelOutput,1)-1) - if v println("Writing fuel profiles...") end + insertcols!(NewFuelOutput, 1, :Time_Index => 0:(size(NewFuelOutput, 1) - 1)) + if v + println("Writing fuel profiles...") + end CSV.write(joinpath(inpath, Fuel_Outfile), NewFuelOutput) ### TDR_Results/Period_map.csv - if v println("Writing period map...") end + if v + println("Writing period map...") + end CSV.write(joinpath(inpath, PMap_Outfile), PeriodMap) ### TDR_Results/time_domain_reduction_settings.yml - if v println("Writing .yml settings...") end + if v + println("Writing .yml settings...") + end YAML.write_file(joinpath(inpath, YAML_Outfile), myTDRsetup) end return Dict("OutputDF" => FinalOutputData, - "InputDF" => ClusteringInputDF, - "ColToZoneMap" => col_to_zone_map, - "TDRsetup" => myTDRsetup, - "ClusterObject" => R, - "Assignments" => A, - "Weights" => W, - "Centers" => M, - "RMSE" => RMSE) + "InputDF" => ClusteringInputDF, + "ColToZoneMap" => col_to_zone_map, + "TDRsetup" => myTDRsetup, + "ClusterObject" => R, + "Assignments" => A, + "Weights" => W, + "Centers" => M, + "RMSE" => RMSE) end diff --git a/src/write_outputs/capacity_reserve_margin/effective_capacity.jl b/src/write_outputs/capacity_reserve_margin/effective_capacity.jl index 3e15d5ca66..60a70ecd86 100644 --- a/src/write_outputs/capacity_reserve_margin/effective_capacity.jl +++ b/src/write_outputs/capacity_reserve_margin/effective_capacity.jl @@ -7,21 +7,16 @@ Effective capacity in a capacity reserve margin zone for certain resources in the given timesteps. """ -function thermal_plant_effective_capacity( - EP, +function thermal_plant_effective_capacity(EP, inputs, resources::Vector{Int}, capres_zone::Int, - timesteps::Vector{Int}, -)::Matrix{Float64} - eff_cap = - thermal_plant_effective_capacity.( - Ref(EP), - Ref(inputs), - resources, - Ref(capres_zone), - Ref(timesteps), - ) + timesteps::Vector{Int})::Matrix{Float64} + eff_cap = thermal_plant_effective_capacity.(Ref(EP), + Ref(inputs), + resources, + Ref(capres_zone), + Ref(timesteps)) return reduce(hcat, eff_cap) end @@ -31,24 +26,26 @@ function thermal_plant_effective_capacity(EP::Model, inputs::Dict, y, capres_zon return thermal_plant_effective_capacity(EP, inputs, y, capres_zone, timesteps) end -function thermal_plant_effective_capacity( - EP::Model, +function thermal_plant_effective_capacity(EP::Model, inputs::Dict, r_id::Int, capres_zone::Int, - timesteps::Vector{Int}, -)::Vector{Float64} + timesteps::Vector{Int})::Vector{Float64} y = r_id gen = inputs["RESOURCES"] - capresfactor = derating_factor(gen[y], tag=capres_zone) + capresfactor = derating_factor(gen[y], tag = capres_zone) eTotalCap = value.(EP[:eTotalCap][y]) effective_capacity = fill(capresfactor * eTotalCap, length(timesteps)) if has_maintenance(inputs) && y in ids_with_maintenance(gen) - adjustment = thermal_maintenance_capacity_reserve_margin_adjustment(EP, inputs, y, capres_zone, timesteps) - effective_capacity = effective_capacity .+ value.(adjustment) - end + adjustment = thermal_maintenance_capacity_reserve_margin_adjustment(EP, + inputs, + y, + capres_zone, + timesteps) + effective_capacity = effective_capacity .+ value.(adjustment) + end return effective_capacity end diff --git a/src/write_outputs/capacity_reserve_margin/write_capacity_value.jl b/src/write_outputs/capacity_reserve_margin/write_capacity_value.jl index 747bf5602b..2c902862d3 100644 --- a/src/write_outputs/capacity_reserve_margin/write_capacity_value.jl +++ b/src/write_outputs/capacity_reserve_margin/write_capacity_value.jl @@ -1,16 +1,16 @@ function write_capacity_value(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - gen = inputs["RESOURCES"] - zones = zone_id.(gen) - - G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) - T = inputs["T"] # Number of time steps (hours) - THERM_ALL = inputs["THERM_ALL"] - VRE = inputs["VRE"] - HYDRO_RES = inputs["HYDRO_RES"] - STOR_ALL = inputs["STOR_ALL"] - FLEX = inputs["FLEX"] - MUST_RUN = inputs["MUST_RUN"] - VRE_STOR = inputs["VRE_STOR"] + gen = inputs["RESOURCES"] + zones = zone_id.(gen) + + G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) + T = inputs["T"] # Number of time steps (hours) + THERM_ALL = inputs["THERM_ALL"] + VRE = inputs["VRE"] + HYDRO_RES = inputs["HYDRO_RES"] + STOR_ALL = inputs["STOR_ALL"] + FLEX = inputs["FLEX"] + MUST_RUN = inputs["MUST_RUN"] + VRE_STOR = inputs["VRE_STOR"] scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 eTotalCap = value.(EP[:eTotalCap]) @@ -18,88 +18,118 @@ function write_capacity_value(path::AbstractString, inputs::Dict, setup::Dict, E minimum_plant_size = 1 # MW large_plants = findall(>=(minimum_plant_size), eTotalCap * scale_factor) - THERM_ALL_EX = intersect(THERM_ALL, large_plants) - VRE_EX = intersect(VRE, large_plants) - HYDRO_RES_EX = intersect(HYDRO_RES, large_plants) - STOR_ALL_EX = intersect(STOR_ALL, large_plants) - FLEX_EX = intersect(FLEX, large_plants) - MUST_RUN_EX = intersect(MUST_RUN, large_plants) - # Will only be activated if grid connection capacity exists (because may build standalone storage/VRE, which will only be telling by grid connection capacity) - VRE_STOR_EX = intersect(VRE_STOR, large_plants) - if !isempty(VRE_STOR_EX) - DC_DISCHARGE = inputs["VS_STOR_DC_DISCHARGE"] - DC_CHARGE = inputs["VS_STOR_DC_CHARGE"] - VRE_STOR_STOR_EX = intersect(inputs["VS_STOR"], VRE_STOR_EX) - DC_DISCHARGE_EX = intersect(DC_DISCHARGE, VRE_STOR_EX) - AC_DISCHARGE_EX = intersect(inputs["VS_STOR_AC_DISCHARGE"], VRE_STOR_EX) - DC_CHARGE_EX = intersect(DC_CHARGE, VRE_STOR_EX) - AC_CHARGE_EX = intersect(inputs["VS_STOR_AC_CHARGE"], VRE_STOR_EX) - end - - crm_derate(i, y::Vector{Int}) = derating_factor.(gen[y], tag=i)' + THERM_ALL_EX = intersect(THERM_ALL, large_plants) + VRE_EX = intersect(VRE, large_plants) + HYDRO_RES_EX = intersect(HYDRO_RES, large_plants) + STOR_ALL_EX = intersect(STOR_ALL, large_plants) + FLEX_EX = intersect(FLEX, large_plants) + MUST_RUN_EX = intersect(MUST_RUN, large_plants) + # Will only be activated if grid connection capacity exists (because may build standalone storage/VRE, which will only be telling by grid connection capacity) + VRE_STOR_EX = intersect(VRE_STOR, large_plants) + if !isempty(VRE_STOR_EX) + DC_DISCHARGE = inputs["VS_STOR_DC_DISCHARGE"] + DC_CHARGE = inputs["VS_STOR_DC_CHARGE"] + VRE_STOR_STOR_EX = intersect(inputs["VS_STOR"], VRE_STOR_EX) + DC_DISCHARGE_EX = intersect(DC_DISCHARGE, VRE_STOR_EX) + AC_DISCHARGE_EX = intersect(inputs["VS_STOR_AC_DISCHARGE"], VRE_STOR_EX) + DC_CHARGE_EX = intersect(DC_CHARGE, VRE_STOR_EX) + AC_CHARGE_EX = intersect(inputs["VS_STOR_AC_CHARGE"], VRE_STOR_EX) + end + + crm_derate(i, y::Vector{Int}) = derating_factor.(gen[y], tag = i)' max_power(t::Vector{Int}, y::Vector{Int}) = inputs["pP_Max"][y, t]' total_cap(y::Vector{Int}) = eTotalCap[y]' - dfCapValue = DataFrame() - for i in 1:inputs["NCapacityReserveMargin"] + dfCapValue = DataFrame() + for i in 1:inputs["NCapacityReserveMargin"] capvalue = zeros(T, G) minimum_crm_price = 1 # $/MW - riskyhour = findall(>=(minimum_crm_price), capacity_reserve_margin_price(EP, inputs, setup, i)) + riskyhour = findall(>=(minimum_crm_price), + capacity_reserve_margin_price(EP, inputs, setup, i)) power(y::Vector{Int}) = value.(EP[:vP][y, riskyhour])' - capvalue[riskyhour, THERM_ALL_EX] = thermal_plant_effective_capacity(EP, inputs, THERM_ALL_EX, i, riskyhour) ./ total_cap(THERM_ALL_EX) + capvalue[riskyhour, THERM_ALL_EX] = thermal_plant_effective_capacity(EP, + inputs, + THERM_ALL_EX, + i, + riskyhour) ./ total_cap(THERM_ALL_EX) capvalue[riskyhour, VRE_EX] = crm_derate(i, VRE_EX) .* max_power(riskyhour, VRE_EX) - capvalue[riskyhour, MUST_RUN_EX] = crm_derate(i, MUST_RUN_EX) .* max_power(riskyhour, MUST_RUN_EX) + capvalue[riskyhour, MUST_RUN_EX] = crm_derate(i, MUST_RUN_EX) .* + max_power(riskyhour, MUST_RUN_EX) - capvalue[riskyhour, HYDRO_RES_EX] = crm_derate(i, HYDRO_RES_EX) .* power(HYDRO_RES_EX) ./ total_cap(HYDRO_RES_EX) + capvalue[riskyhour, HYDRO_RES_EX] = crm_derate(i, HYDRO_RES_EX) .* + power(HYDRO_RES_EX) ./ total_cap(HYDRO_RES_EX) - if !isempty(STOR_ALL_EX) + if !isempty(STOR_ALL_EX) charge = value.(EP[:vCHARGE][STOR_ALL_EX, riskyhour].data)' capres_discharge = value.(EP[:vCAPRES_discharge][STOR_ALL_EX, riskyhour].data)' capres_charge = value.(EP[:vCAPRES_charge][STOR_ALL_EX, riskyhour].data)' - capvalue[riskyhour, STOR_ALL_EX] = crm_derate(i, STOR_ALL_EX) .* (power(STOR_ALL_EX) - charge + capres_discharge - capres_charge) ./ total_cap(STOR_ALL_EX) - end + capvalue[riskyhour, STOR_ALL_EX] = crm_derate(i, STOR_ALL_EX) .* + (power(STOR_ALL_EX) - charge + + capres_discharge - capres_charge) ./ + total_cap(STOR_ALL_EX) + end - if !isempty(FLEX_EX) + if !isempty(FLEX_EX) charge = value.(EP[:vCHARGE_FLEX][FLEX_EX, riskyhour].data)' - capvalue[riskyhour, FLEX_EX] = crm_derate(i, FLEX_EX) .* (charge - power(FLEX_EX)) ./ total_cap(FLEX_EX) - end - if !isempty(VRE_STOR_EX) - capres_dc_discharge = value.(EP[:vCAPRES_DC_DISCHARGE][DC_DISCHARGE, riskyhour].data)' + capvalue[riskyhour, FLEX_EX] = crm_derate(i, FLEX_EX) .* + (charge - power(FLEX_EX)) ./ total_cap(FLEX_EX) + end + if !isempty(VRE_STOR_EX) + capres_dc_discharge = value.(EP[:vCAPRES_DC_DISCHARGE][DC_DISCHARGE, + riskyhour].data)' discharge_eff = etainverter.(gen[storage_dc_discharge(gen)])' capvalue_dc_discharge = zeros(T, G) - capvalue_dc_discharge[riskyhour, DC_DISCHARGE] = capres_dc_discharge .* discharge_eff + capvalue_dc_discharge[riskyhour, DC_DISCHARGE] = capres_dc_discharge .* + discharge_eff capres_dc_charge = value.(EP[:vCAPRES_DC_CHARGE][DC_CHARGE, riskyhour].data)' charge_eff = etainverter.(gen[storage_dc_charge(gen)])' capvalue_dc_charge = zeros(T, G) capvalue_dc_charge[riskyhour, DC_CHARGE] = capres_dc_charge ./ charge_eff - capvalue[riskyhour, VRE_STOR_EX] = crm_derate(i, VRE_STOR_EX) .* power(VRE_STOR_EX) ./ total_cap(VRE_STOR_EX) - - charge_vre_stor = value.(EP[:vCHARGE_VRE_STOR][VRE_STOR_STOR_EX, riskyhour].data)' - capvalue[riskyhour, VRE_STOR_STOR_EX] -= crm_derate(i, VRE_STOR_STOR_EX) .* charge_vre_stor ./ total_cap(VRE_STOR_STOR_EX) - - capvalue[riskyhour, DC_DISCHARGE_EX] += crm_derate(i, DC_DISCHARGE_EX) .* capvalue_dc_discharge[riskyhour, DC_DISCHARGE_EX] ./ total_cap(DC_DISCHARGE_EX) - capres_ac_discharge = value.(EP[:vCAPRES_AC_DISCHARGE][AC_DISCHARGE_EX, riskyhour].data)' - capvalue[riskyhour, AC_DISCHARGE_EX] += crm_derate(i, AC_DISCHARGE_EX) .* capres_ac_discharge ./ total_cap(AC_DISCHARGE_EX) - - capvalue[riskyhour, DC_CHARGE_EX] -= crm_derate(i, DC_CHARGE_EX) .* capvalue_dc_charge[riskyhour, DC_CHARGE_EX] ./ total_cap(DC_CHARGE_EX) + capvalue[riskyhour, VRE_STOR_EX] = crm_derate(i, VRE_STOR_EX) .* + power(VRE_STOR_EX) ./ total_cap(VRE_STOR_EX) + + charge_vre_stor = value.(EP[:vCHARGE_VRE_STOR][VRE_STOR_STOR_EX, + riskyhour].data)' + capvalue[riskyhour, VRE_STOR_STOR_EX] -= crm_derate(i, VRE_STOR_STOR_EX) .* + charge_vre_stor ./ + total_cap(VRE_STOR_STOR_EX) + + capvalue[riskyhour, DC_DISCHARGE_EX] += crm_derate(i, DC_DISCHARGE_EX) .* + capvalue_dc_discharge[riskyhour, + DC_DISCHARGE_EX] ./ total_cap(DC_DISCHARGE_EX) + capres_ac_discharge = value.(EP[:vCAPRES_AC_DISCHARGE][AC_DISCHARGE_EX, + riskyhour].data)' + capvalue[riskyhour, AC_DISCHARGE_EX] += crm_derate(i, AC_DISCHARGE_EX) .* + capres_ac_discharge ./ + total_cap(AC_DISCHARGE_EX) + + capvalue[riskyhour, DC_CHARGE_EX] -= crm_derate(i, DC_CHARGE_EX) .* + capvalue_dc_charge[riskyhour, + DC_CHARGE_EX] ./ total_cap(DC_CHARGE_EX) capres_ac_charge = value.(EP[:vCAPRES_AC_CHARGE][AC_CHARGE_EX, riskyhour].data)' - capvalue[riskyhour, AC_CHARGE_EX] -= crm_derate(i, AC_CHARGE_EX) .* capres_ac_charge ./ total_cap(AC_CHARGE_EX) - end + capvalue[riskyhour, AC_CHARGE_EX] -= crm_derate(i, AC_CHARGE_EX) .* + capres_ac_charge ./ total_cap(AC_CHARGE_EX) + end capvalue = collect(transpose(capvalue)) - temp_dfCapValue = DataFrame(Resource = inputs["RESOURCE_NAMES"], Zone = zones, Reserve = fill(Symbol("CapRes_$i"), G)) - temp_dfCapValue = hcat(temp_dfCapValue, DataFrame(capvalue, :auto)) - auxNew_Names = [Symbol("Resource"); Symbol("Zone"); Symbol("Reserve"); [Symbol("t$t") for t in 1:T]] - rename!(temp_dfCapValue, auxNew_Names) - append!(dfCapValue, temp_dfCapValue) - end + temp_dfCapValue = DataFrame(Resource = inputs["RESOURCE_NAMES"], + Zone = zones, + Reserve = fill(Symbol("CapRes_$i"), G)) + temp_dfCapValue = hcat(temp_dfCapValue, DataFrame(capvalue, :auto)) + auxNew_Names = [Symbol("Resource"); + Symbol("Zone"); + Symbol("Reserve"); + [Symbol("t$t") for t in 1:T]] + rename!(temp_dfCapValue, auxNew_Names) + append!(dfCapValue, temp_dfCapValue) + end write_simple_csv(joinpath(path, "CapacityValue.csv"), dfCapValue) end @@ -117,9 +147,11 @@ be calculated only if `WriteShadowPrices` is activated. Returns a vector, with units of $/MW """ -function capacity_reserve_margin_price(EP::Model, inputs::Dict, setup::Dict, capres_zone::Int)::Vector{Float64} +function capacity_reserve_margin_price(EP::Model, + inputs::Dict, + setup::Dict, + capres_zone::Int)::Vector{Float64} ω = inputs["omega"] scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 return dual.(EP[:cCapacityResMargin][capres_zone, :]) ./ ω * scale_factor end - diff --git a/src/write_outputs/capacity_reserve_margin/write_reserve_margin.jl b/src/write_outputs/capacity_reserve_margin/write_reserve_margin.jl index 6d2f8c2e80..1eeca0ef0e 100644 --- a/src/write_outputs/capacity_reserve_margin/write_reserve_margin.jl +++ b/src/write_outputs/capacity_reserve_margin/write_reserve_margin.jl @@ -1,9 +1,9 @@ function write_reserve_margin(path::AbstractString, setup::Dict, EP::Model) - temp_ResMar = dual.(EP[:cCapacityResMargin]) - if setup["ParameterScale"] == 1 - temp_ResMar = temp_ResMar * ModelScalingFactor # Convert from MillionUS$/GWh to US$/MWh - end - dfResMar = DataFrame(temp_ResMar, :auto) - CSV.write(joinpath(path, "ReserveMargin.csv"), dfResMar) - return nothing + temp_ResMar = dual.(EP[:cCapacityResMargin]) + if setup["ParameterScale"] == 1 + temp_ResMar = temp_ResMar * ModelScalingFactor # Convert from MillionUS$/GWh to US$/MWh + end + dfResMar = DataFrame(temp_ResMar, :auto) + CSV.write(joinpath(path, "ReserveMargin.csv"), dfResMar) + return nothing end diff --git a/src/write_outputs/capacity_reserve_margin/write_reserve_margin_revenue.jl b/src/write_outputs/capacity_reserve_margin/write_reserve_margin_revenue.jl index 629cc76756..5036b0759c 100644 --- a/src/write_outputs/capacity_reserve_margin/write_reserve_margin_revenue.jl +++ b/src/write_outputs/capacity_reserve_margin/write_reserve_margin_revenue.jl @@ -8,60 +8,94 @@ Function for reporting the capacity revenue earned by each generator listed in t The last column is the total revenue received from all capacity reserve margin constraints. As a reminder, GenX models the capacity reserve margin (aka capacity market) at the time-dependent level, and each constraint either stands for an overall market or a locality constraint. """ -function write_reserve_margin_revenue(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) +function write_reserve_margin_revenue(path::AbstractString, + inputs::Dict, + setup::Dict, + EP::Model) scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 - - gen = inputs["RESOURCES"] - regions = region.(gen) - clusters = cluster.(gen) - zones = zone_id.(gen) + gen = inputs["RESOURCES"] + regions = region.(gen) + clusters = cluster.(gen) + zones = zone_id.(gen) - G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) - T = inputs["T"] # Number of time steps (hours) - THERM_ALL = inputs["THERM_ALL"] - VRE = inputs["VRE"] - HYDRO_RES = inputs["HYDRO_RES"] - STOR_ALL = inputs["STOR_ALL"] - FLEX = inputs["FLEX"] - MUST_RUN = inputs["MUST_RUN"] - VRE_STOR = inputs["VRE_STOR"] - if !isempty(VRE_STOR) - VRE_STOR_STOR = inputs["VS_STOR"] - DC_DISCHARGE = inputs["VS_STOR_DC_DISCHARGE"] - AC_DISCHARGE = inputs["VS_STOR_AC_DISCHARGE"] - DC_CHARGE = inputs["VS_STOR_DC_CHARGE"] - AC_CHARGE = inputs["VS_STOR_AC_CHARGE"] - end - dfResRevenue = DataFrame(Region = regions, Resource = inputs["RESOURCE_NAMES"], Zone = zones, Cluster = clusters) - annual_sum = zeros(G) - for i in 1:inputs["NCapacityReserveMargin"] - weighted_price = capacity_reserve_margin_price(EP, inputs, setup, i) .* inputs["omega"] - tempresrev = zeros(G) - tempresrev[THERM_ALL] = thermal_plant_effective_capacity(EP, inputs, THERM_ALL, i)' * weighted_price - tempresrev[VRE] = derating_factor.(gen.Vre, tag=i) .* (value.(EP[:eTotalCap][VRE])) .* (inputs["pP_Max"][VRE, :] * weighted_price) - tempresrev[MUST_RUN] = derating_factor.(gen.MustRun, tag=i) .* (value.(EP[:eTotalCap][MUST_RUN])) .* (inputs["pP_Max"][MUST_RUN, :] * weighted_price) - tempresrev[HYDRO_RES] = derating_factor.(gen.Hydro, tag=i) .* (value.(EP[:vP][HYDRO_RES, :]) * weighted_price) - if !isempty(STOR_ALL) - tempresrev[STOR_ALL] = derating_factor.(gen.Storage, tag=i) .* ((value.(EP[:vP][STOR_ALL, :]) - value.(EP[:vCHARGE][STOR_ALL, :]).data + value.(EP[:vCAPRES_discharge][STOR_ALL, :]).data - value.(EP[:vCAPRES_charge][STOR_ALL, :]).data) * weighted_price) - end - if !isempty(FLEX) - tempresrev[FLEX] = derating_factor.(gen.FlexDemand, tag=i) .* ((value.(EP[:vCHARGE_FLEX][FLEX, :]).data - value.(EP[:vP][FLEX, :])) * weighted_price) - end - if !isempty(VRE_STOR) - gen_VRE_STOR = gen.VreStorage - tempresrev[VRE_STOR] = derating_factor.(gen_VRE_STOR, tag=i) .* ((value.(EP[:vP][VRE_STOR, :])) * weighted_price) - tempresrev[VRE_STOR_STOR] .-= derating_factor.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_discharge.!=0) .| (gen_VRE_STOR.stor_dc_charge.!=0) .| (gen_VRE_STOR.stor_ac_discharge.!=0) .|(gen_VRE_STOR.stor_ac_charge.!=0)], tag=i) .* (value.(EP[:vCHARGE_VRE_STOR][VRE_STOR_STOR, :]).data * weighted_price) - tempresrev[DC_DISCHARGE] .+= derating_factor.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_discharge.!=0)], tag=i) .* ((value.(EP[:vCAPRES_DC_DISCHARGE][DC_DISCHARGE, :]).data .* etainverter.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_discharge.!=0)])) * weighted_price) - tempresrev[AC_DISCHARGE] .+= derating_factor.(gen_VRE_STOR[(gen_VRE_STOR.stor_ac_discharge.!=0)], tag=i) .* ((value.(EP[:vCAPRES_AC_DISCHARGE][AC_DISCHARGE, :]).data) * weighted_price) - tempresrev[DC_CHARGE] .-= derating_factor.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_charge.!=0)], tag=i) .* ((value.(EP[:vCAPRES_DC_CHARGE][DC_CHARGE, :]).data ./ etainverter.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_charge.!=0)])) * weighted_price) - tempresrev[AC_CHARGE] .-= derating_factor.(gen_VRE_STOR[(gen_VRE_STOR.stor_ac_charge.!=0)], tag=i) .* ((value.(EP[:vCAPRES_AC_CHARGE][AC_CHARGE, :]).data) * weighted_price) - end - tempresrev *= scale_factor - annual_sum .+= tempresrev - dfResRevenue = hcat(dfResRevenue, DataFrame([tempresrev], [Symbol("CapRes_$i")])) - end - dfResRevenue.AnnualSum = annual_sum - CSV.write(joinpath(path, "ReserveMarginRevenue.csv"), dfResRevenue) - return dfResRevenue + G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) + T = inputs["T"] # Number of time steps (hours) + THERM_ALL = inputs["THERM_ALL"] + VRE = inputs["VRE"] + HYDRO_RES = inputs["HYDRO_RES"] + STOR_ALL = inputs["STOR_ALL"] + FLEX = inputs["FLEX"] + MUST_RUN = inputs["MUST_RUN"] + VRE_STOR = inputs["VRE_STOR"] + if !isempty(VRE_STOR) + VRE_STOR_STOR = inputs["VS_STOR"] + DC_DISCHARGE = inputs["VS_STOR_DC_DISCHARGE"] + AC_DISCHARGE = inputs["VS_STOR_AC_DISCHARGE"] + DC_CHARGE = inputs["VS_STOR_DC_CHARGE"] + AC_CHARGE = inputs["VS_STOR_AC_CHARGE"] + end + dfResRevenue = DataFrame(Region = regions, + Resource = inputs["RESOURCE_NAMES"], + Zone = zones, + Cluster = clusters) + annual_sum = zeros(G) + for i in 1:inputs["NCapacityReserveMargin"] + weighted_price = capacity_reserve_margin_price(EP, inputs, setup, i) .* + inputs["omega"] + tempresrev = zeros(G) + tempresrev[THERM_ALL] = thermal_plant_effective_capacity(EP, + inputs, + THERM_ALL, + i)' * weighted_price + tempresrev[VRE] = derating_factor.(gen.Vre, tag = i) .* + (value.(EP[:eTotalCap][VRE])) .* + (inputs["pP_Max"][VRE, :] * weighted_price) + tempresrev[MUST_RUN] = derating_factor.(gen.MustRun, tag = i) .* + (value.(EP[:eTotalCap][MUST_RUN])) .* + (inputs["pP_Max"][MUST_RUN, :] * weighted_price) + tempresrev[HYDRO_RES] = derating_factor.(gen.Hydro, tag = i) .* + (value.(EP[:vP][HYDRO_RES, :]) * weighted_price) + if !isempty(STOR_ALL) + tempresrev[STOR_ALL] = derating_factor.(gen.Storage, tag = i) .* + ((value.(EP[:vP][STOR_ALL, :]) - + value.(EP[:vCHARGE][STOR_ALL, :]).data + + value.(EP[:vCAPRES_discharge][STOR_ALL, :]).data - + value.(EP[:vCAPRES_charge][STOR_ALL, :]).data) * + weighted_price) + end + if !isempty(FLEX) + tempresrev[FLEX] = derating_factor.(gen.FlexDemand, tag = i) .* + ((value.(EP[:vCHARGE_FLEX][FLEX, :]).data - + value.(EP[:vP][FLEX, :])) * weighted_price) + end + if !isempty(VRE_STOR) + gen_VRE_STOR = gen.VreStorage + tempresrev[VRE_STOR] = derating_factor.(gen_VRE_STOR, tag = i) .* + ((value.(EP[:vP][VRE_STOR, :])) * weighted_price) + tempresrev[VRE_STOR_STOR] .-= derating_factor.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_discharge .!= 0) .| (gen_VRE_STOR.stor_dc_charge .!= 0) .| (gen_VRE_STOR.stor_ac_discharge .!= 0) .| (gen_VRE_STOR.stor_ac_charge .!= 0)], + tag = i) .* (value.(EP[:vCHARGE_VRE_STOR][VRE_STOR_STOR, + :]).data * weighted_price) + tempresrev[DC_DISCHARGE] .+= derating_factor.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_discharge .!= 0)], + tag = i) .* ((value.(EP[:vCAPRES_DC_DISCHARGE][DC_DISCHARGE, + :]).data .* etainverter.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_discharge .!= 0)])) * + weighted_price) + tempresrev[AC_DISCHARGE] .+= derating_factor.(gen_VRE_STOR[(gen_VRE_STOR.stor_ac_discharge .!= 0)], + tag = i) .* ((value.(EP[:vCAPRES_AC_DISCHARGE][AC_DISCHARGE, + :]).data) * weighted_price) + tempresrev[DC_CHARGE] .-= derating_factor.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_charge .!= 0)], + tag = i) .* ((value.(EP[:vCAPRES_DC_CHARGE][DC_CHARGE, :]).data ./ + etainverter.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_charge .!= 0)])) * + weighted_price) + tempresrev[AC_CHARGE] .-= derating_factor.(gen_VRE_STOR[(gen_VRE_STOR.stor_ac_charge .!= 0)], + tag = i) .* ((value.(EP[:vCAPRES_AC_CHARGE][AC_CHARGE, :]).data) * + weighted_price) + end + tempresrev *= scale_factor + annual_sum .+= tempresrev + dfResRevenue = hcat(dfResRevenue, DataFrame([tempresrev], [Symbol("CapRes_$i")])) + end + dfResRevenue.AnnualSum = annual_sum + CSV.write(joinpath(path, "ReserveMarginRevenue.csv"), dfResRevenue) + return dfResRevenue end diff --git a/src/write_outputs/capacity_reserve_margin/write_reserve_margin_slack.jl b/src/write_outputs/capacity_reserve_margin/write_reserve_margin_slack.jl index 99b0e9e0f6..c3f2ebf2c4 100644 --- a/src/write_outputs/capacity_reserve_margin/write_reserve_margin_slack.jl +++ b/src/write_outputs/capacity_reserve_margin/write_reserve_margin_slack.jl @@ -1,10 +1,13 @@ -function write_reserve_margin_slack(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) +function write_reserve_margin_slack(path::AbstractString, + inputs::Dict, + setup::Dict, + EP::Model) NCRM = inputs["NCapacityReserveMargin"] T = inputs["T"] # Number of time steps (hours) - dfResMar_slack = DataFrame(CRM_Constraint = [Symbol("CapRes_$res") for res = 1:NCRM], - AnnualSum = value.(EP[:eCapResSlack_Year]), - Penalty = value.(EP[:eCCapResSlack])) - + dfResMar_slack = DataFrame(CRM_Constraint = [Symbol("CapRes_$res") for res in 1:NCRM], + AnnualSum = value.(EP[:eCapResSlack_Year]), + Penalty = value.(EP[:eCCapResSlack])) + if setup["ParameterScale"] == 1 dfResMar_slack.AnnualSum .*= ModelScalingFactor # Convert GW to MW dfResMar_slack.Penalty .*= ModelScalingFactor^2 # Convert Million $ to $ @@ -17,9 +20,11 @@ function write_reserve_margin_slack(path::AbstractString, inputs::Dict, setup::D if setup["ParameterScale"] == 1 temp_ResMar_slack .*= ModelScalingFactor # Convert GW to MW end - dfResMar_slack = hcat(dfResMar_slack, DataFrame(temp_ResMar_slack, [Symbol("t$t") for t in 1:T])) - CSV.write(joinpath(path, "ReserveMargin_prices_and_penalties.csv"), dftranspose(dfResMar_slack, false), writeheader=false) + dfResMar_slack = hcat(dfResMar_slack, + DataFrame(temp_ResMar_slack, [Symbol("t$t") for t in 1:T])) + CSV.write(joinpath(path, "ReserveMargin_prices_and_penalties.csv"), + dftranspose(dfResMar_slack, false), + writeheader = false) end return nothing end - diff --git a/src/write_outputs/capacity_reserve_margin/write_reserve_margin_w.jl b/src/write_outputs/capacity_reserve_margin/write_reserve_margin_w.jl index 00c273adfc..025f5cd4be 100644 --- a/src/write_outputs/capacity_reserve_margin/write_reserve_margin_w.jl +++ b/src/write_outputs/capacity_reserve_margin/write_reserve_margin_w.jl @@ -1,13 +1,14 @@ function write_reserve_margin_w(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - T = inputs["T"] # Number of time steps (hours) - #dfResMar dataframe with weights included for calculations - dfResMar_w = DataFrame(Constraint = [Symbol("t$t") for t in 1:T]) - temp_ResMar_w = transpose(dual.(EP[:cCapacityResMargin]))./inputs["omega"] - if setup["ParameterScale"] == 1 - temp_ResMar_w = temp_ResMar_w * ModelScalingFactor # Convert from MillionUS$/GWh to US$/MWh - end - dfResMar_w = hcat(dfResMar_w, DataFrame(temp_ResMar_w, :auto)) - auxNew_Names_res=[Symbol("Constraint"); [Symbol("CapRes_$i") for i in 1:inputs["NCapacityReserveMargin"]]] - rename!(dfResMar_w,auxNew_Names_res) - CSV.write(joinpath(path, "ReserveMargin_w.csv"), dfResMar_w) -end \ No newline at end of file + T = inputs["T"] # Number of time steps (hours) + #dfResMar dataframe with weights included for calculations + dfResMar_w = DataFrame(Constraint = [Symbol("t$t") for t in 1:T]) + temp_ResMar_w = transpose(dual.(EP[:cCapacityResMargin])) ./ inputs["omega"] + if setup["ParameterScale"] == 1 + temp_ResMar_w = temp_ResMar_w * ModelScalingFactor # Convert from MillionUS$/GWh to US$/MWh + end + dfResMar_w = hcat(dfResMar_w, DataFrame(temp_ResMar_w, :auto)) + auxNew_Names_res = [Symbol("Constraint"); + [Symbol("CapRes_$i") for i in 1:inputs["NCapacityReserveMargin"]]] + rename!(dfResMar_w, auxNew_Names_res) + CSV.write(joinpath(path, "ReserveMargin_w.csv"), dfResMar_w) +end diff --git a/src/write_outputs/capacity_reserve_margin/write_virtual_discharge.jl b/src/write_outputs/capacity_reserve_margin/write_virtual_discharge.jl index 9a4d8308a6..1aa52623de 100644 --- a/src/write_outputs/capacity_reserve_margin/write_virtual_discharge.jl +++ b/src/write_outputs/capacity_reserve_margin/write_virtual_discharge.jl @@ -5,25 +5,25 @@ Function for writing the "virtual" discharge of each storage technology. Virtual allow storage resources to contribute to the capacity reserve margin without actually discharging. """ function write_virtual_discharge(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) + G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) + T = inputs["T"] # Number of time steps (hours) + STOR_ALL = inputs["STOR_ALL"] - G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) - T = inputs["T"] # Number of time steps (hours) - STOR_ALL = inputs["STOR_ALL"] + scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 - scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 - - resources = inputs["RESOURCE_NAMES"][STOR_ALL] - zones = inputs["R_ZONES"][STOR_ALL] - virtual_discharge = (value.(EP[:vCAPRES_discharge][STOR_ALL, :].data) - value.(EP[:vCAPRES_charge][STOR_ALL, :].data)) * scale_factor + resources = inputs["RESOURCE_NAMES"][STOR_ALL] + zones = inputs["R_ZONES"][STOR_ALL] + virtual_discharge = (value.(EP[:vCAPRES_discharge][STOR_ALL, :].data) - + value.(EP[:vCAPRES_charge][STOR_ALL, :].data)) * scale_factor - dfVirtualDischarge = DataFrame(Resource = resources, Zone = zones) - dfVirtualDischarge.AnnualSum .= virtual_discharge * inputs["omega"] + dfVirtualDischarge = DataFrame(Resource = resources, Zone = zones) + dfVirtualDischarge.AnnualSum .= virtual_discharge * inputs["omega"] - filepath = joinpath(path, "virtual_discharge.csv") - if setup["WriteOutputs"] == "annual" - write_annual(filepath, dfVirtualDischarge) - else # setup["WriteOutputs"] == "full" - write_fulltimeseries(filepath, virtual_discharge, dfVirtualDischarge) - end - return nothing -end + filepath = joinpath(path, "virtual_discharge.csv") + if setup["WriteOutputs"] == "annual" + write_annual(filepath, dfVirtualDischarge) + else # setup["WriteOutputs"] == "full" + write_fulltimeseries(filepath, virtual_discharge, dfVirtualDischarge) + end + return nothing +end diff --git a/src/write_outputs/choose_output_dir.jl b/src/write_outputs/choose_output_dir.jl index dc051f9881..2da796944d 100644 --- a/src/write_outputs/choose_output_dir.jl +++ b/src/write_outputs/choose_output_dir.jl @@ -5,11 +5,11 @@ Avoid overwriting (potentially important) existing results by appending to the d Checks if the suggested output directory already exists. While yes, it appends _1, _2, etc till an unused name is found """ function choose_output_dir(pathinit::String) - path = pathinit - counter = 1 - while isdir(path) - path = string(pathinit, "_", counter) - counter += 1 - end - return path + path = pathinit + counter = 1 + while isdir(path) + path = string(pathinit, "_", counter) + counter += 1 + end + return path end diff --git a/src/write_outputs/co2_cap/write_co2_cap.jl b/src/write_outputs/co2_cap/write_co2_cap.jl index fa8e479ec8..a78ddf2f23 100644 --- a/src/write_outputs/co2_cap/write_co2_cap.jl +++ b/src/write_outputs/co2_cap/write_co2_cap.jl @@ -5,19 +5,19 @@ Function for reporting carbon price associated with carbon cap constraints. """ function write_co2_cap(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - dfCO2Price = DataFrame(CO2_Cap = [Symbol("CO2_Cap_$cap") for cap = 1:inputs["NCO2Cap"]], - CO2_Price = (-1) * (dual.(EP[:cCO2Emissions_systemwide]))) + dfCO2Price = DataFrame(CO2_Cap = [Symbol("CO2_Cap_$cap") for cap in 1:inputs["NCO2Cap"]], + CO2_Price = (-1) * (dual.(EP[:cCO2Emissions_systemwide]))) if setup["ParameterScale"] == 1 dfCO2Price.CO2_Price .*= ModelScalingFactor # Convert Million$/kton to $/ton end - if haskey(inputs, "dfCO2Cap_slack") - dfCO2Price[!,:CO2_Mass_Slack] = convert(Array{Float64}, value.(EP[:vCO2Cap_slack])) - dfCO2Price[!,:CO2_Penalty] = convert(Array{Float64}, value.(EP[:eCCO2Cap_slack])) - if setup["ParameterScale"] == 1 + if haskey(inputs, "dfCO2Cap_slack") + dfCO2Price[!, :CO2_Mass_Slack] = convert(Array{Float64}, value.(EP[:vCO2Cap_slack])) + dfCO2Price[!, :CO2_Penalty] = convert(Array{Float64}, value.(EP[:eCCO2Cap_slack])) + if setup["ParameterScale"] == 1 dfCO2Price.CO2_Mass_Slack .*= ModelScalingFactor # Convert ktons to tons dfCO2Price.CO2_Penalty .*= ModelScalingFactor^2 # Convert Million$ to $ - end - end + end + end CSV.write(joinpath(path, "CO2_prices_and_penalties.csv"), dfCO2Price) diff --git a/src/write_outputs/dftranspose.jl b/src/write_outputs/dftranspose.jl index e482a2a37b..21c8295899 100644 --- a/src/write_outputs/dftranspose.jl +++ b/src/write_outputs/dftranspose.jl @@ -18,10 +18,11 @@ FIXME: This is for DataFrames@0.20.2, as used in GenX. Versions 0.21+ could use stack and unstack to make further changes while retaining the order """ function dftranspose(df::DataFrame, withhead::Bool) - if withhead - colnames = cat(:Row, Symbol.(df[!,1]), dims=1) - return DataFrame([[names(df)]; collect.(eachrow(df))], colnames) - else - return DataFrame([[names(df)]; collect.(eachrow(df))], [:Row; Symbol.("x",axes(df, 1))]) - end + if withhead + colnames = cat(:Row, Symbol.(df[!, 1]), dims = 1) + return DataFrame([[names(df)]; collect.(eachrow(df))], colnames) + else + return DataFrame([[names(df)]; collect.(eachrow(df))], + [:Row; Symbol.("x", axes(df, 1))]) + end end # End dftranpose() diff --git a/src/write_outputs/energy_share_requirement/write_esr_prices.jl b/src/write_outputs/energy_share_requirement/write_esr_prices.jl index fe1127bb42..e9cccc46ae 100644 --- a/src/write_outputs/energy_share_requirement/write_esr_prices.jl +++ b/src/write_outputs/energy_share_requirement/write_esr_prices.jl @@ -1,17 +1,17 @@ function write_esr_prices(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - dfESR = DataFrame(ESR_Price = convert(Array{Float64}, dual.(EP[:cESRShare]))) - if setup["ParameterScale"] == 1 - dfESR[!,:ESR_Price] = dfESR[!,:ESR_Price] * ModelScalingFactor # Converting MillionUS$/GWh to US$/MWh - end + dfESR = DataFrame(ESR_Price = convert(Array{Float64}, dual.(EP[:cESRShare]))) + if setup["ParameterScale"] == 1 + dfESR[!, :ESR_Price] = dfESR[!, :ESR_Price] * ModelScalingFactor # Converting MillionUS$/GWh to US$/MWh + end - if haskey(inputs, "dfESR_slack") - dfESR[!,:ESR_AnnualSlack] = convert(Array{Float64}, value.(EP[:vESR_slack])) - dfESR[!,:ESR_AnnualPenalty] = convert(Array{Float64}, value.(EP[:eCESRSlack])) - if setup["ParameterScale"] == 1 - dfESR[!,:ESR_AnnualSlack] *= ModelScalingFactor # Converting GWh to MWh - dfESR[!,:ESR_AnnualPenalty] *= (ModelScalingFactor^2) # Converting MillionUSD to USD - end - end - CSV.write(joinpath(path, "ESR_prices_and_penalties.csv"), dfESR) - return dfESR + if haskey(inputs, "dfESR_slack") + dfESR[!, :ESR_AnnualSlack] = convert(Array{Float64}, value.(EP[:vESR_slack])) + dfESR[!, :ESR_AnnualPenalty] = convert(Array{Float64}, value.(EP[:eCESRSlack])) + if setup["ParameterScale"] == 1 + dfESR[!, :ESR_AnnualSlack] *= ModelScalingFactor # Converting GWh to MWh + dfESR[!, :ESR_AnnualPenalty] *= (ModelScalingFactor^2) # Converting MillionUSD to USD + end + end + CSV.write(joinpath(path, "ESR_prices_and_penalties.csv"), dfESR) + return dfESR end diff --git a/src/write_outputs/energy_share_requirement/write_esr_revenue.jl b/src/write_outputs/energy_share_requirement/write_esr_revenue.jl index c212caccf8..246853eaec 100644 --- a/src/write_outputs/energy_share_requirement/write_esr_revenue.jl +++ b/src/write_outputs/energy_share_requirement/write_esr_revenue.jl @@ -3,67 +3,82 @@ Function for reporting the renewable/clean credit revenue earned by each generator listed in the input file. GenX will print this file only when RPS/CES is modeled and the shadow price can be obtained form the solver. Each row corresponds to a generator, and each column starting from the 6th to the second last is the total revenue earned from each RPS constraint. The revenue is calculated as the total annual generation (if elgible for the corresponding constraint) multiplied by the RPS/CES price. The last column is the total revenue received from all constraint. The unit is \$. """ -function write_esr_revenue(path::AbstractString, inputs::Dict, setup::Dict, dfPower::DataFrame, dfESR::DataFrame, EP::Model) - gen = inputs["RESOURCES"] - regions = region.(gen) - clusters = cluster.(gen) - zones = zone_id.(gen) - rid = resource_id.(gen) +function write_esr_revenue(path::AbstractString, + inputs::Dict, + setup::Dict, + dfPower::DataFrame, + dfESR::DataFrame, + EP::Model) + gen = inputs["RESOURCES"] + regions = region.(gen) + clusters = cluster.(gen) + zones = zone_id.(gen) + rid = resource_id.(gen) - dfESRRev = DataFrame(region = regions, Resource = inputs["RESOURCE_NAMES"], zone = zones, Cluster = clusters, R_ID = rid) - G = inputs["G"] - nESR = inputs["nESR"] - weight = inputs["omega"] + dfESRRev = DataFrame(region = regions, + Resource = inputs["RESOURCE_NAMES"], + zone = zones, + Cluster = clusters, + R_ID = rid) + G = inputs["G"] + nESR = inputs["nESR"] + weight = inputs["omega"] # Load VRE-storage inputs - VRE_STOR = inputs["VRE_STOR"] # Set of VRE-STOR generators (indices) - - if !isempty(VRE_STOR) - gen_VRE_STOR = gen.VreStorage # Set of VRE-STOR generators (objects) - SOLAR = inputs["VS_SOLAR"] - WIND = inputs["VS_WIND"] - SOLAR_ONLY = setdiff(SOLAR, WIND) - WIND_ONLY = setdiff(WIND, SOLAR) - SOLAR_WIND = intersect(SOLAR, WIND) - end + VRE_STOR = inputs["VRE_STOR"] # Set of VRE-STOR generators (indices) - for i in 1:nESR - esr_col = Symbol("ESR_$i") - price = dfESR[i, :ESR_Price] - derated_annual_net_generation = dfPower[1:G,:AnnualSum] .* esr.(gen, tag=i) - revenue = derated_annual_net_generation * price - dfESRRev[!, esr_col] = revenue + if !isempty(VRE_STOR) + gen_VRE_STOR = gen.VreStorage # Set of VRE-STOR generators (objects) + SOLAR = inputs["VS_SOLAR"] + WIND = inputs["VS_WIND"] + SOLAR_ONLY = setdiff(SOLAR, WIND) + WIND_ONLY = setdiff(WIND, SOLAR) + SOLAR_WIND = intersect(SOLAR, WIND) + end - if !isempty(VRE_STOR) - if !isempty(SOLAR_ONLY) - solar_resources = ((gen_VRE_STOR.wind.==0) .& (gen_VRE_STOR.solar.!=0)) - dfESRRev[SOLAR, esr_col] = ( - value.(EP[:vP_SOLAR][SOLAR, :]).data - .* etainverter.(gen_VRE_STOR[solar_resources]) * weight - ) .* esr_vrestor.(gen_VRE_STOR[solar_resources], tag=i) * price - end - if !isempty(WIND_ONLY) - wind_resources = ((gen_VRE_STOR.wind.!=0) .& (gen_VRE_STOR.solar.==0)) - dfESRRev[WIND, esr_col] = ( - value.(EP[:vP_WIND][WIND, :]).data - * weight - ) .* esr_vrestor.(gen_VRE_STOR[wind_resources], tag=i) * price - end - if !isempty(SOLAR_WIND) - solar_and_wind_resources = ((gen_VRE_STOR.wind.!=0) .& (gen_VRE_STOR.solar.!=0)) - dfESRRev[SOLAR_WIND, esr_col] = ( - ( - (value.(EP[:vP_WIND][SOLAR_WIND, :]).data * weight) - .* esr_vrestor.(gen_VRE_STOR[solar_and_wind_resources], tag=i) * price - ) + ( - value.(EP[:vP_SOLAR][SOLAR_WIND, :]).data - .* etainverter.(gen_VRE_STOR[solar_and_wind_resources]) - * weight - ) .* esr_vrestor.(gen_VRE_STOR[solar_and_wind_resources], tag=i) * price - ) - end - end - end - dfESRRev.Total = sum(eachcol(dfESRRev[:, 6:nESR + 5])) - CSV.write(joinpath(path, "ESR_Revenue.csv"), dfESRRev) - return dfESRRev -end \ No newline at end of file + for i in 1:nESR + esr_col = Symbol("ESR_$i") + price = dfESR[i, :ESR_Price] + derated_annual_net_generation = dfPower[1:G, :AnnualSum] .* esr.(gen, tag = i) + revenue = derated_annual_net_generation * price + dfESRRev[!, esr_col] = revenue + + if !isempty(VRE_STOR) + if !isempty(SOLAR_ONLY) + solar_resources = ((gen_VRE_STOR.wind .== 0) .& (gen_VRE_STOR.solar .!= 0)) + dfESRRev[SOLAR, esr_col] = (value.(EP[:vP_SOLAR][SOLAR, :]).data + .* + etainverter.(gen_VRE_STOR[solar_resources]) * + weight) .* + esr_vrestor.(gen_VRE_STOR[solar_resources], + tag = i) * price + end + if !isempty(WIND_ONLY) + wind_resources = ((gen_VRE_STOR.wind .!= 0) .& (gen_VRE_STOR.solar .== 0)) + dfESRRev[WIND, esr_col] = (value.(EP[:vP_WIND][WIND, :]).data + * + weight) .* + esr_vrestor.(gen_VRE_STOR[wind_resources], + tag = i) * price + end + if !isempty(SOLAR_WIND) + solar_and_wind_resources = ((gen_VRE_STOR.wind .!= 0) .& + (gen_VRE_STOR.solar .!= 0)) + dfESRRev[SOLAR_WIND, esr_col] = (((value.(EP[:vP_WIND][SOLAR_WIND, + :]).data * weight) + .* + esr_vrestor.(gen_VRE_STOR[solar_and_wind_resources], + tag = i) * price) + + (value.(EP[:vP_SOLAR][SOLAR_WIND, :]).data + .* + etainverter.(gen_VRE_STOR[solar_and_wind_resources]) + * + weight) .* + esr_vrestor.(gen_VRE_STOR[solar_and_wind_resources], + tag = i) * price) + end + end + end + dfESRRev.Total = sum(eachcol(dfESRRev[:, 6:(nESR + 5)])) + CSV.write(joinpath(path, "ESR_Revenue.csv"), dfESRRev) + return dfESRRev +end diff --git a/src/write_outputs/hydrogen/write_hourly_matching_prices.jl b/src/write_outputs/hydrogen/write_hourly_matching_prices.jl index 393544e4e4..00d0ce3220 100644 --- a/src/write_outputs/hydrogen/write_hourly_matching_prices.jl +++ b/src/write_outputs/hydrogen/write_hourly_matching_prices.jl @@ -1,17 +1,25 @@ -function write_hourly_matching_prices(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - T = inputs["T"] # Number of time steps (hours) - Z = inputs["Z"] # Number of zones - scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 +function write_hourly_matching_prices(path::AbstractString, + inputs::Dict, + setup::Dict, + EP::Model) + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones + scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 - ## Extract dual variables of constraints - dfHourlyMatchPrices = DataFrame(Zone = 1:Z) # The unit is $/MWh - # Dividing dual variable for each hour with corresponding hourly weight to retrieve marginal cost of the constraint - dfHourlyMatchPrices = hcat(dfHourlyMatchPrices, DataFrame(dual.(EP[:cHourlyMatching]).data./transpose(inputs["omega"])*scale_factor, :auto)) + ## Extract dual variables of constraints + dfHourlyMatchPrices = DataFrame(Zone = 1:Z) # The unit is $/MWh + # Dividing dual variable for each hour with corresponding hourly weight to retrieve marginal cost of the constraint + dfHourlyMatchPrices = hcat(dfHourlyMatchPrices, + DataFrame(dual.(EP[:cHourlyMatching]).data ./ transpose(inputs["omega"]) * + scale_factor, + :auto)) - auxNew_Names=[Symbol("Zone");[Symbol("t$t") for t in 1:T]] - rename!(dfHourlyMatchPrices,auxNew_Names) + auxNew_Names = [Symbol("Zone"); [Symbol("t$t") for t in 1:T]] + rename!(dfHourlyMatchPrices, auxNew_Names) - CSV.write(joinpath(path, "hourly_matching_prices.csv"), dftranspose(dfHourlyMatchPrices, false), header=false) + CSV.write(joinpath(path, "hourly_matching_prices.csv"), + dftranspose(dfHourlyMatchPrices, false), + header = false) - return nothing + return nothing end diff --git a/src/write_outputs/hydrogen/write_hydrogen_prices.jl b/src/write_outputs/hydrogen/write_hydrogen_prices.jl index 1d7d905491..5b3903a5a2 100644 --- a/src/write_outputs/hydrogen/write_hydrogen_prices.jl +++ b/src/write_outputs/hydrogen/write_hydrogen_prices.jl @@ -1,7 +1,8 @@ function write_hydrogen_prices(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - scale_factor = setup["ParameterScale"] == 1 ? 10^6 : 1 # If ParameterScale==1, costs are in millions of $ - dfHydrogenPrice = DataFrame(Hydrogen_Price_Per_Tonne = convert(Array{Float64}, dual.(EP[:cHydrogenMin])*scale_factor)) + scale_factor = setup["ParameterScale"] == 1 ? 10^6 : 1 # If ParameterScale==1, costs are in millions of $ + dfHydrogenPrice = DataFrame(Hydrogen_Price_Per_Tonne = convert(Array{Float64}, + dual.(EP[:cHydrogenMin]) * scale_factor)) - CSV.write(joinpath(path, "hydrogen_prices.csv"), dfHydrogenPrice) - return nothing + CSV.write(joinpath(path, "hydrogen_prices.csv"), dfHydrogenPrice) + return nothing end diff --git a/src/write_outputs/long_duration_storage/write_opwrap_lds_dstor.jl b/src/write_outputs/long_duration_storage/write_opwrap_lds_dstor.jl index a5ce31ec7b..875d8e6f86 100644 --- a/src/write_outputs/long_duration_storage/write_opwrap_lds_dstor.jl +++ b/src/write_outputs/long_duration_storage/write_opwrap_lds_dstor.jl @@ -1,30 +1,32 @@ function write_opwrap_lds_dstor(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - ## Extract data frames from input dictionary - gen = inputs["RESOURCES"] - zones = zone_id.(gen) + ## Extract data frames from input dictionary + gen = inputs["RESOURCES"] + zones = zone_id.(gen) - W = inputs["REP_PERIOD"] # Number of subperiods - G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) + W = inputs["REP_PERIOD"] # Number of subperiods + G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) - #Excess inventory of storage period built up during representative period w - dfdStorage = DataFrame(Resource = inputs["RESOURCE_NAMES"], Zone = zones) - dsoc = zeros(G,W) - for i in 1:G - if i in inputs["STOR_LONG_DURATION"] - dsoc[i,:] = value.(EP[:vdSOC])[i,:] - end - if !isempty(inputs["VRE_STOR"]) - if i in inputs["VS_LDS"] - dsoc[i,:] = value.(EP[:vdSOC_VRE_STOR])[i,:] - end - end - end - if setup["ParameterScale"] == 1 - dsoc *= ModelScalingFactor - end - - dfdStorage = hcat(dfdStorage, DataFrame(dsoc, :auto)) - auxNew_Names=[Symbol("Resource");Symbol("Zone");[Symbol("w$t") for t in 1:W]] - rename!(dfdStorage,auxNew_Names) - CSV.write(joinpath(path, "dStorage.csv"), dftranspose(dfdStorage, false), header=false) + #Excess inventory of storage period built up during representative period w + dfdStorage = DataFrame(Resource = inputs["RESOURCE_NAMES"], Zone = zones) + dsoc = zeros(G, W) + for i in 1:G + if i in inputs["STOR_LONG_DURATION"] + dsoc[i, :] = value.(EP[:vdSOC])[i, :] + end + if !isempty(inputs["VRE_STOR"]) + if i in inputs["VS_LDS"] + dsoc[i, :] = value.(EP[:vdSOC_VRE_STOR])[i, :] + end + end + end + if setup["ParameterScale"] == 1 + dsoc *= ModelScalingFactor + end + + dfdStorage = hcat(dfdStorage, DataFrame(dsoc, :auto)) + auxNew_Names = [Symbol("Resource"); Symbol("Zone"); [Symbol("w$t") for t in 1:W]] + rename!(dfdStorage, auxNew_Names) + CSV.write(joinpath(path, "dStorage.csv"), + dftranspose(dfdStorage, false), + header = false) end diff --git a/src/write_outputs/long_duration_storage/write_opwrap_lds_stor_init.jl b/src/write_outputs/long_duration_storage/write_opwrap_lds_stor_init.jl index bf1bda48aa..81587b655a 100644 --- a/src/write_outputs/long_duration_storage/write_opwrap_lds_stor_init.jl +++ b/src/write_outputs/long_duration_storage/write_opwrap_lds_stor_init.jl @@ -1,30 +1,35 @@ -function write_opwrap_lds_stor_init(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - ## Extract data frames from input dictionary - gen = inputs["RESOURCES"] - zones = zone_id.(gen) +function write_opwrap_lds_stor_init(path::AbstractString, + inputs::Dict, + setup::Dict, + EP::Model) + ## Extract data frames from input dictionary + gen = inputs["RESOURCES"] + zones = zone_id.(gen) - G = inputs["G"] + G = inputs["G"] - # Initial level of storage in each modeled period - NPeriods = size(inputs["Period_Map"])[1] - dfStorageInit = DataFrame(Resource = inputs["RESOURCE_NAMES"], Zone = zones) - socw = zeros(G,NPeriods) - for i in 1:G - if i in inputs["STOR_LONG_DURATION"] - socw[i,:] = value.(EP[:vSOCw])[i,:] - end - if !isempty(inputs["VRE_STOR"]) - if i in inputs["VS_LDS"] - socw[i, :] = value.(EP[:vSOCw_VRE_STOR][i,:]) - end - end - end - if setup["ParameterScale"] == 1 - socw *= ModelScalingFactor - end + # Initial level of storage in each modeled period + NPeriods = size(inputs["Period_Map"])[1] + dfStorageInit = DataFrame(Resource = inputs["RESOURCE_NAMES"], Zone = zones) + socw = zeros(G, NPeriods) + for i in 1:G + if i in inputs["STOR_LONG_DURATION"] + socw[i, :] = value.(EP[:vSOCw])[i, :] + end + if !isempty(inputs["VRE_STOR"]) + if i in inputs["VS_LDS"] + socw[i, :] = value.(EP[:vSOCw_VRE_STOR][i, :]) + end + end + end + if setup["ParameterScale"] == 1 + socw *= ModelScalingFactor + end - dfStorageInit = hcat(dfStorageInit, DataFrame(socw, :auto)) - auxNew_Names=[Symbol("Resource");Symbol("Zone");[Symbol("n$t") for t in 1:NPeriods]] - rename!(dfStorageInit,auxNew_Names) - CSV.write(joinpath(path, "StorageInit.csv"), dftranspose(dfStorageInit, false), header=false) + dfStorageInit = hcat(dfStorageInit, DataFrame(socw, :auto)) + auxNew_Names = [Symbol("Resource"); Symbol("Zone"); [Symbol("n$t") for t in 1:NPeriods]] + rename!(dfStorageInit, auxNew_Names) + CSV.write(joinpath(path, "StorageInit.csv"), + dftranspose(dfStorageInit, false), + header = false) end diff --git a/src/write_outputs/min_max_capacity_requirement/write_maximum_capacity_requirement.jl b/src/write_outputs/min_max_capacity_requirement/write_maximum_capacity_requirement.jl index 24e3a7f4e6..997057955e 100644 --- a/src/write_outputs/min_max_capacity_requirement/write_maximum_capacity_requirement.jl +++ b/src/write_outputs/min_max_capacity_requirement/write_maximum_capacity_requirement.jl @@ -1,15 +1,19 @@ -function write_maximum_capacity_requirement(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) +function write_maximum_capacity_requirement(path::AbstractString, + inputs::Dict, + setup::Dict, + EP::Model) NumberOfMaxCapReqs = inputs["NumberOfMaxCapReqs"] - dfMaxCapPrice = DataFrame(Constraint = [Symbol("MaxCapReq_$maxcap") for maxcap = 1:NumberOfMaxCapReqs], - Price=-dual.(EP[:cZoneMaxCapReq])) + dfMaxCapPrice = DataFrame(Constraint = [Symbol("MaxCapReq_$maxcap") + for maxcap in 1:NumberOfMaxCapReqs], + Price = -dual.(EP[:cZoneMaxCapReq])) scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 dfMaxCapPrice.Price *= scale_factor if haskey(inputs, "MaxCapPriceCap") - dfMaxCapPrice[!,:Slack] = convert(Array{Float64}, value.(EP[:vMaxCap_slack])) - dfMaxCapPrice[!,:Penalty] = convert(Array{Float64}, value.(EP[:eCMaxCap_slack])) + dfMaxCapPrice[!, :Slack] = convert(Array{Float64}, value.(EP[:vMaxCap_slack])) + dfMaxCapPrice[!, :Penalty] = convert(Array{Float64}, value.(EP[:eCMaxCap_slack])) dfMaxCapPrice.Slack *= scale_factor # Convert GW to MW dfMaxCapPrice.Penalty *= scale_factor^2 # Convert Million $ to $ end diff --git a/src/write_outputs/min_max_capacity_requirement/write_minimum_capacity_requirement.jl b/src/write_outputs/min_max_capacity_requirement/write_minimum_capacity_requirement.jl index a1bfe1d28d..346213ae61 100644 --- a/src/write_outputs/min_max_capacity_requirement/write_minimum_capacity_requirement.jl +++ b/src/write_outputs/min_max_capacity_requirement/write_minimum_capacity_requirement.jl @@ -1,15 +1,19 @@ -function write_minimum_capacity_requirement(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) +function write_minimum_capacity_requirement(path::AbstractString, + inputs::Dict, + setup::Dict, + EP::Model) NumberOfMinCapReqs = inputs["NumberOfMinCapReqs"] - dfMinCapPrice = DataFrame(Constraint = [Symbol("MinCapReq_$mincap") for mincap = 1:NumberOfMinCapReqs], - Price= dual.(EP[:cZoneMinCapReq])) + dfMinCapPrice = DataFrame(Constraint = [Symbol("MinCapReq_$mincap") + for mincap in 1:NumberOfMinCapReqs], + Price = dual.(EP[:cZoneMinCapReq])) scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 dfMinCapPrice.Price *= scale_factor # Convert Million $/GW to $/MW if haskey(inputs, "MinCapPriceCap") - dfMinCapPrice[!,:Slack] = convert(Array{Float64}, value.(EP[:vMinCap_slack])) - dfMinCapPrice[!,:Penalty] = convert(Array{Float64}, value.(EP[:eCMinCap_slack])) + dfMinCapPrice[!, :Slack] = convert(Array{Float64}, value.(EP[:vMinCap_slack])) + dfMinCapPrice[!, :Penalty] = convert(Array{Float64}, value.(EP[:eCMinCap_slack])) dfMinCapPrice.Slack *= scale_factor # Convert GW to MW dfMinCapPrice.Penalty *= scale_factor^2 # Convert Million $ to $ end diff --git a/src/write_outputs/reserves/write_operating_reserve_price_revenue.jl b/src/write_outputs/reserves/write_operating_reserve_price_revenue.jl index 25a2b4a760..c3d4f389a4 100644 --- a/src/write_outputs/reserves/write_operating_reserve_price_revenue.jl +++ b/src/write_outputs/reserves/write_operating_reserve_price_revenue.jl @@ -7,36 +7,47 @@ Function for reporting the operating reserve and regulation revenue earned by ge The last column is the total revenue received from all operating reserve and regulation constraints. As a reminder, GenX models the operating reserve and regulation at the time-dependent level, and each constraint either stands for an overall market or a locality constraint. """ -function write_operating_reserve_regulation_revenue(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 +function write_operating_reserve_regulation_revenue(path::AbstractString, + inputs::Dict, + setup::Dict, + EP::Model) + scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 - gen = inputs["RESOURCES"] - RSV = inputs["RSV"] - REG = inputs["REG"] + gen = inputs["RESOURCES"] + RSV = inputs["RSV"] + REG = inputs["REG"] regions = region.(gen) clusters = cluster.(gen) zones = zone_id.(gen) names = inputs["RESOURCE_NAMES"] - dfOpRsvRevenue = DataFrame(Region = regions[RSV], Resource = names[RSV], Zone = zones[RSV], Cluster = clusters[RSV], AnnualSum = Array{Float64}(undef, length(RSV)),) - dfOpRegRevenue = DataFrame(Region = regions[REG], Resource = names[REG], Zone = zones[REG], Cluster = clusters[REG], AnnualSum = Array{Float64}(undef, length(REG)),) - - weighted_reg_price = operating_regulation_price(EP, inputs, setup) - weighted_rsv_price = operating_reserve_price(EP, inputs, setup) + dfOpRsvRevenue = DataFrame(Region = regions[RSV], + Resource = names[RSV], + Zone = zones[RSV], + Cluster = clusters[RSV], + AnnualSum = Array{Float64}(undef, length(RSV))) + dfOpRegRevenue = DataFrame(Region = regions[REG], + Resource = names[REG], + Zone = zones[REG], + Cluster = clusters[REG], + AnnualSum = Array{Float64}(undef, length(REG))) + + weighted_reg_price = operating_regulation_price(EP, inputs, setup) + weighted_rsv_price = operating_reserve_price(EP, inputs, setup) - rsvrevenue = value.(EP[:vRSV][RSV, :].data) .* transpose(weighted_rsv_price) - regrevenue = value.(EP[:vREG][REG, :].data) .* transpose(weighted_reg_price) + rsvrevenue = value.(EP[:vRSV][RSV, :].data) .* transpose(weighted_rsv_price) + regrevenue = value.(EP[:vREG][REG, :].data) .* transpose(weighted_reg_price) - rsvrevenue *= scale_factor - regrevenue *= scale_factor + rsvrevenue *= scale_factor + regrevenue *= scale_factor - dfOpRsvRevenue.AnnualSum .= rsvrevenue * inputs["omega"] - dfOpRegRevenue.AnnualSum .= regrevenue * inputs["omega"] + dfOpRsvRevenue.AnnualSum .= rsvrevenue * inputs["omega"] + dfOpRegRevenue.AnnualSum .= regrevenue * inputs["omega"] - write_simple_csv(joinpath(path, "OperatingReserveRevenue.csv"), dfOpRsvRevenue) - write_simple_csv(joinpath(path, "OperatingRegulationRevenue.csv"), dfOpRegRevenue) - return dfOpRegRevenue, dfOpRsvRevenue + write_simple_csv(joinpath(path, "OperatingReserveRevenue.csv"), dfOpRsvRevenue) + write_simple_csv(joinpath(path, "OperatingRegulationRevenue.csv"), dfOpRegRevenue) + return dfOpRegRevenue, dfOpRsvRevenue end @doc raw""" diff --git a/src/write_outputs/reserves/write_reg.jl b/src/write_outputs/reserves/write_reg.jl index 4b984fcc14..7d7ca1efd6 100644 --- a/src/write_outputs/reserves/write_reg.jl +++ b/src/write_outputs/reserves/write_reg.jl @@ -1,20 +1,20 @@ function write_reg(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - REG = inputs["REG"] - scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 + REG = inputs["REG"] + scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 - resources = inputs["RESOURCE_NAMES"][REG] - zones = inputs["R_ZONES"][REG] - # Regulation contributions for each resource in each time step - reg = value.(EP[:vREG][REG, :].data) * scale_factor + resources = inputs["RESOURCE_NAMES"][REG] + zones = inputs["R_ZONES"][REG] + # Regulation contributions for each resource in each time step + reg = value.(EP[:vREG][REG, :].data) * scale_factor - dfReg = DataFrame(Resource = resources, Zone = zones) - dfReg.AnnualSum = reg * inputs["omega"] + dfReg = DataFrame(Resource = resources, Zone = zones) + dfReg.AnnualSum = reg * inputs["omega"] - filepath = joinpath(path, "reg.csv") - if setup["WriteOutputs"] == "annual" - write_annual(filepath, dfReg) - else # setup["WriteOutputs"] == "full" - write_fulltimeseries(filepath, reg, dfReg) - end - return nothing + filepath = joinpath(path, "reg.csv") + if setup["WriteOutputs"] == "annual" + write_annual(filepath, dfReg) + else # setup["WriteOutputs"] == "full" + write_fulltimeseries(filepath, reg, dfReg) + end + return nothing end diff --git a/src/write_outputs/reserves/write_rsv.jl b/src/write_outputs/reserves/write_rsv.jl index ebfbca5725..f48b40c034 100644 --- a/src/write_outputs/reserves/write_rsv.jl +++ b/src/write_outputs/reserves/write_rsv.jl @@ -1,32 +1,37 @@ function write_rsv(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - T = inputs["T"] # Number of time steps (hours) - RSV = inputs["RSV"] - scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 + T = inputs["T"] # Number of time steps (hours) + RSV = inputs["RSV"] + scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 - resources = inputs["RESOURCE_NAMES"][RSV] - zones = inputs["R_ZONES"][RSV] - rsv = value.(EP[:vRSV][RSV, :].data) * scale_factor + resources = inputs["RESOURCE_NAMES"][RSV] + zones = inputs["R_ZONES"][RSV] + rsv = value.(EP[:vRSV][RSV, :].data) * scale_factor - dfRsv = DataFrame(Resource = resources, Zone = zones) + dfRsv = DataFrame(Resource = resources, Zone = zones) - dfRsv.AnnualSum = rsv * inputs["omega"] + dfRsv.AnnualSum = rsv * inputs["omega"] - if setup["WriteOutputs"] == "annual" - write_annual(joinpath(path, "reg_dn.csv"), dfRsv) - else # setup["WriteOutputs"] == "full" - unmet_vec = value.(EP[:vUNMET_RSV]) * scale_factor - total_unmet = sum(unmet_vec) - dfRsv = hcat(dfRsv, DataFrame(rsv, :auto)) - auxNew_Names=[Symbol("Resource");Symbol("Zone");Symbol("AnnualSum");[Symbol("t$t") for t in 1:T]] - rename!(dfRsv,auxNew_Names) - - total = DataFrame(["Total" 0 sum(dfRsv.AnnualSum) zeros(1, T)], :auto) - unmet = DataFrame(["unmet" 0 total_unmet zeros(1, T)], :auto) - total[!, 4:T+3] .= sum(rsv, dims = 1) - unmet[!, 4:T+3] .= transpose(unmet_vec) - rename!(total,auxNew_Names) - rename!(unmet,auxNew_Names) - dfRsv = vcat(dfRsv, unmet, total) - CSV.write(joinpath(path, "reg_dn.csv"), dftranspose(dfRsv, false), writeheader=false) - end + if setup["WriteOutputs"] == "annual" + write_annual(joinpath(path, "reg_dn.csv"), dfRsv) + else # setup["WriteOutputs"] == "full" + unmet_vec = value.(EP[:vUNMET_RSV]) * scale_factor + total_unmet = sum(unmet_vec) + dfRsv = hcat(dfRsv, DataFrame(rsv, :auto)) + auxNew_Names = [Symbol("Resource"); + Symbol("Zone"); + Symbol("AnnualSum"); + [Symbol("t$t") for t in 1:T]] + rename!(dfRsv, auxNew_Names) + + total = DataFrame(["Total" 0 sum(dfRsv.AnnualSum) zeros(1, T)], :auto) + unmet = DataFrame(["unmet" 0 total_unmet zeros(1, T)], :auto) + total[!, 4:(T + 3)] .= sum(rsv, dims = 1) + unmet[!, 4:(T + 3)] .= transpose(unmet_vec) + rename!(total, auxNew_Names) + rename!(unmet, auxNew_Names) + dfRsv = vcat(dfRsv, unmet, total) + CSV.write(joinpath(path, "reg_dn.csv"), + dftranspose(dfRsv, false), + writeheader = false) + end end diff --git a/src/write_outputs/transmission/write_nw_expansion.jl b/src/write_outputs/transmission/write_nw_expansion.jl index 973248950c..f89e1bfe1f 100644 --- a/src/write_outputs/transmission/write_nw_expansion.jl +++ b/src/write_outputs/transmission/write_nw_expansion.jl @@ -1,23 +1,23 @@ function write_nw_expansion(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - L = inputs["L"] # Number of transmission lines + L = inputs["L"] # Number of transmission lines - # Transmission network reinforcements - transcap = zeros(L) - for i in 1:L - if i in inputs["EXPANSION_LINES"] - transcap[i] = value.(EP[:vNEW_TRANS_CAP][i]) - end - end + # Transmission network reinforcements + transcap = zeros(L) + for i in 1:L + if i in inputs["EXPANSION_LINES"] + transcap[i] = value.(EP[:vNEW_TRANS_CAP][i]) + end + end - dfTransCap = DataFrame( - Line = 1:L, New_Trans_Capacity = convert(Array{Float64}, transcap), - Cost_Trans_Capacity = convert(Array{Float64}, transcap.*inputs["pC_Line_Reinforcement"]), - ) + dfTransCap = DataFrame(Line = 1:L, + New_Trans_Capacity = convert(Array{Float64}, transcap), + Cost_Trans_Capacity = convert(Array{Float64}, + transcap .* inputs["pC_Line_Reinforcement"])) - if setup["ParameterScale"] == 1 - dfTransCap.New_Trans_Capacity *= ModelScalingFactor # GW to MW - dfTransCap.Cost_Trans_Capacity *= ModelScalingFactor^2 # MUSD to USD - end + if setup["ParameterScale"] == 1 + dfTransCap.New_Trans_Capacity *= ModelScalingFactor # GW to MW + dfTransCap.Cost_Trans_Capacity *= ModelScalingFactor^2 # MUSD to USD + end - CSV.write(joinpath(path, "network_expansion.csv"), dfTransCap) + CSV.write(joinpath(path, "network_expansion.csv"), dfTransCap) end diff --git a/src/write_outputs/transmission/write_transmission_flows.jl b/src/write_outputs/transmission/write_transmission_flows.jl index 74f6f779dc..8494a38511 100644 --- a/src/write_outputs/transmission/write_transmission_flows.jl +++ b/src/write_outputs/transmission/write_transmission_flows.jl @@ -1,25 +1,28 @@ -function write_transmission_flows(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - # Transmission related values - T = inputs["T"] # Number of time steps (hours) - L = inputs["L"] # Number of transmission lines - # Power flows on transmission lines at each time step - dfFlow = DataFrame(Line = 1:L) - flow = value.(EP[:vFLOW]) - if setup["ParameterScale"] == 1 - flow *= ModelScalingFactor - end +function write_transmission_flows(path::AbstractString, + inputs::Dict, + setup::Dict, + EP::Model) + # Transmission related values + T = inputs["T"] # Number of time steps (hours) + L = inputs["L"] # Number of transmission lines + # Power flows on transmission lines at each time step + dfFlow = DataFrame(Line = 1:L) + flow = value.(EP[:vFLOW]) + if setup["ParameterScale"] == 1 + flow *= ModelScalingFactor + end - filepath = joinpath(path, "flow.csv") - if setup["WriteOutputs"] == "annual" - dfFlow.AnnualSum = flow * inputs["omega"] - total = DataFrame(["Total" sum(dfFlow.AnnualSum)], [:Line, :AnnualSum]) - dfFlow = vcat(dfFlow, total) - CSV.write(filepath, dfFlow) - else # setup["WriteOutputs"] == "full" - dfFlow = hcat(dfFlow, DataFrame(flow, :auto)) - auxNew_Names=[Symbol("Line");[Symbol("t$t") for t in 1:T]] - rename!(dfFlow,auxNew_Names) - CSV.write(filepath, dftranspose(dfFlow, false), writeheader=false) - end - return nothing + filepath = joinpath(path, "flow.csv") + if setup["WriteOutputs"] == "annual" + dfFlow.AnnualSum = flow * inputs["omega"] + total = DataFrame(["Total" sum(dfFlow.AnnualSum)], [:Line, :AnnualSum]) + dfFlow = vcat(dfFlow, total) + CSV.write(filepath, dfFlow) + else # setup["WriteOutputs"] == "full" + dfFlow = hcat(dfFlow, DataFrame(flow, :auto)) + auxNew_Names = [Symbol("Line"); [Symbol("t$t") for t in 1:T]] + rename!(dfFlow, auxNew_Names) + CSV.write(filepath, dftranspose(dfFlow, false), writeheader = false) + end + return nothing end diff --git a/src/write_outputs/transmission/write_transmission_losses.jl b/src/write_outputs/transmission/write_transmission_losses.jl index 8f5bb51977..b82204acd0 100644 --- a/src/write_outputs/transmission/write_transmission_losses.jl +++ b/src/write_outputs/transmission/write_transmission_losses.jl @@ -1,29 +1,35 @@ -function write_transmission_losses(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - T = inputs["T"] # Number of time steps (hours) - L = inputs["L"] # Number of transmission lines - LOSS_LINES = inputs["LOSS_LINES"] - # Power losses for transmission between zones at each time step - dfTLosses = DataFrame(Line = 1:L) - tlosses = zeros(L, T) - tlosses[LOSS_LINES, :] = value.(EP[:vTLOSS][LOSS_LINES, :]) - if setup["ParameterScale"] == 1 - tlosses[LOSS_LINES, :] *= ModelScalingFactor - end +function write_transmission_losses(path::AbstractString, + inputs::Dict, + setup::Dict, + EP::Model) + T = inputs["T"] # Number of time steps (hours) + L = inputs["L"] # Number of transmission lines + LOSS_LINES = inputs["LOSS_LINES"] + # Power losses for transmission between zones at each time step + dfTLosses = DataFrame(Line = 1:L) + tlosses = zeros(L, T) + tlosses[LOSS_LINES, :] = value.(EP[:vTLOSS][LOSS_LINES, :]) + if setup["ParameterScale"] == 1 + tlosses[LOSS_LINES, :] *= ModelScalingFactor + end - dfTLosses.AnnualSum = tlosses * inputs["omega"] - - if setup["WriteOutputs"] == "annual" - total = DataFrame(["Total" sum(dfTLosses.AnnualSum)], [:Line, :AnnualSum]) - dfTLosses = vcat(dfTLosses, total) - CSV.write(joinpath(path, "tlosses.csv"), dfTLosses) - else - dfTLosses = hcat(dfTLosses, DataFrame(tlosses, :auto)) - auxNew_Names=[Symbol("Line");Symbol("AnnualSum");[Symbol("t$t") for t in 1:T]] - rename!(dfTLosses,auxNew_Names) - total = DataFrame(["Total" sum(dfTLosses.AnnualSum) fill(0.0, (1,T))], auxNew_Names) - total[:, 3:T+2] .= sum(tlosses, dims = 1) - dfTLosses = vcat(dfTLosses, total) - CSV.write(joinpath(path, "tlosses.csv"), dftranspose(dfTLosses, false), writeheader=false) - end - return nothing + dfTLosses.AnnualSum = tlosses * inputs["omega"] + + if setup["WriteOutputs"] == "annual" + total = DataFrame(["Total" sum(dfTLosses.AnnualSum)], [:Line, :AnnualSum]) + dfTLosses = vcat(dfTLosses, total) + CSV.write(joinpath(path, "tlosses.csv"), dfTLosses) + else + dfTLosses = hcat(dfTLosses, DataFrame(tlosses, :auto)) + auxNew_Names = [Symbol("Line"); Symbol("AnnualSum"); [Symbol("t$t") for t in 1:T]] + rename!(dfTLosses, auxNew_Names) + total = DataFrame(["Total" sum(dfTLosses.AnnualSum) fill(0.0, (1, T))], + auxNew_Names) + total[:, 3:(T + 2)] .= sum(tlosses, dims = 1) + dfTLosses = vcat(dfTLosses, total) + CSV.write(joinpath(path, "tlosses.csv"), + dftranspose(dfTLosses, false), + writeheader = false) + end + return nothing end diff --git a/src/write_outputs/ucommit/write_commit.jl b/src/write_outputs/ucommit/write_commit.jl index 685ad53e0a..bf8e712640 100644 --- a/src/write_outputs/ucommit/write_commit.jl +++ b/src/write_outputs/ucommit/write_commit.jl @@ -1,15 +1,14 @@ function write_commit(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - - COMMIT = inputs["COMMIT"] - T = inputs["T"] + COMMIT = inputs["COMMIT"] + T = inputs["T"] - # Commitment state for each resource in each time step - resources = inputs["RESOURCE_NAMES"][COMMIT] - zones = inputs["R_ZONES"][COMMIT] - commit = value.(EP[:vCOMMIT][COMMIT, :].data) - dfCommit = DataFrame(Resource = resources, Zone = zones) - dfCommit = hcat(dfCommit, DataFrame(commit, :auto)) - auxNew_Names=[Symbol("Resource");Symbol("Zone");[Symbol("t$t") for t in 1:T]] - rename!(dfCommit,auxNew_Names) - CSV.write(joinpath(path, "commit.csv"), dftranspose(dfCommit, false), header=false) + # Commitment state for each resource in each time step + resources = inputs["RESOURCE_NAMES"][COMMIT] + zones = inputs["R_ZONES"][COMMIT] + commit = value.(EP[:vCOMMIT][COMMIT, :].data) + dfCommit = DataFrame(Resource = resources, Zone = zones) + dfCommit = hcat(dfCommit, DataFrame(commit, :auto)) + auxNew_Names = [Symbol("Resource"); Symbol("Zone"); [Symbol("t$t") for t in 1:T]] + rename!(dfCommit, auxNew_Names) + CSV.write(joinpath(path, "commit.csv"), dftranspose(dfCommit, false), header = false) end diff --git a/src/write_outputs/ucommit/write_shutdown.jl b/src/write_outputs/ucommit/write_shutdown.jl index 56325b25f6..8a726a3367 100644 --- a/src/write_outputs/ucommit/write_shutdown.jl +++ b/src/write_outputs/ucommit/write_shutdown.jl @@ -1,19 +1,19 @@ function write_shutdown(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - # Operational decision variable states - COMMIT = inputs["COMMIT"] - zones = inputs["R_ZONES"][COMMIT] - # Shutdown state for each resource in each time step - shut = value.(EP[:vSHUT][COMMIT, :].data) - resources = inputs["RESOURCE_NAMES"][COMMIT] + # Operational decision variable states + COMMIT = inputs["COMMIT"] + zones = inputs["R_ZONES"][COMMIT] + # Shutdown state for each resource in each time step + shut = value.(EP[:vSHUT][COMMIT, :].data) + resources = inputs["RESOURCE_NAMES"][COMMIT] - dfShutdown = DataFrame(Resource = resources, Zone = zones) - dfShutdown.AnnualSum = shut * inputs["omega"] + dfShutdown = DataFrame(Resource = resources, Zone = zones) + dfShutdown.AnnualSum = shut * inputs["omega"] - filepath = joinpath(path, "shutdown.csv") - if setup["WriteOutputs"] == "annual" - write_annual(filepath, dfShutdown) - else # setup["WriteOutputs"] == "full" - write_fulltimeseries(filepath, shut, dfShutdown) - end - return nothing + filepath = joinpath(path, "shutdown.csv") + if setup["WriteOutputs"] == "annual" + write_annual(filepath, dfShutdown) + else # setup["WriteOutputs"] == "full" + write_fulltimeseries(filepath, shut, dfShutdown) + end + return nothing end diff --git a/src/write_outputs/ucommit/write_start.jl b/src/write_outputs/ucommit/write_start.jl index 461d522a17..be23be46bd 100644 --- a/src/write_outputs/ucommit/write_start.jl +++ b/src/write_outputs/ucommit/write_start.jl @@ -1,19 +1,18 @@ function write_start(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) + COMMIT = inputs["COMMIT"] + # Startup state for each resource in each time step + resources = inputs["RESOURCE_NAMES"][COMMIT] + zones = inputs["R_ZONES"][COMMIT] - COMMIT = inputs["COMMIT"] - # Startup state for each resource in each time step - resources = inputs["RESOURCE_NAMES"][COMMIT] - zones = inputs["R_ZONES"][COMMIT] + dfStart = DataFrame(Resource = resources, Zone = zones) + start = value.(EP[:vSTART][COMMIT, :].data) + dfStart.AnnualSum = start * inputs["omega"] - dfStart = DataFrame(Resource = resources, Zone = zones) - start = value.(EP[:vSTART][COMMIT, :].data) - dfStart.AnnualSum = start * inputs["omega"] - - filepath = joinpath(path, "start.csv") - if setup["WriteOutputs"] == "annual" - write_annual(filepath, dfStart) - else # setup["WriteOutputs"] == "full" - write_fulltimeseries(filepath, start, dfStart) - end - return nothing + filepath = joinpath(path, "start.csv") + if setup["WriteOutputs"] == "annual" + write_annual(filepath, dfStart) + else # setup["WriteOutputs"] == "full" + write_fulltimeseries(filepath, start, dfStart) + end + return nothing end diff --git a/src/write_outputs/write_angles.jl b/src/write_outputs/write_angles.jl index f638b37e52..b93870354f 100644 --- a/src/write_outputs/write_angles.jl +++ b/src/write_outputs/write_angles.jl @@ -4,17 +4,19 @@ Function for reporting the bus angles for each model zone and time step if the DC_OPF flag is activated """ function write_angles(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - T = inputs["T"] # Number of time steps (hours) - Z = inputs["Z"] # Number of zones + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones - dfAngles = DataFrame(Zone = 1:Z) - angles = value.(EP[:vANGLE]) - dfAngles = hcat(dfAngles, DataFrame(angles, :auto)) + dfAngles = DataFrame(Zone = 1:Z) + angles = value.(EP[:vANGLE]) + dfAngles = hcat(dfAngles, DataFrame(angles, :auto)) - auxNew_Names=[Symbol("Zone");[Symbol("t$t") for t in 1:T]] - rename!(dfAngles,auxNew_Names) + auxNew_Names = [Symbol("Zone"); [Symbol("t$t") for t in 1:T]] + rename!(dfAngles, auxNew_Names) - ## Linear configuration final output - CSV.write(joinpath(path, "angles.csv"), dftranspose(dfAngles, false), writeheader=false) - return nothing + ## Linear configuration final output + CSV.write(joinpath(path, "angles.csv"), + dftranspose(dfAngles, false), + writeheader = false) + return nothing end diff --git a/src/write_outputs/write_capacity.jl b/src/write_outputs/write_capacity.jl index f102ced874..99e4797ecc 100755 --- a/src/write_outputs/write_capacity.jl +++ b/src/write_outputs/write_capacity.jl @@ -4,129 +4,129 @@ Function for writing the diferent capacities for the different generation technologies (starting capacities or, existing capacities, retired capacities, and new-built capacities). """ function write_capacity(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) + gen = inputs["RESOURCES"] - gen = inputs["RESOURCES"] + MultiStage = setup["MultiStage"] - MultiStage = setup["MultiStage"] - - # Capacity decisions - capdischarge = zeros(size(inputs["RESOURCE_NAMES"])) - for i in inputs["NEW_CAP"] - if i in inputs["COMMIT"] - capdischarge[i] = value(EP[:vCAP][i])*cap_size(gen[i]) - else - capdischarge[i] = value(EP[:vCAP][i]) - end - end + # Capacity decisions + capdischarge = zeros(size(inputs["RESOURCE_NAMES"])) + for i in inputs["NEW_CAP"] + if i in inputs["COMMIT"] + capdischarge[i] = value(EP[:vCAP][i]) * cap_size(gen[i]) + else + capdischarge[i] = value(EP[:vCAP][i]) + end + end - retcapdischarge = zeros(size(inputs["RESOURCE_NAMES"])) - for i in inputs["RET_CAP"] - if i in inputs["COMMIT"] - retcapdischarge[i] = first(value.(EP[:vRETCAP][i]))*cap_size(gen[i]) - else - retcapdischarge[i] = first(value.(EP[:vRETCAP][i])) - end - end + retcapdischarge = zeros(size(inputs["RESOURCE_NAMES"])) + for i in inputs["RET_CAP"] + if i in inputs["COMMIT"] + retcapdischarge[i] = first(value.(EP[:vRETCAP][i])) * cap_size(gen[i]) + else + retcapdischarge[i] = first(value.(EP[:vRETCAP][i])) + end + end - retrocapdischarge = zeros(size(inputs["RESOURCE_NAMES"])) - for i in inputs["RETROFIT_CAP"] - if i in inputs["COMMIT"] - retrocapdischarge[i] = first(value.(EP[:vRETROFITCAP][i])) * cap_size(gen[i]) - else - retrocapdischarge[i] = first(value.(EP[:vRETROFITCAP][i])) - end - end + retrocapdischarge = zeros(size(inputs["RESOURCE_NAMES"])) + for i in inputs["RETROFIT_CAP"] + if i in inputs["COMMIT"] + retrocapdischarge[i] = first(value.(EP[:vRETROFITCAP][i])) * cap_size(gen[i]) + else + retrocapdischarge[i] = first(value.(EP[:vRETROFITCAP][i])) + end + end + capacity_constraint_dual = zeros(size(inputs["RESOURCE_NAMES"])) + for y in ids_with_positive(gen, max_cap_mw) + capacity_constraint_dual[y] = -dual.(EP[:cMaxCap][y]) + end - capacity_constraint_dual = zeros(size(inputs["RESOURCE_NAMES"])) - for y in ids_with_positive(gen, max_cap_mw) - capacity_constraint_dual[y] = -dual.(EP[:cMaxCap][y]) - end + capcharge = zeros(size(inputs["RESOURCE_NAMES"])) + retcapcharge = zeros(size(inputs["RESOURCE_NAMES"])) + existingcapcharge = zeros(size(inputs["RESOURCE_NAMES"])) + for i in inputs["STOR_ASYMMETRIC"] + if i in inputs["NEW_CAP_CHARGE"] + capcharge[i] = value(EP[:vCAPCHARGE][i]) + end + if i in inputs["RET_CAP_CHARGE"] + retcapcharge[i] = value(EP[:vRETCAPCHARGE][i]) + end + existingcapcharge[i] = MultiStage == 1 ? value(EP[:vEXISTINGCAPCHARGE][i]) : + existing_charge_cap_mw(gen[i]) + end - capcharge = zeros(size(inputs["RESOURCE_NAMES"])) - retcapcharge = zeros(size(inputs["RESOURCE_NAMES"])) - existingcapcharge = zeros(size(inputs["RESOURCE_NAMES"])) - for i in inputs["STOR_ASYMMETRIC"] - if i in inputs["NEW_CAP_CHARGE"] - capcharge[i] = value(EP[:vCAPCHARGE][i]) - end - if i in inputs["RET_CAP_CHARGE"] - retcapcharge[i] = value(EP[:vRETCAPCHARGE][i]) - end - existingcapcharge[i] = MultiStage == 1 ? value(EP[:vEXISTINGCAPCHARGE][i]) : existing_charge_cap_mw(gen[i]) - end + capenergy = zeros(size(inputs["RESOURCE_NAMES"])) + retcapenergy = zeros(size(inputs["RESOURCE_NAMES"])) + existingcapenergy = zeros(size(inputs["RESOURCE_NAMES"])) + for i in inputs["STOR_ALL"] + if i in inputs["NEW_CAP_ENERGY"] + capenergy[i] = value(EP[:vCAPENERGY][i]) + end + if i in inputs["RET_CAP_ENERGY"] + retcapenergy[i] = value(EP[:vRETCAPENERGY][i]) + end + existingcapenergy[i] = MultiStage == 1 ? value(EP[:vEXISTINGCAPENERGY][i]) : + existing_cap_mwh(gen[i]) + end + if !isempty(inputs["VRE_STOR"]) + for i in inputs["VS_STOR"] + if i in inputs["NEW_CAP_STOR"] + capenergy[i] = value(EP[:vCAPENERGY_VS][i]) + end + if i in inputs["RET_CAP_STOR"] + retcapenergy[i] = value(EP[:vRETCAPENERGY_VS][i]) + end + existingcapenergy[i] = existing_cap_mwh(gen[i]) # multistage functionality doesn't exist yet for VRE-storage resources + end + end + dfCap = DataFrame(Resource = inputs["RESOURCE_NAMES"], + Zone = zone_id.(gen), + Retrofit_Id = retrofit_id.(gen), + StartCap = MultiStage == 1 ? value.(EP[:vEXISTINGCAP]) : existing_cap_mw.(gen), + RetCap = retcapdischarge[:], + RetroCap = retrocapdischarge[:], #### Need to change later + NewCap = capdischarge[:], + EndCap = value.(EP[:eTotalCap]), + CapacityConstraintDual = capacity_constraint_dual[:], + StartEnergyCap = existingcapenergy[:], + RetEnergyCap = retcapenergy[:], + NewEnergyCap = capenergy[:], + EndEnergyCap = existingcapenergy[:] - retcapenergy[:] + capenergy[:], + StartChargeCap = existingcapcharge[:], + RetChargeCap = retcapcharge[:], + NewChargeCap = capcharge[:], + EndChargeCap = existingcapcharge[:] - retcapcharge[:] + capcharge[:]) + if setup["ParameterScale"] == 1 + dfCap.StartCap = dfCap.StartCap * ModelScalingFactor + dfCap.RetCap = dfCap.RetCap * ModelScalingFactor + dfCap.RetroCap = dfCap.RetroCap * ModelScalingFactor + dfCap.NewCap = dfCap.NewCap * ModelScalingFactor + dfCap.EndCap = dfCap.EndCap * ModelScalingFactor + dfCap.CapacityConstraintDual = dfCap.CapacityConstraintDual * ModelScalingFactor + dfCap.StartEnergyCap = dfCap.StartEnergyCap * ModelScalingFactor + dfCap.RetEnergyCap = dfCap.RetEnergyCap * ModelScalingFactor + dfCap.NewEnergyCap = dfCap.NewEnergyCap * ModelScalingFactor + dfCap.EndEnergyCap = dfCap.EndEnergyCap * ModelScalingFactor + dfCap.StartChargeCap = dfCap.StartChargeCap * ModelScalingFactor + dfCap.RetChargeCap = dfCap.RetChargeCap * ModelScalingFactor + dfCap.NewChargeCap = dfCap.NewChargeCap * ModelScalingFactor + dfCap.EndChargeCap = dfCap.EndChargeCap * ModelScalingFactor + end + total = DataFrame(Resource = "Total", Zone = "n/a", Retrofit_Id = "n/a", + StartCap = sum(dfCap[!, :StartCap]), RetCap = sum(dfCap[!, :RetCap]), + NewCap = sum(dfCap[!, :NewCap]), EndCap = sum(dfCap[!, :EndCap]), + RetroCap = sum(dfCap[!, :RetroCap]), + CapacityConstraintDual = "n/a", + StartEnergyCap = sum(dfCap[!, :StartEnergyCap]), + RetEnergyCap = sum(dfCap[!, :RetEnergyCap]), + NewEnergyCap = sum(dfCap[!, :NewEnergyCap]), + EndEnergyCap = sum(dfCap[!, :EndEnergyCap]), + StartChargeCap = sum(dfCap[!, :StartChargeCap]), + RetChargeCap = sum(dfCap[!, :RetChargeCap]), + NewChargeCap = sum(dfCap[!, :NewChargeCap]), + EndChargeCap = sum(dfCap[!, :EndChargeCap])) - capenergy = zeros(size(inputs["RESOURCE_NAMES"])) - retcapenergy = zeros(size(inputs["RESOURCE_NAMES"])) - existingcapenergy = zeros(size(inputs["RESOURCE_NAMES"])) - for i in inputs["STOR_ALL"] - if i in inputs["NEW_CAP_ENERGY"] - capenergy[i] = value(EP[:vCAPENERGY][i]) - end - if i in inputs["RET_CAP_ENERGY"] - retcapenergy[i] = value(EP[:vRETCAPENERGY][i]) - end - existingcapenergy[i] = MultiStage == 1 ? value(EP[:vEXISTINGCAPENERGY][i]) : existing_cap_mwh(gen[i]) - end - if !isempty(inputs["VRE_STOR"]) - for i in inputs["VS_STOR"] - if i in inputs["NEW_CAP_STOR"] - capenergy[i] = value(EP[:vCAPENERGY_VS][i]) - end - if i in inputs["RET_CAP_STOR"] - retcapenergy[i] = value(EP[:vRETCAPENERGY_VS][i]) - end - existingcapenergy[i] = existing_cap_mwh(gen[i]) # multistage functionality doesn't exist yet for VRE-storage resources - end - end - dfCap = DataFrame( - Resource = inputs["RESOURCE_NAMES"], - Zone = zone_id.(gen), - Retrofit_Id = retrofit_id.(gen), - StartCap = MultiStage == 1 ? value.(EP[:vEXISTINGCAP]) : existing_cap_mw.(gen), - RetCap = retcapdischarge[:], - RetroCap = retrocapdischarge[:], #### Need to change later - NewCap = capdischarge[:], - EndCap = value.(EP[:eTotalCap]), - CapacityConstraintDual = capacity_constraint_dual[:], - StartEnergyCap = existingcapenergy[:], - RetEnergyCap = retcapenergy[:], - NewEnergyCap = capenergy[:], - EndEnergyCap = existingcapenergy[:] - retcapenergy[:] + capenergy[:], - StartChargeCap = existingcapcharge[:], - RetChargeCap = retcapcharge[:], - NewChargeCap = capcharge[:], - EndChargeCap = existingcapcharge[:] - retcapcharge[:] + capcharge[:] - ) - if setup["ParameterScale"] ==1 - dfCap.StartCap = dfCap.StartCap * ModelScalingFactor - dfCap.RetCap = dfCap.RetCap * ModelScalingFactor - dfCap.RetroCap = dfCap.RetroCap * ModelScalingFactor - dfCap.NewCap = dfCap.NewCap * ModelScalingFactor - dfCap.EndCap = dfCap.EndCap * ModelScalingFactor - dfCap.CapacityConstraintDual = dfCap.CapacityConstraintDual * ModelScalingFactor - dfCap.StartEnergyCap = dfCap.StartEnergyCap * ModelScalingFactor - dfCap.RetEnergyCap = dfCap.RetEnergyCap * ModelScalingFactor - dfCap.NewEnergyCap = dfCap.NewEnergyCap * ModelScalingFactor - dfCap.EndEnergyCap = dfCap.EndEnergyCap * ModelScalingFactor - dfCap.StartChargeCap = dfCap.StartChargeCap * ModelScalingFactor - dfCap.RetChargeCap = dfCap.RetChargeCap * ModelScalingFactor - dfCap.NewChargeCap = dfCap.NewChargeCap * ModelScalingFactor - dfCap.EndChargeCap = dfCap.EndChargeCap * ModelScalingFactor - end - total = DataFrame( - Resource = "Total", Zone = "n/a", Retrofit_Id = "n/a", - StartCap = sum(dfCap[!,:StartCap]), RetCap = sum(dfCap[!,:RetCap]), - NewCap = sum(dfCap[!,:NewCap]), EndCap = sum(dfCap[!,:EndCap]), - RetroCap = sum(dfCap[!,:RetroCap]), - CapacityConstraintDual = "n/a", - StartEnergyCap = sum(dfCap[!,:StartEnergyCap]), RetEnergyCap = sum(dfCap[!,:RetEnergyCap]), - NewEnergyCap = sum(dfCap[!,:NewEnergyCap]), EndEnergyCap = sum(dfCap[!,:EndEnergyCap]), - StartChargeCap = sum(dfCap[!,:StartChargeCap]), RetChargeCap = sum(dfCap[!,:RetChargeCap]), - NewChargeCap = sum(dfCap[!,:NewChargeCap]), EndChargeCap = sum(dfCap[!,:EndChargeCap]) - ) - - dfCap = vcat(dfCap, total) - CSV.write(joinpath(path, "capacity.csv"), dfCap) - return dfCap -end \ No newline at end of file + dfCap = vcat(dfCap, total) + CSV.write(joinpath(path, "capacity.csv"), dfCap) + return dfCap +end diff --git a/src/write_outputs/write_capacityfactor.jl b/src/write_outputs/write_capacityfactor.jl index 03c2a50e4b..d2b8e94f20 100644 --- a/src/write_outputs/write_capacityfactor.jl +++ b/src/write_outputs/write_capacityfactor.jl @@ -15,40 +15,62 @@ function write_capacityfactor(path::AbstractString, inputs::Dict, setup::Dict, E ELECTROLYZER = inputs["ELECTROLYZER"] VRE_STOR = inputs["VRE_STOR"] - dfCapacityfactor = DataFrame(Resource=inputs["RESOURCE_NAMES"], Zone=zone_id.(gen), AnnualSum=zeros(G), Capacity=zeros(G), CapacityFactor=zeros(G)) + dfCapacityfactor = DataFrame(Resource = inputs["RESOURCE_NAMES"], + Zone = zone_id.(gen), + AnnualSum = zeros(G), + Capacity = zeros(G), + CapacityFactor = zeros(G)) scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 dfCapacityfactor.AnnualSum .= value.(EP[:vP]) * inputs["omega"] * scale_factor dfCapacityfactor.Capacity .= value.(EP[:eTotalCap]) * scale_factor if !isempty(VRE_STOR) - SOLAR = setdiff(inputs["VS_SOLAR"],inputs["VS_WIND"]) - WIND = setdiff(inputs["VS_WIND"],inputs["VS_SOLAR"]) - SOLAR_WIND = intersect(inputs["VS_SOLAR"],inputs["VS_WIND"]) + SOLAR = setdiff(inputs["VS_SOLAR"], inputs["VS_WIND"]) + WIND = setdiff(inputs["VS_WIND"], inputs["VS_SOLAR"]) + SOLAR_WIND = intersect(inputs["VS_SOLAR"], inputs["VS_WIND"]) gen_VRE_STOR = gen.VreStorage if !isempty(SOLAR) - dfCapacityfactor.AnnualSum[SOLAR] .= value.(EP[:vP_SOLAR][SOLAR, :]).data * inputs["omega"] * scale_factor - dfCapacityfactor.Capacity[SOLAR] .= value.(EP[:eTotalCap_SOLAR][SOLAR]).data * scale_factor + dfCapacityfactor.AnnualSum[SOLAR] .= value.(EP[:vP_SOLAR][SOLAR, :]).data * + inputs["omega"] * scale_factor + dfCapacityfactor.Capacity[SOLAR] .= value.(EP[:eTotalCap_SOLAR][SOLAR]).data * + scale_factor end if !isempty(WIND) - dfCapacityfactor.AnnualSum[WIND] .= value.(EP[:vP_WIND][WIND, :]).data * inputs["omega"] * scale_factor - dfCapacityfactor.Capacity[WIND] .= value.(EP[:eTotalCap_WIND][WIND]).data * scale_factor + dfCapacityfactor.AnnualSum[WIND] .= value.(EP[:vP_WIND][WIND, :]).data * + inputs["omega"] * scale_factor + dfCapacityfactor.Capacity[WIND] .= value.(EP[:eTotalCap_WIND][WIND]).data * + scale_factor end if !isempty(SOLAR_WIND) - dfCapacityfactor.AnnualSum[SOLAR_WIND] .= (value.(EP[:vP_WIND][SOLAR_WIND, :]).data - + value.(EP[:vP_SOLAR][SOLAR_WIND, :]).data .* etainverter.(gen_VRE_STOR[(gen_VRE_STOR.wind.!=0) .& (gen_VRE_STOR.solar.!=0)])) * inputs["omega"] * scale_factor - dfCapacityfactor.Capacity[SOLAR_WIND] .= (value.(EP[:eTotalCap_WIND][SOLAR_WIND]).data + value.(EP[:eTotalCap_SOLAR][SOLAR_WIND]).data .* etainverter.(gen_VRE_STOR[(gen_VRE_STOR.wind.!=0) .& (gen_VRE_STOR.solar.!=0)])) * scale_factor + dfCapacityfactor.AnnualSum[SOLAR_WIND] .= (value.(EP[:vP_WIND][SOLAR_WIND, + :]).data + + + value.(EP[:vP_SOLAR][SOLAR_WIND, + :]).data .* + etainverter.(gen_VRE_STOR[(gen_VRE_STOR.wind .!= 0) .& (gen_VRE_STOR.solar .!= 0)])) * + inputs["omega"] * scale_factor + dfCapacityfactor.Capacity[SOLAR_WIND] .= (value.(EP[:eTotalCap_WIND][SOLAR_WIND]).data + + value.(EP[:eTotalCap_SOLAR][SOLAR_WIND]).data .* + etainverter.(gen_VRE_STOR[(gen_VRE_STOR.wind .!= 0) .& (gen_VRE_STOR.solar .!= 0)])) * + scale_factor end end # We only calcualte the resulted capacity factor with total capacity > 1MW and total generation > 1MWh - EXISTING = intersect(findall(x -> x >= 1, dfCapacityfactor.AnnualSum), findall(x -> x >= 1, dfCapacityfactor.Capacity)) + EXISTING = intersect(findall(x -> x >= 1, dfCapacityfactor.AnnualSum), + findall(x -> x >= 1, dfCapacityfactor.Capacity)) # We calculate capacity factor for thermal, vre, hydro and must run. Not for storage and flexible demand CF_GEN = intersect(union(THERM_ALL, VRE, HYDRO_RES, MUST_RUN, VRE_STOR), EXISTING) - dfCapacityfactor.CapacityFactor[CF_GEN] .= (dfCapacityfactor.AnnualSum[CF_GEN] ./ dfCapacityfactor.Capacity[CF_GEN]) / sum(inputs["omega"][t] for t in 1:T) + dfCapacityfactor.CapacityFactor[CF_GEN] .= (dfCapacityfactor.AnnualSum[CF_GEN] ./ + dfCapacityfactor.Capacity[CF_GEN]) / + sum(inputs["omega"][t] for t in 1:T) # Capacity factor for electrolyzers is based on vUSE variable not vP if (!isempty(ELECTROLYZER)) - dfCapacityfactor.AnnualSum[ELECTROLYZER] .= value.(EP[:vUSE][ELECTROLYZER, :]).data * inputs["omega"] * scale_factor - dfCapacityfactor.CapacityFactor[ELECTROLYZER] .= (dfCapacityfactor.AnnualSum[ELECTROLYZER] ./ dfCapacityfactor.Capacity[ELECTROLYZER]) / sum(inputs["omega"][t] for t in 1:T) + dfCapacityfactor.AnnualSum[ELECTROLYZER] .= value.(EP[:vUSE][ELECTROLYZER, + :]).data * inputs["omega"] * scale_factor + dfCapacityfactor.CapacityFactor[ELECTROLYZER] .= (dfCapacityfactor.AnnualSum[ELECTROLYZER] ./ + dfCapacityfactor.Capacity[ELECTROLYZER]) / + sum(inputs["omega"][t] for t in 1:T) end CSV.write(joinpath(path, "capacityfactor.csv"), dfCapacityfactor) diff --git a/src/write_outputs/write_charge.jl b/src/write_outputs/write_charge.jl index 74d00ad65a..1e0e835633 100644 --- a/src/write_outputs/write_charge.jl +++ b/src/write_outputs/write_charge.jl @@ -4,42 +4,44 @@ Function for writing the charging energy values of the different storage technologies. """ function write_charge(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - gen = inputs["RESOURCES"] - zones = zone_id.(gen) + gen = inputs["RESOURCES"] + zones = zone_id.(gen) - G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) - T = inputs["T"] # Number of time steps (hours) - STOR_ALL = inputs["STOR_ALL"] - FLEX = inputs["FLEX"] - ELECTROLYZER = inputs["ELECTROLYZER"] - VRE_STOR = inputs["VRE_STOR"] - VS_STOR = !isempty(VRE_STOR) ? inputs["VS_STOR"] : [] - - # Power withdrawn to charge each resource in each time step - dfCharge = DataFrame(Resource = inputs["RESOURCE_NAMES"], Zone = zones, AnnualSum = Array{Union{Missing,Float64}}(undef, G)) - charge = zeros(G,T) + G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) + T = inputs["T"] # Number of time steps (hours) + STOR_ALL = inputs["STOR_ALL"] + FLEX = inputs["FLEX"] + ELECTROLYZER = inputs["ELECTROLYZER"] + VRE_STOR = inputs["VRE_STOR"] + VS_STOR = !isempty(VRE_STOR) ? inputs["VS_STOR"] : [] - scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 - if !isempty(STOR_ALL) - charge[STOR_ALL, :] = value.(EP[:vCHARGE][STOR_ALL, :]) * scale_factor - end - if !isempty(FLEX) - charge[FLEX, :] = value.(EP[:vCHARGE_FLEX][FLEX, :]) * scale_factor - end - if !isempty(ELECTROLYZER) - charge[ELECTROLYZER, :] = value.(EP[:vUSE][ELECTROLYZER, :]) * scale_factor - end - if !isempty(VS_STOR) - charge[VS_STOR, :] = value.(EP[:vCHARGE_VRE_STOR][VS_STOR, :]) * scale_factor - end + # Power withdrawn to charge each resource in each time step + dfCharge = DataFrame(Resource = inputs["RESOURCE_NAMES"], + Zone = zones, + AnnualSum = Array{Union{Missing, Float64}}(undef, G)) + charge = zeros(G, T) - dfCharge.AnnualSum .= charge * inputs["omega"] + scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 + if !isempty(STOR_ALL) + charge[STOR_ALL, :] = value.(EP[:vCHARGE][STOR_ALL, :]) * scale_factor + end + if !isempty(FLEX) + charge[FLEX, :] = value.(EP[:vCHARGE_FLEX][FLEX, :]) * scale_factor + end + if !isempty(ELECTROLYZER) + charge[ELECTROLYZER, :] = value.(EP[:vUSE][ELECTROLYZER, :]) * scale_factor + end + if !isempty(VS_STOR) + charge[VS_STOR, :] = value.(EP[:vCHARGE_VRE_STOR][VS_STOR, :]) * scale_factor + end - filepath = joinpath(path, "charge.csv") - if setup["WriteOutputs"] == "annual" - write_annual(filepath, dfCharge) - else # setup["WriteOutputs"] == "full" - write_fulltimeseries(filepath, charge, dfCharge) - end - return nothing + dfCharge.AnnualSum .= charge * inputs["omega"] + + filepath = joinpath(path, "charge.csv") + if setup["WriteOutputs"] == "annual" + write_annual(filepath, dfCharge) + else # setup["WriteOutputs"] == "full" + write_fulltimeseries(filepath, charge, dfCharge) + end + return nothing end diff --git a/src/write_outputs/write_charging_cost.jl b/src/write_outputs/write_charging_cost.jl index 7c2c84a812..00410b6b59 100644 --- a/src/write_outputs/write_charging_cost.jl +++ b/src/write_outputs/write_charging_cost.jl @@ -1,38 +1,46 @@ function write_charging_cost(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - gen = inputs["RESOURCES"] + gen = inputs["RESOURCES"] - regions = region.(gen) - clusters = cluster.(gen) - zones = zone_id.(gen) + regions = region.(gen) + clusters = cluster.(gen) + zones = zone_id.(gen) - G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) - T = inputs["T"] # Number of time steps (hours) - STOR_ALL = inputs["STOR_ALL"] - FLEX = inputs["FLEX"] - ELECTROLYZER = inputs["ELECTROLYZER"] - VRE_STOR = inputs["VRE_STOR"] - VS_STOR = !isempty(VRE_STOR) ? inputs["VS_STOR"] : [] + G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) + T = inputs["T"] # Number of time steps (hours) + STOR_ALL = inputs["STOR_ALL"] + FLEX = inputs["FLEX"] + ELECTROLYZER = inputs["ELECTROLYZER"] + VRE_STOR = inputs["VRE_STOR"] + VS_STOR = !isempty(VRE_STOR) ? inputs["VS_STOR"] : [] price = locational_marginal_price(EP, inputs, setup) - dfChargingcost = DataFrame(Region = regions, Resource = inputs["RESOURCE_NAMES"], Zone = zones, Cluster = clusters, AnnualSum = Array{Float64}(undef, G),) - chargecost = zeros(G, T) - if !isempty(STOR_ALL) - chargecost[STOR_ALL, :] .= (value.(EP[:vCHARGE][STOR_ALL, :]).data) .* transpose(price)[zone_id.(gen.Storage), :] - end - if !isempty(FLEX) - chargecost[FLEX, :] .= value.(EP[:vP][FLEX, :]) .* transpose(price)[zone_id.(gen.FlexDemand), :] - end - if !isempty(ELECTROLYZER) - chargecost[ELECTROLYZER, :] .= (value.(EP[:vUSE][ELECTROLYZER, :]).data) .* transpose(price)[zone_id.(gen.Electrolyzer), :] - end - if !isempty(VS_STOR) - chargecost[VS_STOR, :] .= value.(EP[:vCHARGE_VRE_STOR][VS_STOR, :].data) .* transpose(price)[zone_id.(gen[VS_STOR]), :] - end - if setup["ParameterScale"] == 1 - chargecost *= ModelScalingFactor - end - dfChargingcost.AnnualSum .= chargecost * inputs["omega"] - write_simple_csv(joinpath(path, "ChargingCost.csv"), dfChargingcost) - return dfChargingcost + dfChargingcost = DataFrame(Region = regions, + Resource = inputs["RESOURCE_NAMES"], + Zone = zones, + Cluster = clusters, + AnnualSum = Array{Float64}(undef, G)) + chargecost = zeros(G, T) + if !isempty(STOR_ALL) + chargecost[STOR_ALL, :] .= (value.(EP[:vCHARGE][STOR_ALL, :]).data) .* + transpose(price)[zone_id.(gen.Storage), :] + end + if !isempty(FLEX) + chargecost[FLEX, :] .= value.(EP[:vP][FLEX, :]) .* + transpose(price)[zone_id.(gen.FlexDemand), :] + end + if !isempty(ELECTROLYZER) + chargecost[ELECTROLYZER, :] .= (value.(EP[:vUSE][ELECTROLYZER, :]).data) .* + transpose(price)[zone_id.(gen.Electrolyzer), :] + end + if !isempty(VS_STOR) + chargecost[VS_STOR, :] .= value.(EP[:vCHARGE_VRE_STOR][VS_STOR, :].data) .* + transpose(price)[zone_id.(gen[VS_STOR]), :] + end + if setup["ParameterScale"] == 1 + chargecost *= ModelScalingFactor + end + dfChargingcost.AnnualSum .= chargecost * inputs["omega"] + write_simple_csv(joinpath(path, "ChargingCost.csv"), dfChargingcost) + return dfChargingcost end diff --git a/src/write_outputs/write_co2.jl b/src/write_outputs/write_co2.jl index c737652323..050cf91c04 100644 --- a/src/write_outputs/write_co2.jl +++ b/src/write_outputs/write_co2.jl @@ -9,13 +9,17 @@ function write_co2(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) write_co2_capture_plant(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) end - -function write_co2_emissions_plant(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) +function write_co2_emissions_plant(path::AbstractString, + inputs::Dict, + setup::Dict, + EP::Model) gen = inputs["RESOURCES"] G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) # CO2 emissions by plant - dfEmissions_plant = DataFrame(Resource=inputs["RESOURCE_NAMES"], Zone=zone_id.(gen), AnnualSum=zeros(G)) + dfEmissions_plant = DataFrame(Resource = inputs["RESOURCE_NAMES"], + Zone = zone_id.(gen), + AnnualSum = zeros(G)) emissions_plant = value.(EP[:eEmissionsByPlant]) if setup["ParameterScale"] == 1 @@ -26,7 +30,7 @@ function write_co2_emissions_plant(path::AbstractString, inputs::Dict, setup::Di filepath = joinpath(path, "emissions_plant.csv") if setup["WriteOutputs"] == "annual" write_annual(filepath, dfEmissions_plant) - else # setup["WriteOutputs"] == "full" + else # setup["WriteOutputs"] == "full" write_fulltimeseries(filepath, emissions_plant, dfEmissions_plant) end return nothing @@ -39,7 +43,9 @@ function write_co2_capture_plant(path::AbstractString, inputs::Dict, setup::Dict T = inputs["T"] # Number of time steps (hours) Z = inputs["Z"] # Number of zones - dfCapturedEmissions_plant = DataFrame(Resource=inputs["RESOURCE_NAMES"][CCS], Zone=zone_id.(gen[CCS]), AnnualSum=zeros(length(CCS))) + dfCapturedEmissions_plant = DataFrame(Resource = inputs["RESOURCE_NAMES"][CCS], + Zone = zone_id.(gen[CCS]), + AnnualSum = zeros(length(CCS))) if !isempty(CCS) # Captured CO2 emissions by plant emissions_captured_plant = (value.(EP[:eEmissionsCaptureByPlant]).data) @@ -53,8 +59,10 @@ function write_co2_capture_plant(path::AbstractString, inputs::Dict, setup::Dict if setup["WriteOutputs"] == "annual" write_annual(filepath, dfCapturedEmissions_plant) else # setup["WriteOutputs"] == "full" - write_fulltimeseries(filepath, emissions_captured_plant, dfCapturedEmissions_plant) + write_fulltimeseries(filepath, + emissions_captured_plant, + dfCapturedEmissions_plant) end return nothing end -end \ No newline at end of file +end diff --git a/src/write_outputs/write_costs.jl b/src/write_outputs/write_costs.jl index 8cbe60e5c9..055979a126 100644 --- a/src/write_outputs/write_costs.jl +++ b/src/write_outputs/write_costs.jl @@ -4,246 +4,310 @@ Function for writing the costs pertaining to the objective function (fixed, variable O&M etc.). """ function write_costs(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - ## Cost results - gen = inputs["RESOURCES"] - SEG = inputs["SEG"] # Number of lines - Z = inputs["Z"] # Number of zones - T = inputs["T"] # Number of time steps (hours) - VRE_STOR = inputs["VRE_STOR"] - ELECTROLYZER = inputs["ELECTROLYZER"] - - cost_list = ["cTotal", "cFix", "cVar", "cFuel" ,"cNSE", "cStart", "cUnmetRsv", "cNetworkExp", "cUnmetPolicyPenalty", "cCO2"] - if !isempty(VRE_STOR) - push!(cost_list, "cGridConnection") - end - if !isempty(ELECTROLYZER) - push!(cost_list, "cHydrogenRevenue") - end - dfCost = DataFrame(Costs = cost_list) - - cVar = value(EP[:eTotalCVarOut])+ (!isempty(inputs["STOR_ALL"]) ? value(EP[:eTotalCVarIn]) : 0.0) + (!isempty(inputs["FLEX"]) ? value(EP[:eTotalCVarFlexIn]) : 0.0) - cFix = value(EP[:eTotalCFix]) + (!isempty(inputs["STOR_ALL"]) ? value(EP[:eTotalCFixEnergy]) : 0.0) + (!isempty(inputs["STOR_ASYMMETRIC"]) ? value(EP[:eTotalCFixCharge]) : 0.0) - - cFuel = value.(EP[:eTotalCFuelOut]) - - if !isempty(VRE_STOR) - cFix += ((!isempty(inputs["VS_DC"]) ? value(EP[:eTotalCFixDC]) : 0.0) + (!isempty(inputs["VS_SOLAR"]) ? value(EP[:eTotalCFixSolar]) : 0.0) + (!isempty(inputs["VS_WIND"]) ? value(EP[:eTotalCFixWind]) : 0.0)) - cVar += ((!isempty(inputs["VS_SOLAR"]) ? value(EP[:eTotalCVarOutSolar]) : 0.0) + (!isempty(inputs["VS_WIND"]) ? value(EP[:eTotalCVarOutWind]) : 0.0)) - if !isempty(inputs["VS_STOR"]) - cFix += ((!isempty(inputs["VS_STOR"]) ? value(EP[:eTotalCFixStor]) : 0.0) + (!isempty(inputs["VS_ASYM_DC_CHARGE"]) ? value(EP[:eTotalCFixCharge_DC]) : 0.0) + (!isempty(inputs["VS_ASYM_DC_DISCHARGE"]) ? value(EP[:eTotalCFixDischarge_DC]) : 0.0) + (!isempty(inputs["VS_ASYM_AC_CHARGE"]) ? value(EP[:eTotalCFixCharge_AC]) : 0.0) + (!isempty(inputs["VS_ASYM_AC_DISCHARGE"]) ? value(EP[:eTotalCFixDischarge_AC]) : 0.0)) - cVar += (!isempty(inputs["VS_STOR"]) ? value(EP[:eTotalCVarStor]) : 0.0) - end - total_cost =[value(EP[:eObj]), cFix, cVar, cFuel, value(EP[:eTotalCNSE]), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - else - total_cost = [value(EP[:eObj]), cFix, cVar, cFuel, value(EP[:eTotalCNSE]), 0.0, 0.0, 0.0, 0.0, 0.0] - end - - if !isempty(ELECTROLYZER) - push!(total_cost,(!isempty(inputs["ELECTROLYZER"]) ? -1*value(EP[:eTotalHydrogenValue]) : 0.0)) - end - - dfCost[!,Symbol("Total")] = total_cost - - if setup["ParameterScale"] == 1 - dfCost.Total *= ModelScalingFactor^2 - end - - if setup["UCommit"]>=1 - dfCost[6,2] = value(EP[:eTotalCStart]) + value(EP[:eTotalCFuelStart]) - end - - if setup["OperationalReserves"]==1 - dfCost[7,2] = value(EP[:eTotalCRsvPen]) - end - - if setup["NetworkExpansion"] == 1 && Z > 1 - dfCost[8,2] = value(EP[:eTotalCNetworkExp]) - end - - if haskey(inputs, "dfCapRes_slack") - dfCost[9,2] += value(EP[:eCTotalCapResSlack]) - end - - if haskey(inputs, "dfESR_slack") - dfCost[9,2] += value(EP[:eCTotalESRSlack]) - end - - if haskey(inputs, "dfCO2Cap_slack") - dfCost[9,2] += value(EP[:eCTotalCO2CapSlack]) - end - - if haskey(inputs, "MinCapPriceCap") - dfCost[9,2] += value(EP[:eTotalCMinCapSlack]) - end - - if !isempty(VRE_STOR) - dfCost[!,2][11] = value(EP[:eTotalCGrid]) * (setup["ParameterScale"] == 1 ? ModelScalingFactor^2 : 1) - end - - if any(co2_capture_fraction.(gen) .!= 0) - dfCost[10,2] += value(EP[:eTotaleCCO2Sequestration]) - end - - if setup["ParameterScale"] == 1 - dfCost[6,2] *= ModelScalingFactor^2 - dfCost[7,2] *= ModelScalingFactor^2 - dfCost[8,2] *= ModelScalingFactor^2 - dfCost[9,2] *= ModelScalingFactor^2 - dfCost[10,2] *= ModelScalingFactor^2 - end - - for z in 1:Z - tempCTotal = 0.0 - tempCFix = 0.0 - tempCVar = 0.0 - tempCFuel = 0.0 - tempCStart = 0.0 - tempCNSE = 0.0 - tempHydrogenValue = 0.0 - tempCCO2 = 0.0 - - Y_ZONE = resources_in_zone_by_rid(gen,z) - STOR_ALL_ZONE = intersect(inputs["STOR_ALL"], Y_ZONE) - STOR_ASYMMETRIC_ZONE = intersect(inputs["STOR_ASYMMETRIC"], Y_ZONE) - FLEX_ZONE = intersect(inputs["FLEX"], Y_ZONE) - COMMIT_ZONE = intersect(inputs["COMMIT"], Y_ZONE) - ELECTROLYZERS_ZONE = intersect(inputs["ELECTROLYZER"], Y_ZONE) - CCS_ZONE = intersect(inputs["CCS"], Y_ZONE) - - eCFix = sum(value.(EP[:eCFix][Y_ZONE])) - tempCFix += eCFix - tempCTotal += eCFix - - tempCVar = sum(value.(EP[:eCVar_out][Y_ZONE,:])) - tempCTotal += tempCVar - - tempCFuel = sum(value.(EP[:ePlantCFuelOut][Y_ZONE,:])) - tempCTotal += tempCFuel - - if !isempty(STOR_ALL_ZONE) - eCVar_in = sum(value.(EP[:eCVar_in][STOR_ALL_ZONE,:])) - tempCVar += eCVar_in - eCFixEnergy = sum(value.(EP[:eCFixEnergy][STOR_ALL_ZONE])) - tempCFix += eCFixEnergy - tempCTotal += eCVar_in + eCFixEnergy - end - if !isempty(STOR_ASYMMETRIC_ZONE) - eCFixCharge = sum(value.(EP[:eCFixCharge][STOR_ASYMMETRIC_ZONE])) - tempCFix += eCFixCharge - tempCTotal += eCFixCharge - end - if !isempty(FLEX_ZONE) - eCVarFlex_in = sum(value.(EP[:eCVarFlex_in][FLEX_ZONE,:])) - tempCVar += eCVarFlex_in - tempCTotal += eCVarFlex_in - end - if !isempty(VRE_STOR) - gen_VRE_STOR = gen.VreStorage - Y_ZONE_VRE_STOR = resources_in_zone_by_rid(gen_VRE_STOR, z) - - # Fixed Costs - eCFix_VRE_STOR = 0.0 - SOLAR_ZONE_VRE_STOR = intersect(Y_ZONE_VRE_STOR, inputs["VS_SOLAR"]) - if !isempty(SOLAR_ZONE_VRE_STOR) - eCFix_VRE_STOR += sum(value.(EP[:eCFixSolar][SOLAR_ZONE_VRE_STOR])) - end - WIND_ZONE_VRE_STOR = intersect(Y_ZONE_VRE_STOR, inputs["VS_WIND"]) - if !isempty(WIND_ZONE_VRE_STOR) - eCFix_VRE_STOR += sum(value.(EP[:eCFixWind][WIND_ZONE_VRE_STOR])) - end - DC_ZONE_VRE_STOR = intersect(Y_ZONE_VRE_STOR, inputs["VS_DC"]) - if !isempty(DC_ZONE_VRE_STOR) - eCFix_VRE_STOR += sum(value.(EP[:eCFixDC][DC_ZONE_VRE_STOR])) - end - STOR_ALL_ZONE_VRE_STOR = intersect(inputs["VS_STOR"], Y_ZONE_VRE_STOR) - if !isempty(STOR_ALL_ZONE_VRE_STOR) - eCFix_VRE_STOR += sum(value.(EP[:eCFixEnergy_VS][STOR_ALL_ZONE_VRE_STOR])) - DC_CHARGE_ALL_ZONE_VRE_STOR = intersect(inputs["VS_ASYM_DC_CHARGE"], Y_ZONE_VRE_STOR) - if !isempty(DC_CHARGE_ALL_ZONE_VRE_STOR) - eCFix_VRE_STOR += sum(value.(EP[:eCFixCharge_DC][DC_CHARGE_ALL_ZONE_VRE_STOR])) - end - DC_DISCHARGE_ALL_ZONE_VRE_STOR = intersect(inputs["VS_ASYM_DC_DISCHARGE"], Y_ZONE_VRE_STOR) - if !isempty(DC_DISCHARGE_ALL_ZONE_VRE_STOR) - eCFix_VRE_STOR += sum(value.(EP[:eCFixDischarge_DC][DC_DISCHARGE_ALL_ZONE_VRE_STOR])) - end - AC_DISCHARGE_ALL_ZONE_VRE_STOR = intersect(inputs["VS_ASYM_AC_DISCHARGE"], Y_ZONE_VRE_STOR) - if !isempty(AC_DISCHARGE_ALL_ZONE_VRE_STOR) - eCFix_VRE_STOR += sum(value.(EP[:eCFixDischarge_AC][AC_DISCHARGE_ALL_ZONE_VRE_STOR])) - end - AC_CHARGE_ALL_ZONE_VRE_STOR = intersect(inputs["VS_ASYM_AC_CHARGE"], Y_ZONE_VRE_STOR) - if !isempty(AC_CHARGE_ALL_ZONE_VRE_STOR) - eCFix_VRE_STOR += sum(value.(EP[:eCFixCharge_AC][AC_CHARGE_ALL_ZONE_VRE_STOR])) - end - end - tempCFix += eCFix_VRE_STOR - - # Variable Costs - eCVar_VRE_STOR = 0.0 - if !isempty(SOLAR_ZONE_VRE_STOR) - eCVar_VRE_STOR += sum(value.(EP[:eCVarOutSolar][SOLAR_ZONE_VRE_STOR,:])) - end - if !isempty(WIND_ZONE_VRE_STOR) - eCVar_VRE_STOR += sum(value.(EP[:eCVarOutWind][WIND_ZONE_VRE_STOR, :])) - end - if !isempty(STOR_ALL_ZONE_VRE_STOR) - vom_map = Dict( - DC_CHARGE_ALL_ZONE_VRE_STOR => :eCVar_Charge_DC, - DC_DISCHARGE_ALL_ZONE_VRE_STOR => :eCVar_Discharge_DC, - AC_DISCHARGE_ALL_ZONE_VRE_STOR => :eCVar_Discharge_AC, - AC_CHARGE_ALL_ZONE_VRE_STOR => :eCVar_Charge_AC - ) - for (set, symbol) in vom_map - if !isempty(set) - eCVar_VRE_STOR += sum(value.(EP[symbol][set, :])) - end - end - end - tempCVar += eCVar_VRE_STOR - - # Total Added Costs - tempCTotal += (eCFix_VRE_STOR + eCVar_VRE_STOR) - end - - if setup["UCommit"] >= 1 && !isempty(COMMIT_ZONE) - eCStart = sum(value.(EP[:eCStart][COMMIT_ZONE,:])) + sum(value.(EP[:ePlantCFuelStart][COMMIT_ZONE,:])) - tempCStart += eCStart - tempCTotal += eCStart - end - - if !isempty(ELECTROLYZERS_ZONE) - tempHydrogenValue = -1*sum(value.(EP[:eHydrogenValue][ELECTROLYZERS_ZONE,:])) - tempCTotal += tempHydrogenValue - end - - - tempCNSE = sum(value.(EP[:eCNSE][:,:,z])) - tempCTotal += tempCNSE - - # if any(dfGen.CO2_Capture_Fraction .!=0) - if !isempty(CCS_ZONE) - tempCCO2 = sum(value.(EP[:ePlantCCO2Sequestration][CCS_ZONE])) - tempCTotal += tempCCO2 - end - - if setup["ParameterScale"] == 1 - tempCTotal *= ModelScalingFactor^2 - tempCFix *= ModelScalingFactor^2 - tempCVar *= ModelScalingFactor^2 - tempCFuel *= ModelScalingFactor^2 - tempCNSE *= ModelScalingFactor^2 - tempCStart *= ModelScalingFactor^2 - tempHydrogenValue *= ModelScalingFactor^2 - tempCCO2 *= ModelScalingFactor^2 - end - temp_cost_list = [tempCTotal, tempCFix, tempCVar, tempCFuel,tempCNSE, tempCStart, "-", "-", "-", tempCCO2] - if !isempty(VRE_STOR) - push!(temp_cost_list, "-") - end - if !isempty(ELECTROLYZERS_ZONE) - push!(temp_cost_list,tempHydrogenValue) - end - - dfCost[!,Symbol("Zone$z")] = temp_cost_list - end - CSV.write(joinpath(path, "costs.csv"), dfCost) + ## Cost results + gen = inputs["RESOURCES"] + SEG = inputs["SEG"] # Number of lines + Z = inputs["Z"] # Number of zones + T = inputs["T"] # Number of time steps (hours) + VRE_STOR = inputs["VRE_STOR"] + ELECTROLYZER = inputs["ELECTROLYZER"] + + cost_list = [ + "cTotal", + "cFix", + "cVar", + "cFuel", + "cNSE", + "cStart", + "cUnmetRsv", + "cNetworkExp", + "cUnmetPolicyPenalty", + "cCO2", + ] + if !isempty(VRE_STOR) + push!(cost_list, "cGridConnection") + end + if !isempty(ELECTROLYZER) + push!(cost_list, "cHydrogenRevenue") + end + dfCost = DataFrame(Costs = cost_list) + + cVar = value(EP[:eTotalCVarOut]) + + (!isempty(inputs["STOR_ALL"]) ? value(EP[:eTotalCVarIn]) : 0.0) + + (!isempty(inputs["FLEX"]) ? value(EP[:eTotalCVarFlexIn]) : 0.0) + cFix = value(EP[:eTotalCFix]) + + (!isempty(inputs["STOR_ALL"]) ? value(EP[:eTotalCFixEnergy]) : 0.0) + + (!isempty(inputs["STOR_ASYMMETRIC"]) ? value(EP[:eTotalCFixCharge]) : 0.0) + + cFuel = value.(EP[:eTotalCFuelOut]) + + if !isempty(VRE_STOR) + cFix += ((!isempty(inputs["VS_DC"]) ? value(EP[:eTotalCFixDC]) : 0.0) + + (!isempty(inputs["VS_SOLAR"]) ? value(EP[:eTotalCFixSolar]) : 0.0) + + (!isempty(inputs["VS_WIND"]) ? value(EP[:eTotalCFixWind]) : 0.0)) + cVar += ((!isempty(inputs["VS_SOLAR"]) ? value(EP[:eTotalCVarOutSolar]) : 0.0) + + (!isempty(inputs["VS_WIND"]) ? value(EP[:eTotalCVarOutWind]) : 0.0)) + if !isempty(inputs["VS_STOR"]) + cFix += ((!isempty(inputs["VS_STOR"]) ? value(EP[:eTotalCFixStor]) : 0.0) + + (!isempty(inputs["VS_ASYM_DC_CHARGE"]) ? + value(EP[:eTotalCFixCharge_DC]) : 0.0) + + (!isempty(inputs["VS_ASYM_DC_DISCHARGE"]) ? + value(EP[:eTotalCFixDischarge_DC]) : 0.0) + + (!isempty(inputs["VS_ASYM_AC_CHARGE"]) ? + value(EP[:eTotalCFixCharge_AC]) : 0.0) + + (!isempty(inputs["VS_ASYM_AC_DISCHARGE"]) ? + value(EP[:eTotalCFixDischarge_AC]) : 0.0)) + cVar += (!isempty(inputs["VS_STOR"]) ? value(EP[:eTotalCVarStor]) : 0.0) + end + total_cost = [ + value(EP[:eObj]), + cFix, + cVar, + cFuel, + value(EP[:eTotalCNSE]), + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ] + else + total_cost = [ + value(EP[:eObj]), + cFix, + cVar, + cFuel, + value(EP[:eTotalCNSE]), + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ] + end + + if !isempty(ELECTROLYZER) + push!(total_cost, + (!isempty(inputs["ELECTROLYZER"]) ? -1 * value(EP[:eTotalHydrogenValue]) : 0.0)) + end + + dfCost[!, Symbol("Total")] = total_cost + + if setup["ParameterScale"] == 1 + dfCost.Total *= ModelScalingFactor^2 + end + + if setup["UCommit"] >= 1 + dfCost[6, 2] = value(EP[:eTotalCStart]) + value(EP[:eTotalCFuelStart]) + end + + if setup["OperationalReserves"] == 1 + dfCost[7, 2] = value(EP[:eTotalCRsvPen]) + end + + if setup["NetworkExpansion"] == 1 && Z > 1 + dfCost[8, 2] = value(EP[:eTotalCNetworkExp]) + end + + if haskey(inputs, "dfCapRes_slack") + dfCost[9, 2] += value(EP[:eCTotalCapResSlack]) + end + + if haskey(inputs, "dfESR_slack") + dfCost[9, 2] += value(EP[:eCTotalESRSlack]) + end + + if haskey(inputs, "dfCO2Cap_slack") + dfCost[9, 2] += value(EP[:eCTotalCO2CapSlack]) + end + + if haskey(inputs, "MinCapPriceCap") + dfCost[9, 2] += value(EP[:eTotalCMinCapSlack]) + end + + if !isempty(VRE_STOR) + dfCost[!, 2][11] = value(EP[:eTotalCGrid]) * + (setup["ParameterScale"] == 1 ? ModelScalingFactor^2 : 1) + end + + if any(co2_capture_fraction.(gen) .!= 0) + dfCost[10, 2] += value(EP[:eTotaleCCO2Sequestration]) + end + + if setup["ParameterScale"] == 1 + dfCost[6, 2] *= ModelScalingFactor^2 + dfCost[7, 2] *= ModelScalingFactor^2 + dfCost[8, 2] *= ModelScalingFactor^2 + dfCost[9, 2] *= ModelScalingFactor^2 + dfCost[10, 2] *= ModelScalingFactor^2 + end + + for z in 1:Z + tempCTotal = 0.0 + tempCFix = 0.0 + tempCVar = 0.0 + tempCFuel = 0.0 + tempCStart = 0.0 + tempCNSE = 0.0 + tempHydrogenValue = 0.0 + tempCCO2 = 0.0 + + Y_ZONE = resources_in_zone_by_rid(gen, z) + STOR_ALL_ZONE = intersect(inputs["STOR_ALL"], Y_ZONE) + STOR_ASYMMETRIC_ZONE = intersect(inputs["STOR_ASYMMETRIC"], Y_ZONE) + FLEX_ZONE = intersect(inputs["FLEX"], Y_ZONE) + COMMIT_ZONE = intersect(inputs["COMMIT"], Y_ZONE) + ELECTROLYZERS_ZONE = intersect(inputs["ELECTROLYZER"], Y_ZONE) + CCS_ZONE = intersect(inputs["CCS"], Y_ZONE) + + eCFix = sum(value.(EP[:eCFix][Y_ZONE])) + tempCFix += eCFix + tempCTotal += eCFix + + tempCVar = sum(value.(EP[:eCVar_out][Y_ZONE, :])) + tempCTotal += tempCVar + + tempCFuel = sum(value.(EP[:ePlantCFuelOut][Y_ZONE, :])) + tempCTotal += tempCFuel + + if !isempty(STOR_ALL_ZONE) + eCVar_in = sum(value.(EP[:eCVar_in][STOR_ALL_ZONE, :])) + tempCVar += eCVar_in + eCFixEnergy = sum(value.(EP[:eCFixEnergy][STOR_ALL_ZONE])) + tempCFix += eCFixEnergy + tempCTotal += eCVar_in + eCFixEnergy + end + if !isempty(STOR_ASYMMETRIC_ZONE) + eCFixCharge = sum(value.(EP[:eCFixCharge][STOR_ASYMMETRIC_ZONE])) + tempCFix += eCFixCharge + tempCTotal += eCFixCharge + end + if !isempty(FLEX_ZONE) + eCVarFlex_in = sum(value.(EP[:eCVarFlex_in][FLEX_ZONE, :])) + tempCVar += eCVarFlex_in + tempCTotal += eCVarFlex_in + end + if !isempty(VRE_STOR) + gen_VRE_STOR = gen.VreStorage + Y_ZONE_VRE_STOR = resources_in_zone_by_rid(gen_VRE_STOR, z) + + # Fixed Costs + eCFix_VRE_STOR = 0.0 + SOLAR_ZONE_VRE_STOR = intersect(Y_ZONE_VRE_STOR, inputs["VS_SOLAR"]) + if !isempty(SOLAR_ZONE_VRE_STOR) + eCFix_VRE_STOR += sum(value.(EP[:eCFixSolar][SOLAR_ZONE_VRE_STOR])) + end + WIND_ZONE_VRE_STOR = intersect(Y_ZONE_VRE_STOR, inputs["VS_WIND"]) + if !isempty(WIND_ZONE_VRE_STOR) + eCFix_VRE_STOR += sum(value.(EP[:eCFixWind][WIND_ZONE_VRE_STOR])) + end + DC_ZONE_VRE_STOR = intersect(Y_ZONE_VRE_STOR, inputs["VS_DC"]) + if !isempty(DC_ZONE_VRE_STOR) + eCFix_VRE_STOR += sum(value.(EP[:eCFixDC][DC_ZONE_VRE_STOR])) + end + STOR_ALL_ZONE_VRE_STOR = intersect(inputs["VS_STOR"], Y_ZONE_VRE_STOR) + if !isempty(STOR_ALL_ZONE_VRE_STOR) + eCFix_VRE_STOR += sum(value.(EP[:eCFixEnergy_VS][STOR_ALL_ZONE_VRE_STOR])) + DC_CHARGE_ALL_ZONE_VRE_STOR = intersect(inputs["VS_ASYM_DC_CHARGE"], + Y_ZONE_VRE_STOR) + if !isempty(DC_CHARGE_ALL_ZONE_VRE_STOR) + eCFix_VRE_STOR += sum(value.(EP[:eCFixCharge_DC][DC_CHARGE_ALL_ZONE_VRE_STOR])) + end + DC_DISCHARGE_ALL_ZONE_VRE_STOR = intersect(inputs["VS_ASYM_DC_DISCHARGE"], + Y_ZONE_VRE_STOR) + if !isempty(DC_DISCHARGE_ALL_ZONE_VRE_STOR) + eCFix_VRE_STOR += sum(value.(EP[:eCFixDischarge_DC][DC_DISCHARGE_ALL_ZONE_VRE_STOR])) + end + AC_DISCHARGE_ALL_ZONE_VRE_STOR = intersect(inputs["VS_ASYM_AC_DISCHARGE"], + Y_ZONE_VRE_STOR) + if !isempty(AC_DISCHARGE_ALL_ZONE_VRE_STOR) + eCFix_VRE_STOR += sum(value.(EP[:eCFixDischarge_AC][AC_DISCHARGE_ALL_ZONE_VRE_STOR])) + end + AC_CHARGE_ALL_ZONE_VRE_STOR = intersect(inputs["VS_ASYM_AC_CHARGE"], + Y_ZONE_VRE_STOR) + if !isempty(AC_CHARGE_ALL_ZONE_VRE_STOR) + eCFix_VRE_STOR += sum(value.(EP[:eCFixCharge_AC][AC_CHARGE_ALL_ZONE_VRE_STOR])) + end + end + tempCFix += eCFix_VRE_STOR + + # Variable Costs + eCVar_VRE_STOR = 0.0 + if !isempty(SOLAR_ZONE_VRE_STOR) + eCVar_VRE_STOR += sum(value.(EP[:eCVarOutSolar][SOLAR_ZONE_VRE_STOR, :])) + end + if !isempty(WIND_ZONE_VRE_STOR) + eCVar_VRE_STOR += sum(value.(EP[:eCVarOutWind][WIND_ZONE_VRE_STOR, :])) + end + if !isempty(STOR_ALL_ZONE_VRE_STOR) + vom_map = Dict(DC_CHARGE_ALL_ZONE_VRE_STOR => :eCVar_Charge_DC, + DC_DISCHARGE_ALL_ZONE_VRE_STOR => :eCVar_Discharge_DC, + AC_DISCHARGE_ALL_ZONE_VRE_STOR => :eCVar_Discharge_AC, + AC_CHARGE_ALL_ZONE_VRE_STOR => :eCVar_Charge_AC) + for (set, symbol) in vom_map + if !isempty(set) + eCVar_VRE_STOR += sum(value.(EP[symbol][set, :])) + end + end + end + tempCVar += eCVar_VRE_STOR + + # Total Added Costs + tempCTotal += (eCFix_VRE_STOR + eCVar_VRE_STOR) + end + + if setup["UCommit"] >= 1 && !isempty(COMMIT_ZONE) + eCStart = sum(value.(EP[:eCStart][COMMIT_ZONE, :])) + + sum(value.(EP[:ePlantCFuelStart][COMMIT_ZONE, :])) + tempCStart += eCStart + tempCTotal += eCStart + end + + if !isempty(ELECTROLYZERS_ZONE) + tempHydrogenValue = -1 * sum(value.(EP[:eHydrogenValue][ELECTROLYZERS_ZONE, :])) + tempCTotal += tempHydrogenValue + end + + tempCNSE = sum(value.(EP[:eCNSE][:, :, z])) + tempCTotal += tempCNSE + + # if any(dfGen.CO2_Capture_Fraction .!=0) + if !isempty(CCS_ZONE) + tempCCO2 = sum(value.(EP[:ePlantCCO2Sequestration][CCS_ZONE])) + tempCTotal += tempCCO2 + end + + if setup["ParameterScale"] == 1 + tempCTotal *= ModelScalingFactor^2 + tempCFix *= ModelScalingFactor^2 + tempCVar *= ModelScalingFactor^2 + tempCFuel *= ModelScalingFactor^2 + tempCNSE *= ModelScalingFactor^2 + tempCStart *= ModelScalingFactor^2 + tempHydrogenValue *= ModelScalingFactor^2 + tempCCO2 *= ModelScalingFactor^2 + end + temp_cost_list = [ + tempCTotal, + tempCFix, + tempCVar, + tempCFuel, + tempCNSE, + tempCStart, + "-", + "-", + "-", + tempCCO2, + ] + if !isempty(VRE_STOR) + push!(temp_cost_list, "-") + end + if !isempty(ELECTROLYZERS_ZONE) + push!(temp_cost_list, tempHydrogenValue) + end + + dfCost[!, Symbol("Zone$z")] = temp_cost_list + end + CSV.write(joinpath(path, "costs.csv"), dfCost) end diff --git a/src/write_outputs/write_curtailment.jl b/src/write_outputs/write_curtailment.jl index 6cb151f448..8ee244f105 100644 --- a/src/write_outputs/write_curtailment.jl +++ b/src/write_outputs/write_curtailment.jl @@ -5,42 +5,59 @@ Function for writing the curtailment values of the different variable renewable co-located). """ function write_curtailment(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - gen = inputs["RESOURCES"] - G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) - T = inputs["T"] # Number of time steps (hours) - VRE = inputs["VRE"] - dfCurtailment = DataFrame(Resource = inputs["RESOURCE_NAMES"], Zone = zone_id.(gen), AnnualSum = zeros(G)) - curtailment = zeros(G, T) - scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 - curtailment[VRE, :] = scale_factor * (value.(EP[:eTotalCap][VRE]) .* inputs["pP_Max"][VRE, :] .- value.(EP[:vP][VRE, :])) - - VRE_STOR = inputs["VRE_STOR"] - if !isempty(VRE_STOR) - SOLAR = setdiff(inputs["VS_SOLAR"],inputs["VS_WIND"]) - WIND = setdiff(inputs["VS_WIND"],inputs["VS_SOLAR"]) - SOLAR_WIND = intersect(inputs["VS_SOLAR"],inputs["VS_WIND"]) - gen_VRE_STOR = gen.VreStorage - if !isempty(SOLAR) - curtailment[SOLAR, :] = scale_factor * (value.(EP[:eTotalCap_SOLAR][SOLAR]).data .* inputs["pP_Max_Solar"][SOLAR, :] .- value.(EP[:vP_SOLAR][SOLAR, :]).data) .* etainverter.(gen_VRE_STOR[(gen_VRE_STOR.solar.!=0)]) - end - if !isempty(WIND) - curtailment[WIND, :] = scale_factor * (value.(EP[:eTotalCap_WIND][WIND]).data .* inputs["pP_Max_Wind"][WIND, :] .- value.(EP[:vP_WIND][WIND, :]).data) - end - if !isempty(SOLAR_WIND) - curtailment[SOLAR_WIND, :] = scale_factor * ((value.(EP[:eTotalCap_SOLAR])[SOLAR_WIND].data - .* inputs["pP_Max_Solar"][SOLAR_WIND, :] .- value.(EP[:vP_SOLAR][SOLAR_WIND, :]).data) - .* etainverter.(gen_VRE_STOR[((gen_VRE_STOR.wind.!=0) .& (gen_VRE_STOR.solar.!=0))]) - + (value.(EP[:eTotalCap_WIND][SOLAR_WIND]).data .* inputs["pP_Max_Wind"][SOLAR_WIND, :] .- value.(EP[:vP_WIND][SOLAR_WIND, :]).data)) - end - end + gen = inputs["RESOURCES"] + G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) + T = inputs["T"] # Number of time steps (hours) + VRE = inputs["VRE"] + dfCurtailment = DataFrame(Resource = inputs["RESOURCE_NAMES"], + Zone = zone_id.(gen), + AnnualSum = zeros(G)) + curtailment = zeros(G, T) + scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 + curtailment[VRE, :] = scale_factor * + (value.(EP[:eTotalCap][VRE]) .* inputs["pP_Max"][VRE, :] .- + value.(EP[:vP][VRE, :])) - dfCurtailment.AnnualSum = curtailment * inputs["omega"] + VRE_STOR = inputs["VRE_STOR"] + if !isempty(VRE_STOR) + SOLAR = setdiff(inputs["VS_SOLAR"], inputs["VS_WIND"]) + WIND = setdiff(inputs["VS_WIND"], inputs["VS_SOLAR"]) + SOLAR_WIND = intersect(inputs["VS_SOLAR"], inputs["VS_WIND"]) + gen_VRE_STOR = gen.VreStorage + if !isempty(SOLAR) + curtailment[SOLAR, :] = scale_factor * + (value.(EP[:eTotalCap_SOLAR][SOLAR]).data .* + inputs["pP_Max_Solar"][SOLAR, :] .- + value.(EP[:vP_SOLAR][SOLAR, :]).data) .* + etainverter.(gen_VRE_STOR[(gen_VRE_STOR.solar .!= 0)]) + end + if !isempty(WIND) + curtailment[WIND, :] = scale_factor * (value.(EP[:eTotalCap_WIND][WIND]).data .* + inputs["pP_Max_Wind"][WIND, :] .- + value.(EP[:vP_WIND][WIND, :]).data) + end + if !isempty(SOLAR_WIND) + curtailment[SOLAR_WIND, :] = scale_factor * + ((value.(EP[:eTotalCap_SOLAR])[SOLAR_WIND].data + .* + inputs["pP_Max_Solar"][SOLAR_WIND, :] .- + value.(EP[:vP_SOLAR][SOLAR_WIND, :]).data) + .* + etainverter.(gen_VRE_STOR[((gen_VRE_STOR.wind .!= 0) .& (gen_VRE_STOR.solar .!= 0))]) + + + (value.(EP[:eTotalCap_WIND][SOLAR_WIND]).data .* + inputs["pP_Max_Wind"][SOLAR_WIND, :] .- + value.(EP[:vP_WIND][SOLAR_WIND, :]).data)) + end + end - filename = joinpath(path, "curtail.csv") - if setup["WriteOutputs"] == "annual" - write_annual(filename, dfCurtailment) - else # setup["WriteOutputs"] == "full" - write_fulltimeseries(filename, curtailment, dfCurtailment) - end - return nothing + dfCurtailment.AnnualSum = curtailment * inputs["omega"] + + filename = joinpath(path, "curtail.csv") + if setup["WriteOutputs"] == "annual" + write_annual(filename, dfCurtailment) + else # setup["WriteOutputs"] == "full" + write_fulltimeseries(filename, curtailment, dfCurtailment) + end + return nothing end diff --git a/src/write_outputs/write_emissions.jl b/src/write_outputs/write_emissions.jl index f4aaa00550..60758c085f 100644 --- a/src/write_outputs/write_emissions.jl +++ b/src/write_outputs/write_emissions.jl @@ -5,92 +5,123 @@ Function for reporting time-dependent CO$_2$ emissions by zone. """ function write_emissions(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - - T = inputs["T"] # Number of time steps (hours) - Z = inputs["Z"] # Number of zones + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones - scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 + scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 - if (setup["WriteShadowPrices"]==1 || setup["UCommit"]==0 || (setup["UCommit"]==2 && (setup["OperationalReserves"]==0 || (setup["OperationalReserves"]>0 && inputs["pDynamic_Contingency"]==0)))) # fully linear model - # CO2 emissions by zone + if (setup["WriteShadowPrices"] == 1 || setup["UCommit"] == 0 || + (setup["UCommit"] == 2 && (setup["OperationalReserves"] == 0 || + (setup["OperationalReserves"] > 0 && inputs["pDynamic_Contingency"] == 0)))) # fully linear model + # CO2 emissions by zone - if setup["CO2Cap"]>=1 - # Dual variable of CO2 constraint = shadow price of CO2 - tempCO2Price = zeros(Z,inputs["NCO2Cap"]) - if has_duals(EP) == 1 - for cap in 1:inputs["NCO2Cap"] - for z in findall(x->x==1, inputs["dfCO2CapZones"][:,cap]) - tempCO2Price[z,cap] = (-1) * dual.(EP[:cCO2Emissions_systemwide])[cap] - # when scaled, The objective function is in unit of Million US$/kton, thus k$/ton, to get $/ton, multiply 1000 - tempCO2Price[z,cap] *= scale_factor - end - end - end - dfEmissions = hcat(DataFrame(Zone = 1:Z), DataFrame(tempCO2Price, :auto), DataFrame(AnnualSum = Array{Float64}(undef, Z))) - auxNew_Names=[Symbol("Zone"); [Symbol("CO2_Price_$cap") for cap in 1:inputs["NCO2Cap"]]; Symbol("AnnualSum")] - rename!(dfEmissions,auxNew_Names) - else - dfEmissions = DataFrame(Zone = 1:Z, AnnualSum = Array{Float64}(undef, Z)) - end + if setup["CO2Cap"] >= 1 + # Dual variable of CO2 constraint = shadow price of CO2 + tempCO2Price = zeros(Z, inputs["NCO2Cap"]) + if has_duals(EP) == 1 + for cap in 1:inputs["NCO2Cap"] + for z in findall(x -> x == 1, inputs["dfCO2CapZones"][:, cap]) + tempCO2Price[z, cap] = (-1) * + dual.(EP[:cCO2Emissions_systemwide])[cap] + # when scaled, The objective function is in unit of Million US$/kton, thus k$/ton, to get $/ton, multiply 1000 + tempCO2Price[z, cap] *= scale_factor + end + end + end + dfEmissions = hcat(DataFrame(Zone = 1:Z), + DataFrame(tempCO2Price, :auto), + DataFrame(AnnualSum = Array{Float64}(undef, Z))) + auxNew_Names = [Symbol("Zone"); + [Symbol("CO2_Price_$cap") for cap in 1:inputs["NCO2Cap"]]; + Symbol("AnnualSum")] + rename!(dfEmissions, auxNew_Names) + else + dfEmissions = DataFrame(Zone = 1:Z, AnnualSum = Array{Float64}(undef, Z)) + end - emissions_by_zone = value.(EP[:eEmissionsByZone]) - for i in 1:Z - dfEmissions[i,:AnnualSum] = sum(inputs["omega"] .* emissions_by_zone[i,:]) * scale_factor - end + emissions_by_zone = value.(EP[:eEmissionsByZone]) + for i in 1:Z + dfEmissions[i, :AnnualSum] = sum(inputs["omega"] .* emissions_by_zone[i, :]) * + scale_factor + end - if setup["WriteOutputs"] == "annual" - total = DataFrame(["Total" sum(dfEmissions.AnnualSum)], [:Zone;:AnnualSum]) - if setup["CO2Cap"]>=1 - total = DataFrame(["Total" zeros(1,inputs["NCO2Cap"]) sum(dfEmissions.AnnualSum)], [:Zone;[Symbol("CO2_Price_$cap") for cap in 1:inputs["NCO2Cap"]];:AnnualSum]) - end - dfEmissions = vcat(dfEmissions, total) - CSV.write(joinpath(path, "emissions.csv"), dfEmissions) - else # setup["WriteOutputs"] == "full" - dfEmissions = hcat(dfEmissions, DataFrame(emissions_by_zone * scale_factor, :auto)) - if setup["CO2Cap"]>=1 - auxNew_Names=[Symbol("Zone");[Symbol("CO2_Price_$cap") for cap in 1:inputs["NCO2Cap"]];Symbol("AnnualSum");[Symbol("t$t") for t in 1:T]] - rename!(dfEmissions,auxNew_Names) - total = DataFrame(["Total" zeros(1,inputs["NCO2Cap"]) sum(dfEmissions[!,:AnnualSum]) fill(0.0, (1,T))], :auto) - for t in 1:T - total[:,t+inputs["NCO2Cap"]+2] .= sum(dfEmissions[:,Symbol("t$t")][1:Z]) - end - else - auxNew_Names=[Symbol("Zone"); Symbol("AnnualSum"); [Symbol("t$t") for t in 1:T]] - rename!(dfEmissions,auxNew_Names) - total = DataFrame(["Total" sum(dfEmissions[!,:AnnualSum]) fill(0.0, (1,T))], :auto) - for t in 1:T - total[:,t+2] .= sum(dfEmissions[:,Symbol("t$t")][1:Z]) - end - end - rename!(total,auxNew_Names) - dfEmissions = vcat(dfEmissions, total) - CSV.write(joinpath(path, "emissions.csv"), dftranspose(dfEmissions, false), writeheader=false) - end -## Aaron - Combined elseif setup["Dual_MIP"]==1 block with the first block since they were identical. Why do we have this third case? What is different about it? - else - # CO2 emissions by zone - emissions_by_zone = value.(EP[:eEmissionsByZone]) - dfEmissions = hcat(DataFrame(Zone = 1:Z), DataFrame(AnnualSum = Array{Float64}(undef, Z))) - for i in 1:Z - dfEmissions[i,:AnnualSum] = sum(inputs["omega"] .* emissions_by_zone[i,:]) * scale_factor - end + if setup["WriteOutputs"] == "annual" + total = DataFrame(["Total" sum(dfEmissions.AnnualSum)], [:Zone; :AnnualSum]) + if setup["CO2Cap"] >= 1 + total = DataFrame(["Total" zeros(1, inputs["NCO2Cap"]) sum(dfEmissions.AnnualSum)], + [:Zone; + [Symbol("CO2_Price_$cap") for cap in 1:inputs["NCO2Cap"]]; + :AnnualSum]) + end + dfEmissions = vcat(dfEmissions, total) + CSV.write(joinpath(path, "emissions.csv"), dfEmissions) + else# setup["WriteOutputs"] == "full" + dfEmissions = hcat(dfEmissions, + DataFrame(emissions_by_zone * scale_factor, :auto)) + if setup["CO2Cap"] >= 1 + auxNew_Names = [Symbol("Zone"); + [Symbol("CO2_Price_$cap") for cap in 1:inputs["NCO2Cap"]]; + Symbol("AnnualSum"); + [Symbol("t$t") for t in 1:T]] + rename!(dfEmissions, auxNew_Names) + total = DataFrame(["Total" zeros(1, inputs["NCO2Cap"]) sum(dfEmissions[!, + :AnnualSum]) fill(0.0, (1, T))], + :auto) + for t in 1:T + total[:, t + inputs["NCO2Cap"] + 2] .= sum(dfEmissions[:, + Symbol("t$t")][1:Z]) + end + else + auxNew_Names = [Symbol("Zone"); + Symbol("AnnualSum"); + [Symbol("t$t") for t in 1:T]] + rename!(dfEmissions, auxNew_Names) + total = DataFrame(["Total" sum(dfEmissions[!, :AnnualSum]) fill(0.0, + (1, T))], + :auto) + for t in 1:T + total[:, t + 2] .= sum(dfEmissions[:, Symbol("t$t")][1:Z]) + end + end + rename!(total, auxNew_Names) + dfEmissions = vcat(dfEmissions, total) + CSV.write(joinpath(path, "emissions.csv"), + dftranspose(dfEmissions, false), + writeheader = false) + end + ## Aaron - Combined elseif setup["Dual_MIP"]==1 block with the first block since they were identical. Why do we have this third case? What is different about it? + else + # CO2 emissions by zone + emissions_by_zone = value.(EP[:eEmissionsByZone]) + dfEmissions = hcat(DataFrame(Zone = 1:Z), + DataFrame(AnnualSum = Array{Float64}(undef, Z))) + for i in 1:Z + dfEmissions[i, :AnnualSum] = sum(inputs["omega"] .* emissions_by_zone[i, :]) * + scale_factor + end - if setup["WriteOutputs"] == "annual" - total = DataFrame(["Total" sum(dfEmissions.AnnualSum)], [:Zone;:AnnualSum]) - dfEmissions = vcat(dfEmissions, total) - CSV.write(joinpath(path, "emissions.csv"), dfEmissions) - else # setup["WriteOutputs"] == "full" - dfEmissions = hcat(dfEmissions, DataFrame(emissions_by_zone * scale_factor, :auto)) - auxNew_Names=[Symbol("Zone");Symbol("AnnualSum");[Symbol("t$t") for t in 1:T]] - rename!(dfEmissions,auxNew_Names) - total = DataFrame(["Total" sum(dfEmissions[!,:AnnualSum]) fill(0.0, (1,T))], :auto) - for t in 1:T - total[:,t+2] .= sum(dfEmissions[:,Symbol("t$t")][1:Z]) - end - rename!(total,auxNew_Names) - dfEmissions = vcat(dfEmissions, total) - CSV.write(joinpath(path, "emissions.csv"), dftranspose(dfEmissions, false), writeheader=false) - end - end - return nothing + if setup["WriteOutputs"] == "annual" + total = DataFrame(["Total" sum(dfEmissions.AnnualSum)], [:Zone; :AnnualSum]) + dfEmissions = vcat(dfEmissions, total) + CSV.write(joinpath(path, "emissions.csv"), dfEmissions) + else# setup["WriteOutputs"] == "full" + dfEmissions = hcat(dfEmissions, + DataFrame(emissions_by_zone * scale_factor, :auto)) + auxNew_Names = [Symbol("Zone"); + Symbol("AnnualSum"); + [Symbol("t$t") for t in 1:T]] + rename!(dfEmissions, auxNew_Names) + total = DataFrame(["Total" sum(dfEmissions[!, :AnnualSum]) fill(0.0, (1, T))], + :auto) + for t in 1:T + total[:, t + 2] .= sum(dfEmissions[:, Symbol("t$t")][1:Z]) + end + rename!(total, auxNew_Names) + dfEmissions = vcat(dfEmissions, total) + CSV.write(joinpath(path, "emissions.csv"), + dftranspose(dfEmissions, false), + writeheader = false) + end + end + return nothing end diff --git a/src/write_outputs/write_energy_revenue.jl b/src/write_outputs/write_energy_revenue.jl index 92168c52f1..3e0834bd1e 100644 --- a/src/write_outputs/write_energy_revenue.jl +++ b/src/write_outputs/write_energy_revenue.jl @@ -4,26 +4,32 @@ Function for writing energy revenue from the different generation technologies. """ function write_energy_revenue(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - gen = inputs["RESOURCES"] - regions = region.(gen) - clusters = cluster.(gen) - zones = zone_id.(gen) + gen = inputs["RESOURCES"] + regions = region.(gen) + clusters = cluster.(gen) + zones = zone_id.(gen) - G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) - T = inputs["T"] # Number of time steps (hours) - FLEX = inputs["FLEX"] - NONFLEX = setdiff(collect(1:G), FLEX) - dfEnergyRevenue = DataFrame(Region = regions, Resource = inputs["RESOURCE_NAMES"], Zone = zones, Cluster = clusters, AnnualSum = Array{Float64}(undef, G),) - energyrevenue = zeros(G, T) + G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) + T = inputs["T"] # Number of time steps (hours) + FLEX = inputs["FLEX"] + NONFLEX = setdiff(collect(1:G), FLEX) + dfEnergyRevenue = DataFrame(Region = regions, + Resource = inputs["RESOURCE_NAMES"], + Zone = zones, + Cluster = clusters, + AnnualSum = Array{Float64}(undef, G)) + energyrevenue = zeros(G, T) price = locational_marginal_price(EP, inputs, setup) - energyrevenue[NONFLEX, :] = value.(EP[:vP][NONFLEX, :]) .* transpose(price)[zone_id.(gen[NONFLEX]), :] - if !isempty(FLEX) - energyrevenue[FLEX, :] = value.(EP[:vCHARGE_FLEX][FLEX, :]).data .* transpose(price)[zone_id.(gen[FLEX]), :] - end - if setup["ParameterScale"] == 1 - energyrevenue *= ModelScalingFactor - end - dfEnergyRevenue.AnnualSum .= energyrevenue * inputs["omega"] - write_simple_csv(joinpath(path, "EnergyRevenue.csv"), dfEnergyRevenue) - return dfEnergyRevenue + energyrevenue[NONFLEX, :] = value.(EP[:vP][NONFLEX, :]) .* + transpose(price)[zone_id.(gen[NONFLEX]), :] + if !isempty(FLEX) + energyrevenue[FLEX, :] = value.(EP[:vCHARGE_FLEX][FLEX, :]).data .* + transpose(price)[zone_id.(gen[FLEX]), :] + end + if setup["ParameterScale"] == 1 + energyrevenue *= ModelScalingFactor + end + dfEnergyRevenue.AnnualSum .= energyrevenue * inputs["omega"] + write_simple_csv(joinpath(path, "EnergyRevenue.csv"), dfEnergyRevenue) + return dfEnergyRevenue end diff --git a/src/write_outputs/write_fuel_consumption.jl b/src/write_outputs/write_fuel_consumption.jl index 7a661b9386..baff1301d6 100644 --- a/src/write_outputs/write_fuel_consumption.jl +++ b/src/write_outputs/write_fuel_consumption.jl @@ -4,57 +4,68 @@ Write fuel consumption of each power plant. """ function write_fuel_consumption(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - - write_fuel_consumption_plant(path::AbstractString,inputs::Dict, setup::Dict, EP::Model) - if setup["WriteOutputs"] != "annual" - write_fuel_consumption_ts(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - end - write_fuel_consumption_tot(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) + write_fuel_consumption_plant(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) + if setup["WriteOutputs"] != "annual" + write_fuel_consumption_ts(path::AbstractString, + inputs::Dict, + setup::Dict, + EP::Model) + end + write_fuel_consumption_tot(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) end -function write_fuel_consumption_plant(path::AbstractString,inputs::Dict, setup::Dict, EP::Model) - gen = inputs["RESOURCES"] +function write_fuel_consumption_plant(path::AbstractString, + inputs::Dict, + setup::Dict, + EP::Model) + gen = inputs["RESOURCES"] + + HAS_FUEL = inputs["HAS_FUEL"] + MULTI_FUELS = inputs["MULTI_FUELS"] - HAS_FUEL = inputs["HAS_FUEL"] - MULTI_FUELS = inputs["MULTI_FUELS"] + # Fuel consumption cost by each resource, including start up fuel + dfPlantFuel = DataFrame(Resource = inputs["RESOURCE_NAMES"][HAS_FUEL], + Fuel = fuel.(gen[HAS_FUEL]), + Zone = zone_id.(gen[HAS_FUEL]), + AnnualSumCosts = zeros(length(HAS_FUEL))) + tempannualsum = value.(EP[:ePlantCFuelOut][HAS_FUEL]) + + value.(EP[:ePlantCFuelStart][HAS_FUEL]) - # Fuel consumption cost by each resource, including start up fuel - dfPlantFuel = DataFrame(Resource = inputs["RESOURCE_NAMES"][HAS_FUEL], - Fuel = fuel.(gen[HAS_FUEL]), - Zone = zone_id.(gen[HAS_FUEL]), - AnnualSumCosts = zeros(length(HAS_FUEL))) - tempannualsum = value.(EP[:ePlantCFuelOut][HAS_FUEL]) + value.(EP[:ePlantCFuelStart][HAS_FUEL]) + if !isempty(MULTI_FUELS) + fuel_cols_num = inputs["FUEL_COLS"]# TODO: rename it + max_fuels = inputs["MAX_NUM_FUELS"] + dfPlantFuel.Multi_Fuels = multi_fuels.(gen[HAS_FUEL]) + for i in 1:max_fuels + tempannualsum_fuel_heat_multi_generation = zeros(length(HAS_FUEL)) + tempannualsum_fuel_heat_multi_start = zeros(length(HAS_FUEL)) + tempannualsum_fuel_heat_multi_total = zeros(length(HAS_FUEL)) + tempannualsum_fuel_cost_multi = zeros(length(HAS_FUEL)) + for g in MULTI_FUELS + tempannualsum_fuel_heat_multi_generation[findfirst(x -> x == g, HAS_FUEL)] = value.(EP[:ePlantFuelConsumptionYear_multi_generation][g, + i]) + tempannualsum_fuel_heat_multi_start[findfirst(x -> x == g, HAS_FUEL)] = value.(EP[:ePlantFuelConsumptionYear_multi_start][g, + i]) + tempannualsum_fuel_heat_multi_total[findfirst(x -> x == g, HAS_FUEL)] = value.(EP[:ePlantFuelConsumptionYear_multi][g, + i]) + tempannualsum_fuel_cost_multi[findfirst(x -> x == g, HAS_FUEL)] = value.(EP[:ePlantCFuelOut_multi][g, + i]) + value.(EP[:ePlantCFuelOut_multi_start][g, + i]) + end + if setup["ParameterScale"] == 1 + tempannualsum_fuel_heat_multi_generation *= ModelScalingFactor + tempannualsum_fuel_heat_multi_start *= ModelScalingFactor + tempannualsum_fuel_heat_multi_total *= ModelScalingFactor + tempannualsum_fuel_cost_multi *= ModelScalingFactor^2 + end - if !isempty(MULTI_FUELS) - fuel_cols_num = inputs["FUEL_COLS"] # TODO: rename it - max_fuels = inputs["MAX_NUM_FUELS"] - dfPlantFuel.Multi_Fuels = multi_fuels.(gen[HAS_FUEL]) - for i = 1:max_fuels - tempannualsum_fuel_heat_multi_generation = zeros(length(HAS_FUEL)) - tempannualsum_fuel_heat_multi_start = zeros(length(HAS_FUEL)) - tempannualsum_fuel_heat_multi_total = zeros(length(HAS_FUEL)) - tempannualsum_fuel_cost_multi = zeros(length(HAS_FUEL)) - for g in MULTI_FUELS - tempannualsum_fuel_heat_multi_generation[findfirst(x->x==g, HAS_FUEL)] = value.(EP[:ePlantFuelConsumptionYear_multi_generation][g,i]) - tempannualsum_fuel_heat_multi_start[findfirst(x->x==g, HAS_FUEL)] = value.(EP[:ePlantFuelConsumptionYear_multi_start][g,i]) - tempannualsum_fuel_heat_multi_total[findfirst(x->x==g, HAS_FUEL)] = value.(EP[:ePlantFuelConsumptionYear_multi][g,i]) - tempannualsum_fuel_cost_multi[findfirst(x->x==g, HAS_FUEL)] = value.(EP[:ePlantCFuelOut_multi][g,i]) + value.(EP[:ePlantCFuelOut_multi_start][g,i]) - end - if setup["ParameterScale"] == 1 - tempannualsum_fuel_heat_multi_generation *= ModelScalingFactor - tempannualsum_fuel_heat_multi_start *= ModelScalingFactor - tempannualsum_fuel_heat_multi_total *= ModelScalingFactor - tempannualsum_fuel_cost_multi *= ModelScalingFactor^2 - end + dfPlantFuel[!, fuel_cols_num[i]] = fuel_cols.(gen[HAS_FUEL], tag = i) + dfPlantFuel[!, Symbol(string(fuel_cols_num[i], "_AnnualSum_Fuel_HeatInput_Generation_MMBtu"))] = tempannualsum_fuel_heat_multi_generation + dfPlantFuel[!, Symbol(string(fuel_cols_num[i], "_AnnualSum_Fuel_HeatInput_Start_MMBtu"))] = tempannualsum_fuel_heat_multi_start + dfPlantFuel[!, Symbol(string(fuel_cols_num[i], "_AnnualSum_Fuel_HeatInput_Total_MMBtu"))] = tempannualsum_fuel_heat_multi_total + dfPlantFuel[!, Symbol(string(fuel_cols_num[i], "_AnnualSum_Fuel_Cost"))] = tempannualsum_fuel_cost_multi + end + end - dfPlantFuel[!, fuel_cols_num[i]] = fuel_cols.(gen[HAS_FUEL], tag=i) - dfPlantFuel[!, Symbol(string(fuel_cols_num[i],"_AnnualSum_Fuel_HeatInput_Generation_MMBtu"))] = tempannualsum_fuel_heat_multi_generation - dfPlantFuel[!, Symbol(string(fuel_cols_num[i],"_AnnualSum_Fuel_HeatInput_Start_MMBtu"))] = tempannualsum_fuel_heat_multi_start - dfPlantFuel[!, Symbol(string(fuel_cols_num[i],"_AnnualSum_Fuel_HeatInput_Total_MMBtu"))] = tempannualsum_fuel_heat_multi_total - dfPlantFuel[!, Symbol(string(fuel_cols_num[i],"_AnnualSum_Fuel_Cost"))] = tempannualsum_fuel_cost_multi - end - end - if setup["ParameterScale"] == 1 tempannualsum *= ModelScalingFactor^2 # end @@ -62,34 +73,38 @@ function write_fuel_consumption_plant(path::AbstractString,inputs::Dict, setup:: CSV.write(joinpath(path, "Fuel_cost_plant.csv"), dfPlantFuel) end +function write_fuel_consumption_ts(path::AbstractString, + inputs::Dict, + setup::Dict, + EP::Model) + T = inputs["T"] # Number of time steps (hours) + HAS_FUEL = inputs["HAS_FUEL"] -function write_fuel_consumption_ts(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - T = inputs["T"] # Number of time steps (hours) - HAS_FUEL = inputs["HAS_FUEL"] - - # Fuel consumption by each resource per time step, unit is MMBTU - dfPlantFuel_TS = DataFrame(Resource = inputs["RESOURCE_NAMES"][HAS_FUEL]) - tempts = value.(EP[:ePlantFuel_generation] + EP[:ePlantFuel_start])[HAS_FUEL,:] + # Fuel consumption by each resource per time step, unit is MMBTU + dfPlantFuel_TS = DataFrame(Resource = inputs["RESOURCE_NAMES"][HAS_FUEL]) + tempts = value.(EP[:ePlantFuel_generation] + EP[:ePlantFuel_start])[HAS_FUEL, :] if setup["ParameterScale"] == 1 tempts *= ModelScalingFactor # kMMBTU to MMBTU end - dfPlantFuel_TS = hcat(dfPlantFuel_TS, - DataFrame(tempts, [Symbol("t$t") for t in 1:T])) - CSV.write(joinpath(path, "FuelConsumption_plant_MMBTU.csv"), - dftranspose(dfPlantFuel_TS, false), header=false) + dfPlantFuel_TS = hcat(dfPlantFuel_TS, + DataFrame(tempts, [Symbol("t$t") for t in 1:T])) + CSV.write(joinpath(path, "FuelConsumption_plant_MMBTU.csv"), + dftranspose(dfPlantFuel_TS, false), header = false) end - -function write_fuel_consumption_tot(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - # types of fuel - fuel_types = inputs["fuels"] - fuel_number = length(fuel_types) - dfFuel = DataFrame(Fuel = fuel_types, - AnnualSum = zeros(fuel_number)) - tempannualsum = value.(EP[:eFuelConsumptionYear]) +function write_fuel_consumption_tot(path::AbstractString, + inputs::Dict, + setup::Dict, + EP::Model) + # types of fuel + fuel_types = inputs["fuels"] + fuel_number = length(fuel_types) + dfFuel = DataFrame(Fuel = fuel_types, + AnnualSum = zeros(fuel_number)) + tempannualsum = value.(EP[:eFuelConsumptionYear]) if setup["ParameterScale"] == 1 tempannualsum *= ModelScalingFactor # billion MMBTU to MMBTU end - dfFuel.AnnualSum .+= tempannualsum - CSV.write(joinpath(path,"FuelConsumption_total_MMBTU.csv"), dfFuel) + dfFuel.AnnualSum .+= tempannualsum + CSV.write(joinpath(path, "FuelConsumption_total_MMBTU.csv"), dfFuel) end diff --git a/src/write_outputs/write_maintenance.jl b/src/write_outputs/write_maintenance.jl index d00af7b696..f7a491828f 100644 --- a/src/write_outputs/write_maintenance.jl +++ b/src/write_outputs/write_maintenance.jl @@ -7,7 +7,7 @@ function write_simple_csv(filename::AbstractString, header::Vector, matrix) write_simple_csv(filename, df) end -function prepare_timeseries_variables(EP::Model, set::Set{Symbol}, scale::Float64=1.0) +function prepare_timeseries_variables(EP::Model, set::Set{Symbol}, scale::Float64 = 1.0) # function to extract data from DenseAxisArray data(var) = scale * value.(EP[var]).data diff --git a/src/write_outputs/write_net_revenue.jl b/src/write_outputs/write_net_revenue.jl index 3b3beb0772..a647a81dbd 100644 --- a/src/write_outputs/write_net_revenue.jl +++ b/src/write_outputs/write_net_revenue.jl @@ -3,223 +3,288 @@ Function for writing net revenue of different generation technologies. """ -function write_net_revenue(path::AbstractString, inputs::Dict, setup::Dict, EP::Model, dfCap::DataFrame, dfESRRev::DataFrame, dfResRevenue::DataFrame, dfChargingcost::DataFrame, dfPower::DataFrame, dfEnergyRevenue::DataFrame, dfSubRevenue::DataFrame, dfRegSubRevenue::DataFrame, dfVreStor::DataFrame, dfOpRegRevenue::DataFrame, dfOpRsvRevenue::DataFrame) - - gen = inputs["RESOURCES"] - zones = zone_id.(gen) - regions = region.(gen) - clusters = cluster.(gen) - rid = resource_id.(gen) - - G = inputs["G"] # Number of generators - COMMIT = inputs["COMMIT"] # Thermal units for unit commitment - STOR_ALL = inputs["STOR_ALL"] - - if setup["OperationalReserves"] >= 1 - RSV = inputs["RSV"] # Generators contributing to operating reserves - REG = inputs["REG"] # Generators contributing to regulation - end - - VRE_STOR = inputs["VRE_STOR"] - CCS = inputs["CCS"] - if !isempty(VRE_STOR) - gen_VRE_STOR = gen.VreStorage - VRE_STOR_LENGTH = size(inputs["VRE_STOR"])[1] - SOLAR = inputs["VS_SOLAR"] - WIND = inputs["VS_WIND"] - DC = inputs["VS_DC"] - DC_DISCHARGE = inputs["VS_STOR_DC_DISCHARGE"] - AC_DISCHARGE = inputs["VS_STOR_AC_DISCHARGE"] - DC_CHARGE = inputs["VS_STOR_DC_CHARGE"] - AC_CHARGE = inputs["VS_STOR_AC_CHARGE"] - # Should read in charge asymmetric capacities - end - - # Create a NetRevenue dataframe - dfNetRevenue = DataFrame(region = regions, Resource = inputs["RESOURCE_NAMES"], zone = zones, Cluster = clusters, R_ID = rid) - - # Add investment cost to the dataframe - dfNetRevenue.Inv_cost_MW = inv_cost_per_mwyr.(gen) .* dfCap[1:G,:NewCap] - dfNetRevenue.Inv_cost_MWh = inv_cost_per_mwhyr.(gen) .* dfCap[1:G,:NewEnergyCap] - dfNetRevenue.Inv_cost_charge_MW = inv_cost_charge_per_mwyr.(gen) .* dfCap[1:G,:NewChargeCap] - if !isempty(VRE_STOR) - # Doesn't include charge capacities - if !isempty(SOLAR) - dfNetRevenue.Inv_cost_MW[VRE_STOR] += inv_cost_solar_per_mwyr.(gen_VRE_STOR) .* dfVreStor[1:VRE_STOR_LENGTH,:NewCapSolar] - end - if !isempty(DC) - dfNetRevenue.Inv_cost_MW[VRE_STOR] += inv_cost_inverter_per_mwyr.(gen_VRE_STOR) .* dfVreStor[1:VRE_STOR_LENGTH,:NewCapDC] - end - if !isempty(WIND) - dfNetRevenue.Inv_cost_MW[VRE_STOR] += inv_cost_wind_per_mwyr.(gen_VRE_STOR) .* dfVreStor[1:VRE_STOR_LENGTH,:NewCapWind] - end - end - if setup["ParameterScale"] == 1 - dfNetRevenue.Inv_cost_MWh *= ModelScalingFactor # converting Million US$ to US$ - dfNetRevenue.Inv_cost_MW *= ModelScalingFactor # converting Million US$ to US$ - dfNetRevenue.Inv_cost_charge_MW *= ModelScalingFactor # converting Million US$ to US$ - end - - # Add operations and maintenance cost to the dataframe - dfNetRevenue.Fixed_OM_cost_MW = fixed_om_cost_per_mwyr.(gen) .* dfCap[1:G,:EndCap] - dfNetRevenue.Fixed_OM_cost_MWh = fixed_om_cost_per_mwhyr.(gen) .* dfCap[1:G,:EndEnergyCap] - dfNetRevenue.Fixed_OM_cost_charge_MW = fixed_om_cost_charge_per_mwyr.(gen) .* dfCap[1:G, :EndChargeCap] - - dfNetRevenue.Var_OM_cost_out = var_om_cost_per_mwh.(gen) .* dfPower[1:G,:AnnualSum] - if !isempty(VRE_STOR) - if !isempty(SOLAR) - dfNetRevenue.Fixed_OM_cost_MW[VRE_STOR] += fixed_om_solar_cost_per_mwyr.(gen_VRE_STOR) .* dfVreStor[1:VRE_STOR_LENGTH, :EndCapSolar] - dfNetRevenue.Var_OM_cost_out[SOLAR] += var_om_cost_per_mwh_solar.(gen_VRE_STOR[(gen_VRE_STOR.solar.!=0)]) .* (value.(EP[:vP_SOLAR][SOLAR, :]).data .* etainverter.(gen_VRE_STOR[(gen_VRE_STOR.solar.!=0)]) * inputs["omega"]) - end - if !isempty(WIND) - dfNetRevenue.Fixed_OM_cost_MW[VRE_STOR] += fixed_om_wind_cost_per_mwyr.(gen_VRE_STOR) .* dfVreStor[1:VRE_STOR_LENGTH, :EndCapWind] - dfNetRevenue.Var_OM_cost_out[WIND] += var_om_cost_per_mwh_wind.(gen_VRE_STOR[(gen_VRE_STOR.wind.!=0)]) .* (value.(EP[:vP_WIND][WIND, :]).data * inputs["omega"]) - end - if !isempty(DC) - dfNetRevenue.Fixed_OM_cost_MW[VRE_STOR] += fixed_om_inverter_cost_per_mwyr.(gen_VRE_STOR) .* dfVreStor[1:VRE_STOR_LENGTH, :EndCapDC] - end - if !isempty(DC_DISCHARGE) - dfNetRevenue.Var_OM_cost_out[DC_DISCHARGE] += var_om_cost_per_mwh_discharge_dc.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_discharge.!=0)]) .* (value.(EP[:vP_DC_DISCHARGE][DC_DISCHARGE, :]).data .* etainverter.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_discharge.!=0)]) * inputs["omega"]) - end - if !isempty(AC_DISCHARGE) - dfNetRevenue.Var_OM_cost_out[AC_DISCHARGE] += var_om_cost_per_mwh_discharge_ac.(gen_VRE_STOR[(gen_VRE_STOR.stor_ac_discharge.!=0)]) .* (value.(EP[:vP_AC_DISCHARGE][AC_DISCHARGE, :]).data * inputs["omega"]) - end - end - if setup["ParameterScale"] == 1 - dfNetRevenue.Fixed_OM_cost_MW *= ModelScalingFactor # converting Million US$ to US$ - dfNetRevenue.Fixed_OM_cost_MWh *= ModelScalingFactor # converting Million US$ to US$ - dfNetRevenue.Fixed_OM_cost_charge_MW *= ModelScalingFactor # converting Million US$ to US$ - dfNetRevenue.Var_OM_cost_out *= ModelScalingFactor # converting Million US$ to US$ - end - - # Add fuel cost to the dataframe - dfNetRevenue.Fuel_cost = sum(value.(EP[:ePlantCFuelOut]), dims = 2) - if setup["ParameterScale"] == 1 - dfNetRevenue.Fuel_cost *= ModelScalingFactor^2 # converting Million US$ to US$ - end - - # Add storage cost to the dataframe - dfNetRevenue.Var_OM_cost_in = zeros(nrow(dfNetRevenue)) - if !isempty(STOR_ALL) - dfNetRevenue.Var_OM_cost_in[STOR_ALL] = var_om_cost_per_mwh_in.(gen.Storage) .* ((value.(EP[:vCHARGE][STOR_ALL,:]).data) * inputs["omega"]) - end - if !isempty(VRE_STOR) - if !isempty(DC_CHARGE) - dfNetRevenue.Var_OM_cost_in[DC_CHARGE] += var_om_cost_per_mwh_charge_dc.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_charge.!=0)]) .* (value.(EP[:vP_DC_CHARGE][DC_CHARGE, :]).data ./ etainverter.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_charge.!=0)]) * inputs["omega"]) - end - if !isempty(AC_CHARGE) - dfNetRevenue.Var_OM_cost_in[AC_CHARGE] += var_om_cost_per_mwh_charge_ac.(gen_VRE_STOR[(gen_VRE_STOR.stor_ac_charge.!=0)]) .* (value.(EP[:vP_AC_CHARGE][AC_CHARGE, :]).data * inputs["omega"]) - end - end - - if setup["ParameterScale"] == 1 - dfNetRevenue.Var_OM_cost_in *= ModelScalingFactor^2 # converting Million US$ to US$ - end - # Add start-up cost to the dataframe - dfNetRevenue.StartCost = zeros(nrow(dfNetRevenue)) - if setup["UCommit"]>=1 && !isempty(COMMIT) - start_costs = vec(sum(value.(EP[:eCStart][COMMIT, :]).data, dims = 2)) - start_fuel_costs = vec(value.(EP[:ePlantCFuelStart][COMMIT])) - dfNetRevenue.StartCost[COMMIT] .= start_costs + start_fuel_costs - end - if setup["ParameterScale"] == 1 - dfNetRevenue.StartCost *= ModelScalingFactor^2 # converting Million US$ to US$ - end - # Add charge cost to the dataframe - dfNetRevenue.Charge_cost = zeros(nrow(dfNetRevenue)) - if has_duals(EP) - dfNetRevenue.Charge_cost = dfChargingcost[1:G,:AnnualSum] # Unit is confirmed to be US$ - end - - # Add CO2 releated sequestration cost or credit (e.g. 45 Q) to the dataframe - dfNetRevenue.CO2SequestrationCost = zeros(nrow(dfNetRevenue)) - if any(co2_capture_fraction.(gen) .!= 0) - dfNetRevenue.CO2SequestrationCost = zeros(G) - dfNetRevenue[CCS, :CO2SequestrationCost] = value.(EP[:ePlantCCO2Sequestration]).data - end - if setup["ParameterScale"] == 1 - dfNetRevenue.CO2SequestrationCost *= ModelScalingFactor^2 # converting Million US$ to US$ - end - - # Add energy and subsidy revenue to the dataframe - dfNetRevenue.EnergyRevenue = zeros(nrow(dfNetRevenue)) - dfNetRevenue.SubsidyRevenue = zeros(nrow(dfNetRevenue)) - if has_duals(EP) - dfNetRevenue.EnergyRevenue = dfEnergyRevenue[1:G,:AnnualSum] # Unit is confirmed to be US$ - dfNetRevenue.SubsidyRevenue = dfSubRevenue[1:G,:SubsidyRevenue] # Unit is confirmed to be US$ - end - - # Add energy and subsidy revenue to the dataframe - dfNetRevenue.OperatingReserveRevenue = zeros(nrow(dfNetRevenue)) - dfNetRevenue.OperatingRegulationRevenue = zeros(nrow(dfNetRevenue)) - if setup["OperationalReserves"] > 0 && has_duals(EP) - dfNetRevenue.OperatingReserveRevenue[RSV] = dfOpRsvRevenue.AnnualSum # Unit is confirmed to be US$ - dfNetRevenue.OperatingRegulationRevenue[REG] = dfOpRegRevenue.AnnualSum # Unit is confirmed to be US$ - end - - # Add capacity revenue to the dataframe - dfNetRevenue.ReserveMarginRevenue = zeros(nrow(dfNetRevenue)) - if setup["CapacityReserveMargin"] > 0 && has_duals(EP) # The unit is confirmed to be $ - dfNetRevenue.ReserveMarginRevenue = dfResRevenue[1:G,:AnnualSum] - end - - # Add RPS/CES revenue to the dataframe - dfNetRevenue.ESRRevenue = zeros(nrow(dfNetRevenue)) - if setup["EnergyShareRequirement"] > 0 && has_duals(EP) # The unit is confirmed to be $ - dfNetRevenue.ESRRevenue = dfESRRev[1:G,:Total] - end - - # Calculate emissions cost - dfNetRevenue.EmissionsCost = zeros(nrow(dfNetRevenue)) - if setup["CO2Cap"] >=1 && has_duals(EP) - for cap in 1:inputs["NCO2Cap"] - co2_cap_dual = dual(EP[:cCO2Emissions_systemwide][cap]) - CO2ZONES = findall(x->x==1, inputs["dfCO2CapZones"][:,cap]) - GEN_IN_ZONE = resource_id.(gen[[y in CO2ZONES for y in zone_id.(gen)]]) - if setup["CO2Cap"]==1 || setup["CO2Cap"]==2 # Mass-based or Demand + Rate-based - # Cost = sum(sum(emissions for zone z * dual(CO2 constraint[cap]) for z in Z) for cap in setup["NCO2"]) - temp_vec = value.(EP[:eEmissionsByPlant][GEN_IN_ZONE, :]) * inputs["omega"] - dfNetRevenue.EmissionsCost[GEN_IN_ZONE] += - co2_cap_dual * temp_vec - elseif setup["CO2Cap"]==3 # Generation + Rate-based - SET_WITH_MAXCO2RATE = union(inputs["THERM_ALL"],inputs["VRE"], inputs["VRE"],inputs["MUST_RUN"],inputs["HYDRO_RES"]) - Y = intersect(GEN_IN_ZONE, SET_WITH_MAXCO2RATE) - temp_vec = (value.(EP[:eEmissionsByPlant][Y,:]) - (value.(EP[:vP][Y,:]) .* inputs["dfMaxCO2Rate"][zone_id.(gen[Y]), cap])) * inputs["omega"] - dfNetRevenue.EmissionsCost[Y] += - co2_cap_dual * temp_vec - end - end - if setup["ParameterScale"] == 1 - dfNetRevenue.EmissionsCost *= ModelScalingFactor^2 # converting Million US$ to US$ - end - end - - # Add regional technology subsidy revenue to the dataframe - dfNetRevenue.RegSubsidyRevenue = zeros(nrow(dfNetRevenue)) - if setup["MinCapReq"] >= 1 && has_duals(EP)# The unit is confirmed to be US$ - dfNetRevenue.RegSubsidyRevenue = dfRegSubRevenue[1:G,:SubsidyRevenue] - end - - dfNetRevenue.Revenue = dfNetRevenue.EnergyRevenue - .+ dfNetRevenue.SubsidyRevenue - .+ dfNetRevenue.ReserveMarginRevenue - .+ dfNetRevenue.ESRRevenue - .+ dfNetRevenue.RegSubsidyRevenue - .+ dfNetRevenue.OperatingReserveRevenue - .+ dfNetRevenue.OperatingRegulationRevenue - - dfNetRevenue.Cost = (dfNetRevenue.Inv_cost_MW - .+ dfNetRevenue.Inv_cost_MWh - .+ dfNetRevenue.Inv_cost_charge_MW - .+ dfNetRevenue.Fixed_OM_cost_MW - .+ dfNetRevenue.Fixed_OM_cost_MWh - .+ dfNetRevenue.Fixed_OM_cost_charge_MW - .+ dfNetRevenue.Var_OM_cost_out - .+ dfNetRevenue.Var_OM_cost_in - .+ dfNetRevenue.Fuel_cost - .+ dfNetRevenue.Charge_cost - .+ dfNetRevenue.EmissionsCost - .+ dfNetRevenue.StartCost - .+ dfNetRevenue.CO2SequestrationCost) - dfNetRevenue.Profit = dfNetRevenue.Revenue .- dfNetRevenue.Cost - - CSV.write(joinpath(path, "NetRevenue.csv"), dfNetRevenue) +function write_net_revenue(path::AbstractString, + inputs::Dict, + setup::Dict, + EP::Model, + dfCap::DataFrame, + dfESRRev::DataFrame, + dfResRevenue::DataFrame, + dfChargingcost::DataFrame, + dfPower::DataFrame, + dfEnergyRevenue::DataFrame, + dfSubRevenue::DataFrame, + dfRegSubRevenue::DataFrame, + dfVreStor::DataFrame, + dfOpRegRevenue::DataFrame, + dfOpRsvRevenue::DataFrame) + gen = inputs["RESOURCES"] + zones = zone_id.(gen) + regions = region.(gen) + clusters = cluster.(gen) + rid = resource_id.(gen) + + G = inputs["G"] # Number of generators + COMMIT = inputs["COMMIT"]# Thermal units for unit commitment + STOR_ALL = inputs["STOR_ALL"] + + if setup["OperationalReserves"] >= 1 + RSV = inputs["RSV"]# Generators contributing to operating reserves + REG = inputs["REG"] # Generators contributing to regulation + end + + VRE_STOR = inputs["VRE_STOR"] + CCS = inputs["CCS"] + if !isempty(VRE_STOR) + gen_VRE_STOR = gen.VreStorage + VRE_STOR_LENGTH = size(inputs["VRE_STOR"])[1] + SOLAR = inputs["VS_SOLAR"] + WIND = inputs["VS_WIND"] + DC = inputs["VS_DC"] + DC_DISCHARGE = inputs["VS_STOR_DC_DISCHARGE"] + AC_DISCHARGE = inputs["VS_STOR_AC_DISCHARGE"] + DC_CHARGE = inputs["VS_STOR_DC_CHARGE"] + AC_CHARGE = inputs["VS_STOR_AC_CHARGE"] + # Should read in charge asymmetric capacities + end + + # Create a NetRevenue dataframe + dfNetRevenue = DataFrame(region = regions, + Resource = inputs["RESOURCE_NAMES"], + zone = zones, + Cluster = clusters, + R_ID = rid) + + # Add investment cost to the dataframe + dfNetRevenue.Inv_cost_MW = inv_cost_per_mwyr.(gen) .* dfCap[1:G, :NewCap] + dfNetRevenue.Inv_cost_MWh = inv_cost_per_mwhyr.(gen) .* dfCap[1:G, :NewEnergyCap] + dfNetRevenue.Inv_cost_charge_MW = inv_cost_charge_per_mwyr.(gen) .* + dfCap[1:G, :NewChargeCap] + if !isempty(VRE_STOR) + # Doesn't include charge capacities + if !isempty(SOLAR) + dfNetRevenue.Inv_cost_MW[VRE_STOR] += inv_cost_solar_per_mwyr.(gen_VRE_STOR) .* + dfVreStor[1:VRE_STOR_LENGTH, :NewCapSolar] + end + if !isempty(DC) + dfNetRevenue.Inv_cost_MW[VRE_STOR] += inv_cost_inverter_per_mwyr.(gen_VRE_STOR) .* + dfVreStor[1:VRE_STOR_LENGTH, :NewCapDC] + end + if !isempty(WIND) + dfNetRevenue.Inv_cost_MW[VRE_STOR] += inv_cost_wind_per_mwyr.(gen_VRE_STOR) .* + dfVreStor[1:VRE_STOR_LENGTH, :NewCapWind] + end + end + if setup["ParameterScale"] == 1 + dfNetRevenue.Inv_cost_MWh *= ModelScalingFactor # converting Million US$ to US$ + dfNetRevenue.Inv_cost_MW *= ModelScalingFactor # converting Million US$ to US$ + dfNetRevenue.Inv_cost_charge_MW *= ModelScalingFactor # converting Million US$ to US$ + end + + # Add operations and maintenance cost to the dataframe + dfNetRevenue.Fixed_OM_cost_MW = fixed_om_cost_per_mwyr.(gen) .* dfCap[1:G, :EndCap] + dfNetRevenue.Fixed_OM_cost_MWh = fixed_om_cost_per_mwhyr.(gen) .* + dfCap[1:G, :EndEnergyCap] + dfNetRevenue.Fixed_OM_cost_charge_MW = fixed_om_cost_charge_per_mwyr.(gen) .* + dfCap[1:G, :EndChargeCap] + + dfNetRevenue.Var_OM_cost_out = var_om_cost_per_mwh.(gen) .* dfPower[1:G, :AnnualSum] + if !isempty(VRE_STOR) + if !isempty(SOLAR) + dfNetRevenue.Fixed_OM_cost_MW[VRE_STOR] += fixed_om_solar_cost_per_mwyr.(gen_VRE_STOR) .* + dfVreStor[1:VRE_STOR_LENGTH, + :EndCapSolar] + dfNetRevenue.Var_OM_cost_out[SOLAR] += var_om_cost_per_mwh_solar.(gen_VRE_STOR[(gen_VRE_STOR.solar .!= 0)]) .* + (value.(EP[:vP_SOLAR][SOLAR, :]).data .* + etainverter.(gen_VRE_STOR[(gen_VRE_STOR.solar .!= 0)]) * + inputs["omega"]) + end + if !isempty(WIND) + dfNetRevenue.Fixed_OM_cost_MW[VRE_STOR] += fixed_om_wind_cost_per_mwyr.(gen_VRE_STOR) .* + dfVreStor[1:VRE_STOR_LENGTH, + :EndCapWind] + dfNetRevenue.Var_OM_cost_out[WIND] += var_om_cost_per_mwh_wind.(gen_VRE_STOR[(gen_VRE_STOR.wind .!= 0)]) .* + (value.(EP[:vP_WIND][WIND, :]).data * + inputs["omega"]) + end + if !isempty(DC) + dfNetRevenue.Fixed_OM_cost_MW[VRE_STOR] += fixed_om_inverter_cost_per_mwyr.(gen_VRE_STOR) .* + dfVreStor[1:VRE_STOR_LENGTH, + :EndCapDC] + end + if !isempty(DC_DISCHARGE) + dfNetRevenue.Var_OM_cost_out[DC_DISCHARGE] += var_om_cost_per_mwh_discharge_dc.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_discharge .!= 0)]) .* + (value.(EP[:vP_DC_DISCHARGE][DC_DISCHARGE, + :]).data .* etainverter.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_discharge .!= 0)]) * + inputs["omega"]) + end + if !isempty(AC_DISCHARGE) + dfNetRevenue.Var_OM_cost_out[AC_DISCHARGE] += var_om_cost_per_mwh_discharge_ac.(gen_VRE_STOR[(gen_VRE_STOR.stor_ac_discharge .!= 0)]) .* + (value.(EP[:vP_AC_DISCHARGE][AC_DISCHARGE, + :]).data * inputs["omega"]) + end + end + if setup["ParameterScale"] == 1 + dfNetRevenue.Fixed_OM_cost_MW *= ModelScalingFactor # converting Million US$ to US$ + dfNetRevenue.Fixed_OM_cost_MWh *= ModelScalingFactor # converting Million US$ to US$ + dfNetRevenue.Fixed_OM_cost_charge_MW *= ModelScalingFactor # converting Million US$ to US$ + dfNetRevenue.Var_OM_cost_out *= ModelScalingFactor # converting Million US$ to US$ + end + + # Add fuel cost to the dataframe + dfNetRevenue.Fuel_cost = sum(value.(EP[:ePlantCFuelOut]), dims = 2) + if setup["ParameterScale"] == 1 + dfNetRevenue.Fuel_cost *= ModelScalingFactor^2 # converting Million US$ to US$ + end + + # Add storage cost to the dataframe + dfNetRevenue.Var_OM_cost_in = zeros(nrow(dfNetRevenue)) + if !isempty(STOR_ALL) + dfNetRevenue.Var_OM_cost_in[STOR_ALL] = var_om_cost_per_mwh_in.(gen.Storage) .* + ((value.(EP[:vCHARGE][STOR_ALL, :]).data) * + inputs["omega"]) + end + if !isempty(VRE_STOR) + if !isempty(DC_CHARGE) + dfNetRevenue.Var_OM_cost_in[DC_CHARGE] += var_om_cost_per_mwh_charge_dc.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_charge .!= 0)]) .* + (value.(EP[:vP_DC_CHARGE][DC_CHARGE, + :]).data ./ etainverter.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_charge .!= 0)]) * + inputs["omega"]) + end + if !isempty(AC_CHARGE) + dfNetRevenue.Var_OM_cost_in[AC_CHARGE] += var_om_cost_per_mwh_charge_ac.(gen_VRE_STOR[(gen_VRE_STOR.stor_ac_charge .!= 0)]) .* + (value.(EP[:vP_AC_CHARGE][AC_CHARGE, + :]).data * inputs["omega"]) + end + end + + if setup["ParameterScale"] == 1 + dfNetRevenue.Var_OM_cost_in *= ModelScalingFactor^2 # converting Million US$ to US$ + end + # Add start-up cost to the dataframe + dfNetRevenue.StartCost = zeros(nrow(dfNetRevenue)) + if setup["UCommit"] >= 1 && !isempty(COMMIT) + start_costs = vec(sum(value.(EP[:eCStart][COMMIT, :]).data, dims = 2)) + start_fuel_costs = vec(value.(EP[:ePlantCFuelStart][COMMIT])) + dfNetRevenue.StartCost[COMMIT] .= start_costs + start_fuel_costs + end + if setup["ParameterScale"] == 1 + dfNetRevenue.StartCost *= ModelScalingFactor^2 # converting Million US$ to US$ + end + # Add charge cost to the dataframe + dfNetRevenue.Charge_cost = zeros(nrow(dfNetRevenue)) + if has_duals(EP) + dfNetRevenue.Charge_cost = dfChargingcost[1:G, :AnnualSum] # Unit is confirmed to be US$ + end + + # Add CO2 releated sequestration cost or credit (e.g. 45 Q) to the dataframe + dfNetRevenue.CO2SequestrationCost = zeros(nrow(dfNetRevenue)) + if any(co2_capture_fraction.(gen) .!= 0) + dfNetRevenue.CO2SequestrationCost = zeros(G) + dfNetRevenue[CCS, :CO2SequestrationCost] = value.(EP[:ePlantCCO2Sequestration]).data + end + if setup["ParameterScale"] == 1 + dfNetRevenue.CO2SequestrationCost *= ModelScalingFactor^2 # converting Million US$ to US$ + end + + # Add energy and subsidy revenue to the dataframe + dfNetRevenue.EnergyRevenue = zeros(nrow(dfNetRevenue)) + dfNetRevenue.SubsidyRevenue = zeros(nrow(dfNetRevenue)) + if has_duals(EP) + dfNetRevenue.EnergyRevenue = dfEnergyRevenue[1:G, :AnnualSum] # Unit is confirmed to be US$ + dfNetRevenue.SubsidyRevenue = dfSubRevenue[1:G, :SubsidyRevenue] # Unit is confirmed to be US$ + end + + # Add energy and subsidy revenue to the dataframe + dfNetRevenue.OperatingReserveRevenue = zeros(nrow(dfNetRevenue)) + dfNetRevenue.OperatingRegulationRevenue = zeros(nrow(dfNetRevenue)) + if setup["OperationalReserves"] > 0 && has_duals(EP) + dfNetRevenue.OperatingReserveRevenue[RSV] = dfOpRsvRevenue.AnnualSum # Unit is confirmed to be US$ + dfNetRevenue.OperatingRegulationRevenue[REG] = dfOpRegRevenue.AnnualSum # Unit is confirmed to be US$ + end + + # Add capacity revenue to the dataframe + dfNetRevenue.ReserveMarginRevenue = zeros(nrow(dfNetRevenue)) + if setup["CapacityReserveMargin"] > 0 && has_duals(EP) # The unit is confirmed to be $ + dfNetRevenue.ReserveMarginRevenue = dfResRevenue[1:G, :AnnualSum] + end + + # Add RPS/CES revenue to the dataframe + dfNetRevenue.ESRRevenue = zeros(nrow(dfNetRevenue)) + if setup["EnergyShareRequirement"] > 0 && has_duals(EP) # The unit is confirmed to be $ + dfNetRevenue.ESRRevenue = dfESRRev[1:G, :Total] + end + + # Calculate emissions cost + dfNetRevenue.EmissionsCost = zeros(nrow(dfNetRevenue)) + if setup["CO2Cap"] >= 1 && has_duals(EP) + for cap in 1:inputs["NCO2Cap"] + co2_cap_dual = dual(EP[:cCO2Emissions_systemwide][cap]) + CO2ZONES = findall(x -> x == 1, inputs["dfCO2CapZones"][:, cap]) + GEN_IN_ZONE = resource_id.(gen[[y in CO2ZONES for y in zone_id.(gen)]]) + if setup["CO2Cap"] == 1 || setup["CO2Cap"] == 2 # Mass-based or Demand + Rate-based + # Cost = sum(sum(emissions for zone z * dual(CO2 constraint[cap]) for z in Z) for cap in setup["NCO2"]) + temp_vec = value.(EP[:eEmissionsByPlant][GEN_IN_ZONE, :]) * inputs["omega"] + dfNetRevenue.EmissionsCost[GEN_IN_ZONE] += -co2_cap_dual * temp_vec + elseif setup["CO2Cap"] == 3 # Generation + Rate-based + SET_WITH_MAXCO2RATE = union(inputs["THERM_ALL"], + inputs["VRE"], + inputs["VRE"], + inputs["MUST_RUN"], + inputs["HYDRO_RES"]) + Y = intersect(GEN_IN_ZONE, SET_WITH_MAXCO2RATE) + temp_vec = (value.(EP[:eEmissionsByPlant][Y, :]) - + (value.(EP[:vP][Y, :]) .* + inputs["dfMaxCO2Rate"][zone_id.(gen[Y]), cap])) * + inputs["omega"] + dfNetRevenue.EmissionsCost[Y] += -co2_cap_dual * temp_vec + end + end + if setup["ParameterScale"] == 1 + dfNetRevenue.EmissionsCost *= ModelScalingFactor^2 # converting Million US$ to US$ + end + end + + # Add regional technology subsidy revenue to the dataframe + dfNetRevenue.RegSubsidyRevenue = zeros(nrow(dfNetRevenue)) + if setup["MinCapReq"] >= 1 && has_duals(EP)# The unit is confirmed to be US$ + dfNetRevenue.RegSubsidyRevenue = dfRegSubRevenue[1:G, :SubsidyRevenue] + end + + dfNetRevenue.Revenue = dfNetRevenue.EnergyRevenue + .+dfNetRevenue.SubsidyRevenue + .+dfNetRevenue.ReserveMarginRevenue + .+dfNetRevenue.ESRRevenue + .+dfNetRevenue.RegSubsidyRevenue + .+dfNetRevenue.OperatingReserveRevenue + .+dfNetRevenue.OperatingRegulationRevenue + + dfNetRevenue.Cost = (dfNetRevenue.Inv_cost_MW + .+ + dfNetRevenue.Inv_cost_MWh + .+ + dfNetRevenue.Inv_cost_charge_MW + .+ + dfNetRevenue.Fixed_OM_cost_MW + .+ + dfNetRevenue.Fixed_OM_cost_MWh + .+ + dfNetRevenue.Fixed_OM_cost_charge_MW + .+ + dfNetRevenue.Var_OM_cost_out + .+ + dfNetRevenue.Var_OM_cost_in + .+ + dfNetRevenue.Fuel_cost + .+ + dfNetRevenue.Charge_cost + .+ + dfNetRevenue.EmissionsCost + .+ + dfNetRevenue.StartCost + .+ + dfNetRevenue.CO2SequestrationCost) + dfNetRevenue.Profit = dfNetRevenue.Revenue .- dfNetRevenue.Cost + + CSV.write(joinpath(path, "NetRevenue.csv"), dfNetRevenue) end diff --git a/src/write_outputs/write_nse.jl b/src/write_outputs/write_nse.jl index 5d30dcc987..3cdc1104a7 100644 --- a/src/write_outputs/write_nse.jl +++ b/src/write_outputs/write_nse.jl @@ -4,33 +4,39 @@ Function for reporting non-served energy for every model zone, time step and cost-segment. """ function write_nse(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - T = inputs["T"] # Number of time steps (hours) - Z = inputs["Z"] # Number of zones - SEG = inputs["SEG"] # Number of demand curtailment segments - # Non-served energy/demand curtailment by segment in each time step - dfNse = DataFrame(Segment = repeat(1:SEG, outer = Z), Zone = repeat(1:Z, inner = SEG), AnnualSum = zeros(SEG * Z)) - nse = zeros(SEG * Z, T) - scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 - for z in 1:Z - nse[((z-1)*SEG+1):z*SEG, :] = value.(EP[:vNSE])[:, :, z] * scale_factor - end - dfNse.AnnualSum .= nse * inputs["omega"] + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones + SEG = inputs["SEG"] # Number of demand curtailment segments + # Non-served energy/demand curtailment by segment in each time step + dfNse = DataFrame(Segment = repeat(1:SEG, outer = Z), + Zone = repeat(1:Z, inner = SEG), + AnnualSum = zeros(SEG * Z)) + nse = zeros(SEG * Z, T) + scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 + for z in 1:Z + nse[((z - 1) * SEG + 1):(z * SEG), :] = value.(EP[:vNSE])[:, :, z] * scale_factor + end + dfNse.AnnualSum .= nse * inputs["omega"] - if setup["WriteOutputs"] == "annual" - total = DataFrame(["Total" 0 sum(dfNse[!,:AnnualSum])], [:Segment, :Zone, :AnnualSum]) - dfNse = vcat(dfNse, total) - CSV.write(joinpath(path, "nse.csv"), dfNse) - else # setup["WriteOutputs"] == "full" - dfNse = hcat(dfNse, DataFrame(nse, :auto)) - auxNew_Names=[Symbol("Segment");Symbol("Zone");Symbol("AnnualSum");[Symbol("t$t") for t in 1:T]] - rename!(dfNse,auxNew_Names) + if setup["WriteOutputs"] == "annual" + total = DataFrame(["Total" 0 sum(dfNse[!, :AnnualSum])], + [:Segment, :Zone, :AnnualSum]) + dfNse = vcat(dfNse, total) + CSV.write(joinpath(path, "nse.csv"), dfNse) + else # setup["WriteOutputs"] == "full" + dfNse = hcat(dfNse, DataFrame(nse, :auto)) + auxNew_Names = [Symbol("Segment"); + Symbol("Zone"); + Symbol("AnnualSum"); + [Symbol("t$t") for t in 1:T]] + rename!(dfNse, auxNew_Names) - total = DataFrame(["Total" 0 sum(dfNse[!,:AnnualSum]) fill(0.0, (1,T))], :auto) - total[:, 4:T+3] .= sum(nse, dims = 1) - rename!(total,auxNew_Names) - dfNse = vcat(dfNse, total) + total = DataFrame(["Total" 0 sum(dfNse[!, :AnnualSum]) fill(0.0, (1, T))], :auto) + total[:, 4:(T + 3)] .= sum(nse, dims = 1) + rename!(total, auxNew_Names) + dfNse = vcat(dfNse, total) - CSV.write(joinpath(path, "nse.csv"), dftranspose(dfNse, false), writeheader=false) - end - return nothing + CSV.write(joinpath(path, "nse.csv"), dftranspose(dfNse, false), writeheader = false) + end + return nothing end diff --git a/src/write_outputs/write_outputs.jl b/src/write_outputs/write_outputs.jl index 8e88ec0dc1..2c374c0b35 100644 --- a/src/write_outputs/write_outputs.jl +++ b/src/write_outputs/write_outputs.jl @@ -11,389 +11,471 @@ Function for the entry-point for writing the different output files. From here, onward several other functions are called, each for writing specific output files, like costs, capacities, etc. """ function write_outputs(EP::Model, path::AbstractString, setup::Dict, inputs::Dict) - - if setup["OverwriteResults"] == 1 - # Overwrite existing results if dir exists - # This is the default behaviour when there is no flag, to avoid breaking existing code - if !(isdir(path)) - mkpath(path) - end - else - # Find closest unused ouput directory name and create it - path = choose_output_dir(path) - mkpath(path) - end - - # https://jump.dev/MathOptInterface.jl/v0.9.10/apireference/#MathOptInterface.TerminationStatusCode - status = termination_status(EP) - - ## Check if solved sucessfully - time out is included - if status != MOI.OPTIMAL && status != MOI.LOCALLY_SOLVED - if status != MOI.TIME_LIMIT # Model failed to solve, so record solver status and exit - write_status(path, inputs, setup, EP) - return - # Model reached timelimit but failed to find a feasible solution - #### Aaron Schwartz - Not sure if the below condition is valid anymore. We should revisit #### - elseif isnan(objective_value(EP))==true - # Model failed to solve, so record solver status and exit - write_status(path, inputs, setup, EP) - return - end - end - - # Dict containing the list of outputs to write - output_settings_d = setup["WriteOutputsSettingsDict"] - write_settings_file(path, setup) - - output_settings_d["WriteStatus"] && write_status(path, inputs, setup, EP) - - # linearize and re-solve model if duals are not available but ShadowPrices are requested - if !has_duals(EP) && setup["WriteShadowPrices"] == 1 - # function to fix integers and linearize problem - fix_integers(EP) - # re-solve statement for LP solution - println("Solving LP solution for duals") - set_silent(EP) - optimize!(EP) - end - - if output_settings_d["WriteCosts"] - elapsed_time_costs = @elapsed write_costs(path, inputs, setup, EP) - println("Time elapsed for writing costs is") - println(elapsed_time_costs) - end - - if output_settings_d["WriteCapacity"] || output_settings_d["WriteNetRevenue"] - elapsed_time_capacity = @elapsed dfCap = write_capacity(path, inputs, setup, EP) - println("Time elapsed for writing capacity is") - println(elapsed_time_capacity) - end - - if output_settings_d["WritePower"] || output_settings_d["WriteNetRevenue"] - elapsed_time_power = @elapsed dfPower = write_power(path, inputs, setup, EP) - println("Time elapsed for writing power is") - println(elapsed_time_power) - end - - if output_settings_d["WriteCharge"] - elapsed_time_charge = @elapsed write_charge(path, inputs, setup, EP) - println("Time elapsed for writing charge is") - println(elapsed_time_charge) - end - - if output_settings_d["WriteCapacityFactor"] - elapsed_time_capacityfactor = @elapsed write_capacityfactor(path, inputs, setup, EP) - println("Time elapsed for writing capacity factor is") - println(elapsed_time_capacityfactor) - end - - if output_settings_d["WriteStorage"] - elapsed_time_storage = @elapsed write_storage(path, inputs, setup, EP) - println("Time elapsed for writing storage is") - println(elapsed_time_storage) - end - - if output_settings_d["WriteCurtailment"] - elapsed_time_curtailment = @elapsed write_curtailment(path, inputs, setup, EP) - println("Time elapsed for writing curtailment is") - println(elapsed_time_curtailment) - end - - if output_settings_d["WriteNSE"] - elapsed_time_nse = @elapsed write_nse(path, inputs, setup, EP) - println("Time elapsed for writing nse is") - println(elapsed_time_nse) - end - - if output_settings_d["WritePowerBalance"] - elapsed_time_power_balance = @elapsed write_power_balance(path, inputs, setup, EP) - println("Time elapsed for writing power balance is") - println(elapsed_time_power_balance) - end - - if inputs["Z"] > 1 - if output_settings_d["WriteTransmissionFlows"] - elapsed_time_flows = @elapsed write_transmission_flows(path, inputs, setup, EP) - println("Time elapsed for writing transmission flows is") - println(elapsed_time_flows) - end - - if output_settings_d["WriteTransmissionLosses"] - elapsed_time_losses = @elapsed write_transmission_losses(path, inputs, setup, EP) - println("Time elapsed for writing transmission losses is") - println(elapsed_time_losses) - end - - if setup["NetworkExpansion"] == 1 && output_settings_d["WriteNWExpansion"] - elapsed_time_expansion = @elapsed write_nw_expansion(path, inputs, setup, EP) - println("Time elapsed for writing network expansion is") - println(elapsed_time_expansion) - end - end - - if output_settings_d["WriteEmissions"] - elapsed_time_emissions = @elapsed write_emissions(path, inputs, setup, EP) - println("Time elapsed for writing emissions is") - println(elapsed_time_emissions) - end - - dfVreStor = DataFrame() - if !isempty(inputs["VRE_STOR"]) - if output_settings_d["WriteVREStor"] || output_settings_d["WriteNetRevenue"] - elapsed_time_vrestor = @elapsed dfVreStor = write_vre_stor(path, inputs, setup, EP) - println("Time elapsed for writing vre stor is") - println(elapsed_time_vrestor) - end - VS_LDS = inputs["VS_LDS"] - VS_STOR = inputs["VS_STOR"] - else - VS_LDS = [] - VS_STOR = [] - end - - if has_duals(EP) == 1 - if output_settings_d["WriteReliability"] - elapsed_time_reliability = @elapsed write_reliability(path, inputs, setup, EP) - println("Time elapsed for writing reliability is") - println(elapsed_time_reliability) - end - if !isempty(inputs["STOR_ALL"]) || !isempty(VS_STOR) - if output_settings_d["WriteStorageDual"] - elapsed_time_stordual = @elapsed write_storagedual(path, inputs, setup, EP) - println("Time elapsed for writing storage duals is") - println(elapsed_time_stordual) - end - end - end - - if setup["UCommit"] >= 1 - if output_settings_d["WriteCommit"] - elapsed_time_commit = @elapsed write_commit(path, inputs, setup, EP) - println("Time elapsed for writing commitment is") - println(elapsed_time_commit) - end - - if output_settings_d["WriteStart"] - elapsed_time_start = @elapsed write_start(path, inputs, setup, EP) - println("Time elapsed for writing startup is") - println(elapsed_time_start) - end - - if output_settings_d["WriteShutdown"] - elapsed_time_shutdown = @elapsed write_shutdown(path, inputs, setup, EP) - println("Time elapsed for writing shutdown is") - println(elapsed_time_shutdown) - end - - if setup["OperationalReserves"] == 1 - if output_settings_d["WriteReg"] - elapsed_time_reg = @elapsed write_reg(path, inputs, setup, EP) - println("Time elapsed for writing regulation is") - println(elapsed_time_reg) - end - - if output_settings_d["WriteRsv"] - elapsed_time_rsv = @elapsed write_rsv(path, inputs, setup, EP) - println("Time elapsed for writing reserves is") - println(elapsed_time_rsv) - end - end - end - - # Output additional variables related inter-period energy transfer via storage - representative_periods = inputs["REP_PERIOD"] - if representative_periods > 1 && (!isempty(inputs["STOR_LONG_DURATION"]) || !isempty(VS_LDS)) - if output_settings_d["WriteOpWrapLDSStorInit"] - elapsed_time_lds_init = @elapsed write_opwrap_lds_stor_init(path, inputs, setup, EP) - println("Time elapsed for writing lds init is") - println(elapsed_time_lds_init) - end - - if output_settings_d["WriteOpWrapLDSdStor"] - elapsed_time_lds_dstor = @elapsed write_opwrap_lds_dstor(path, inputs, setup, EP) - println("Time elapsed for writing lds dstor is") - println(elapsed_time_lds_dstor) - end - end - - if output_settings_d["WriteFuelConsumption"] - elapsed_time_fuel_consumption = @elapsed write_fuel_consumption(path, inputs, setup, EP) - println("Time elapsed for writing fuel consumption is") - println(elapsed_time_fuel_consumption) - end - - if output_settings_d["WriteCO2"] - elapsed_time_emissions = @elapsed write_co2(path, inputs, setup, EP) - println("Time elapsed for writing co2 is") - println(elapsed_time_emissions) - end - - if has_maintenance(inputs) && output_settings_d["WriteMaintenance"] - write_maintenance(path, inputs, EP) - end - - #Write angles when DC_OPF is activated - if setup["DC_OPF"] == 1 && output_settings_d["WriteAngles"] - elapsed_time_angles = @elapsed write_angles(path, inputs, setup, EP) - println("Time elapsed for writing angles is") - println(elapsed_time_angles) - end - - # Temporary! Suppress these outputs until we know that they are compatable with multi-stage modeling - if setup["MultiStage"] == 0 - dfEnergyRevenue = DataFrame() - dfChargingcost = DataFrame() - dfSubRevenue = DataFrame() - dfRegSubRevenue = DataFrame() - if has_duals(EP) == 1 - if output_settings_d["WritePrice"] - elapsed_time_price = @elapsed write_price(path, inputs, setup, EP) - println("Time elapsed for writing price is") - println(elapsed_time_price) - end - - if output_settings_d["WriteEnergyRevenue"] || output_settings_d["WriteNetRevenue"] - elapsed_time_energy_rev = @elapsed dfEnergyRevenue = write_energy_revenue(path, inputs, setup, EP) - println("Time elapsed for writing energy revenue is") - println(elapsed_time_energy_rev) - end - - if output_settings_d["WriteChargingCost"] || output_settings_d["WriteNetRevenue"] - elapsed_time_charging_cost = @elapsed dfChargingcost = write_charging_cost(path, inputs, setup, EP) - println("Time elapsed for writing charging cost is") - println(elapsed_time_charging_cost) - end - - if output_settings_d["WriteSubsidyRevenue"] || output_settings_d["WriteNetRevenue"] - elapsed_time_subsidy = @elapsed dfSubRevenue, dfRegSubRevenue = write_subsidy_revenue(path, inputs, setup, EP) - println("Time elapsed for writing subsidy is") - println(elapsed_time_subsidy) - end - end - - if output_settings_d["WriteTimeWeights"] - elapsed_time_time_weights = @elapsed write_time_weights(path, inputs) - println("Time elapsed for writing time weights is") - println(elapsed_time_time_weights) - end - - dfESRRev = DataFrame() - if setup["EnergyShareRequirement"] == 1 && has_duals(EP) - dfESR = DataFrame() - if output_settings_d["WriteESRPrices"] || output_settings_d["WriteESRRevenue"] || output_settings_d["WriteNetRevenue"] - elapsed_time_esr_prices = @elapsed dfESR = write_esr_prices(path, inputs, setup, EP) - println("Time elapsed for writing esr prices is") - println(elapsed_time_esr_prices) - end - - if output_settings_d["WriteESRRevenue"] || output_settings_d["WriteNetRevenue"] - elapsed_time_esr_revenue = @elapsed dfESRRev = write_esr_revenue(path, inputs, setup, dfPower, dfESR, EP) - println("Time elapsed for writing esr revenue is") - println(elapsed_time_esr_revenue) - end - - end - - dfResRevenue = DataFrame() - if setup["CapacityReserveMargin"]==1 && has_duals(EP) - if output_settings_d["WriteReserveMargin"] - elapsed_time_reserve_margin = @elapsed write_reserve_margin(path, setup, EP) - println("Time elapsed for writing reserve margin is") - println(elapsed_time_reserve_margin) - end - - if output_settings_d["WriteReserveMarginWithWeights"] - elapsed_time_rsv_margin_w = @elapsed write_reserve_margin_w(path, inputs, setup, EP) - println("Time elapsed for writing reserve margin with weights is") - println(elapsed_time_rsv_margin_w) - end - - if output_settings_d["WriteVirtualDischarge"] - elapsed_time_virtual_discharge = @elapsed write_virtual_discharge(path, inputs, setup, EP) - println("Time elapsed for writing virtual discharge is") - println(elapsed_time_virtual_discharge) - end - - if output_settings_d["WriteReserveMarginRevenue"] || output_settings_d["WriteNetRevenue"] - elapsed_time_res_rev = @elapsed dfResRevenue = write_reserve_margin_revenue(path, inputs, setup, EP) - println("Time elapsed for writing reserve revenue is") - println(elapsed_time_res_rev) - end - - if haskey(inputs, "dfCapRes_slack") && output_settings_d["WriteReserveMarginSlack"] - elapsed_time_rsv_slack = @elapsed write_reserve_margin_slack(path, inputs, setup, EP) - println("Time elapsed for writing reserve margin slack is") - println(elapsed_time_rsv_slack) - end - - if output_settings_d["WriteCapacityValue"] - elapsed_time_cap_value = @elapsed write_capacity_value(path, inputs, setup, EP) - println("Time elapsed for writing capacity value is") - println(elapsed_time_cap_value) - end - - end - - dfOpRegRevenue = DataFrame() - dfOpRsvRevenue = DataFrame() - if setup["OperationalReserves"]==1 && has_duals(EP) - elapsed_time_op_res_rev = @elapsed dfOpRegRevenue, dfOpRsvRevenue = write_operating_reserve_regulation_revenue(path, inputs, setup, EP) - println("Time elapsed for writing oerating reserve and regulation revenue is") - println(elapsed_time_op_res_rev) - end - - if setup["CO2Cap"]>0 && has_duals(EP) == 1 && output_settings_d["WriteCO2Cap"] - elapsed_time_co2_cap = @elapsed write_co2_cap(path, inputs, setup, EP) - println("Time elapsed for writing co2 cap is") - println(elapsed_time_co2_cap) - end - if setup["MinCapReq"] == 1 && has_duals(EP) == 1 && output_settings_d["WriteMinCapReq"] - elapsed_time_min_cap_req = @elapsed write_minimum_capacity_requirement(path, inputs, setup, EP) - println("Time elapsed for writing minimum capacity requirement is") - println(elapsed_time_min_cap_req) - end - - if setup["MaxCapReq"] == 1 && has_duals(EP) == 1 && output_settings_d["WriteMaxCapReq"] - elapsed_time_max_cap_req = @elapsed write_maximum_capacity_requirement(path, inputs, setup, EP) - println("Time elapsed for writing maximum capacity requirement is") - println(elapsed_time_max_cap_req) - end - - if !isempty(inputs["ELECTROLYZER"]) && has_duals(EP) - if output_settings_d["WriteHydrogenPrices"] - elapsed_time_hydrogen_prices = @elapsed write_hydrogen_prices(path, inputs, setup, EP) - println("Time elapsed for writing hydrogen prices is") - println(elapsed_time_hydrogen_prices) - end - if setup["HydrogenHourlyMatching"] == 1 && output_settings_d["WriteHourlyMatchingPrices"] - elapsed_time_hourly_matching_prices = @elapsed write_hourly_matching_prices(path, inputs, setup, EP) - println("Time elapsed for writing hourly matching prices is") - println(elapsed_time_hourly_matching_prices) - end - end - - if output_settings_d["WriteNetRevenue"] - elapsed_time_net_rev = @elapsed write_net_revenue(path, inputs, setup, EP, dfCap, dfESRRev, dfResRevenue, dfChargingcost, dfPower, dfEnergyRevenue, dfSubRevenue, dfRegSubRevenue, dfVreStor, dfOpRegRevenue, dfOpRsvRevenue) - println("Time elapsed for writing net revenue is") - println(elapsed_time_net_rev) - end - end - ## Print confirmation - println("Wrote outputs to $path") - - return path + if setup["OverwriteResults"] == 1 + # Overwrite existing results if dir exists + # This is the default behaviour when there is no flag, to avoid breaking existing code + if !(isdir(path)) + mkpath(path) + end + else + # Find closest unused ouput directory name and create it + path = choose_output_dir(path) + mkpath(path) + end + + # https://jump.dev/MathOptInterface.jl/v0.9.10/apireference/#MathOptInterface.TerminationStatusCode + status = termination_status(EP) + + ## Check if solved sucessfully - time out is included + if status != MOI.OPTIMAL && status != MOI.LOCALLY_SOLVED + if status != MOI.TIME_LIMIT # Model failed to solve, so record solver status and exit + write_status(path, inputs, setup, EP) + return + # Model reached timelimit but failed to find a feasible solution + #### Aaron Schwartz - Not sure if the below condition is valid anymore. We should revisit #### + elseif isnan(objective_value(EP)) == true + # Model failed to solve, so record solver status and exit + write_status(path, inputs, setup, EP) + return + end + end + + # Dict containing the list of outputs to write + output_settings_d = setup["WriteOutputsSettingsDict"] + write_settings_file(path, setup) + + output_settings_d["WriteStatus"] && write_status(path, inputs, setup, EP) + + # linearize and re-solve model if duals are not available but ShadowPrices are requested + if !has_duals(EP) && setup["WriteShadowPrices"] == 1 + # function to fix integers and linearize problem + fix_integers(EP) + # re-solve statement for LP solution + println("Solving LP solution for duals") + set_silent(EP) + optimize!(EP) + end + + if output_settings_d["WriteCosts"] + elapsed_time_costs = @elapsed write_costs(path, inputs, setup, EP) + println("Time elapsed for writing costs is") + println(elapsed_time_costs) + end + + if output_settings_d["WriteCapacity"] || output_settings_d["WriteNetRevenue"] + elapsed_time_capacity = @elapsed dfCap = write_capacity(path, inputs, setup, EP) + println("Time elapsed for writing capacity is") + println(elapsed_time_capacity) + end + + if output_settings_d["WritePower"] || output_settings_d["WriteNetRevenue"] + elapsed_time_power = @elapsed dfPower = write_power(path, inputs, setup, EP) + println("Time elapsed for writing power is") + println(elapsed_time_power) + end + + if output_settings_d["WriteCharge"] + elapsed_time_charge = @elapsed write_charge(path, inputs, setup, EP) + println("Time elapsed for writing charge is") + println(elapsed_time_charge) + end + + if output_settings_d["WriteCapacityFactor"] + elapsed_time_capacityfactor = @elapsed write_capacityfactor(path, inputs, setup, EP) + println("Time elapsed for writing capacity factor is") + println(elapsed_time_capacityfactor) + end + + if output_settings_d["WriteStorage"] + elapsed_time_storage = @elapsed write_storage(path, inputs, setup, EP) + println("Time elapsed for writing storage is") + println(elapsed_time_storage) + end + + if output_settings_d["WriteCurtailment"] + elapsed_time_curtailment = @elapsed write_curtailment(path, inputs, setup, EP) + println("Time elapsed for writing curtailment is") + println(elapsed_time_curtailment) + end + + if output_settings_d["WriteNSE"] + elapsed_time_nse = @elapsed write_nse(path, inputs, setup, EP) + println("Time elapsed for writing nse is") + println(elapsed_time_nse) + end + + if output_settings_d["WritePowerBalance"] + elapsed_time_power_balance = @elapsed write_power_balance(path, inputs, setup, EP) + println("Time elapsed for writing power balance is") + println(elapsed_time_power_balance) + end + + if inputs["Z"] > 1 + if output_settings_d["WriteTransmissionFlows"] + elapsed_time_flows = @elapsed write_transmission_flows(path, inputs, setup, EP) + println("Time elapsed for writing transmission flows is") + println(elapsed_time_flows) + end + + if output_settings_d["WriteTransmissionLosses"] + elapsed_time_losses = @elapsed write_transmission_losses(path, + inputs, + setup, + EP) + println("Time elapsed for writing transmission losses is") + println(elapsed_time_losses) + end + + if setup["NetworkExpansion"] == 1 && output_settings_d["WriteNWExpansion"] + elapsed_time_expansion = @elapsed write_nw_expansion(path, inputs, setup, EP) + println("Time elapsed for writing network expansion is") + println(elapsed_time_expansion) + end + end + + if output_settings_d["WriteEmissions"] + elapsed_time_emissions = @elapsed write_emissions(path, inputs, setup, EP) + println("Time elapsed for writing emissions is") + println(elapsed_time_emissions) + end + + dfVreStor = DataFrame() + if !isempty(inputs["VRE_STOR"]) + if output_settings_d["WriteVREStor"] || output_settings_d["WriteNetRevenue"] + elapsed_time_vrestor = @elapsed dfVreStor = write_vre_stor(path, + inputs, + setup, + EP) + println("Time elapsed for writing vre stor is") + println(elapsed_time_vrestor) + end + VS_LDS = inputs["VS_LDS"] + VS_STOR = inputs["VS_STOR"] + else + VS_LDS = [] + VS_STOR = [] + end + + if has_duals(EP) == 1 + if output_settings_d["WriteReliability"] + elapsed_time_reliability = @elapsed write_reliability(path, inputs, setup, EP) + println("Time elapsed for writing reliability is") + println(elapsed_time_reliability) + end + if !isempty(inputs["STOR_ALL"]) || !isempty(VS_STOR) + if output_settings_d["WriteStorageDual"] + elapsed_time_stordual = @elapsed write_storagedual(path, inputs, setup, EP) + println("Time elapsed for writing storage duals is") + println(elapsed_time_stordual) + end + end + end + + if setup["UCommit"] >= 1 + if output_settings_d["WriteCommit"] + elapsed_time_commit = @elapsed write_commit(path, inputs, setup, EP) + println("Time elapsed for writing commitment is") + println(elapsed_time_commit) + end + + if output_settings_d["WriteStart"] + elapsed_time_start = @elapsed write_start(path, inputs, setup, EP) + println("Time elapsed for writing startup is") + println(elapsed_time_start) + end + + if output_settings_d["WriteShutdown"] + elapsed_time_shutdown = @elapsed write_shutdown(path, inputs, setup, EP) + println("Time elapsed for writing shutdown is") + println(elapsed_time_shutdown) + end + + if setup["OperationalReserves"] == 1 + if output_settings_d["WriteReg"] + elapsed_time_reg = @elapsed write_reg(path, inputs, setup, EP) + println("Time elapsed for writing regulation is") + println(elapsed_time_reg) + end + + if output_settings_d["WriteRsv"] + elapsed_time_rsv = @elapsed write_rsv(path, inputs, setup, EP) + println("Time elapsed for writing reserves is") + println(elapsed_time_rsv) + end + end + end + + # Output additional variables related inter-period energy transfer via storage + representative_periods = inputs["REP_PERIOD"] + if representative_periods > 1 && + (!isempty(inputs["STOR_LONG_DURATION"]) || !isempty(VS_LDS)) + if output_settings_d["WriteOpWrapLDSStorInit"] + elapsed_time_lds_init = @elapsed write_opwrap_lds_stor_init(path, + inputs, + setup, + EP) + println("Time elapsed for writing lds init is") + println(elapsed_time_lds_init) + end + + if output_settings_d["WriteOpWrapLDSdStor"] + elapsed_time_lds_dstor = @elapsed write_opwrap_lds_dstor(path, + inputs, + setup, + EP) + println("Time elapsed for writing lds dstor is") + println(elapsed_time_lds_dstor) + end + end + + if output_settings_d["WriteFuelConsumption"] + elapsed_time_fuel_consumption = @elapsed write_fuel_consumption(path, + inputs, + setup, + EP) + println("Time elapsed for writing fuel consumption is") + println(elapsed_time_fuel_consumption) + end + + if output_settings_d["WriteCO2"] + elapsed_time_emissions = @elapsed write_co2(path, inputs, setup, EP) + println("Time elapsed for writing co2 is") + println(elapsed_time_emissions) + end + + if has_maintenance(inputs) && output_settings_d["WriteMaintenance"] + write_maintenance(path, inputs, EP) + end + + #Write angles when DC_OPF is activated + if setup["DC_OPF"] == 1 && output_settings_d["WriteAngles"] + elapsed_time_angles = @elapsed write_angles(path, inputs, setup, EP) + println("Time elapsed for writing angles is") + println(elapsed_time_angles) + end + + # Temporary! Suppress these outputs until we know that they are compatable with multi-stage modeling + if setup["MultiStage"] == 0 + dfEnergyRevenue = DataFrame() + dfChargingcost = DataFrame() + dfSubRevenue = DataFrame() + dfRegSubRevenue = DataFrame() + if has_duals(EP) == 1 + if output_settings_d["WritePrice"] + elapsed_time_price = @elapsed write_price(path, inputs, setup, EP) + println("Time elapsed for writing price is") + println(elapsed_time_price) + end + + if output_settings_d["WriteEnergyRevenue"] || + output_settings_d["WriteNetRevenue"] + elapsed_time_energy_rev = @elapsed dfEnergyRevenue = write_energy_revenue(path, + inputs, + setup, + EP) + println("Time elapsed for writing energy revenue is") + println(elapsed_time_energy_rev) + end + + if output_settings_d["WriteChargingCost"] || + output_settings_d["WriteNetRevenue"] + elapsed_time_charging_cost = @elapsed dfChargingcost = write_charging_cost(path, + inputs, + setup, + EP) + println("Time elapsed for writing charging cost is") + println(elapsed_time_charging_cost) + end + + if output_settings_d["WriteSubsidyRevenue"] || + output_settings_d["WriteNetRevenue"] + elapsed_time_subsidy = @elapsed dfSubRevenue, dfRegSubRevenue = write_subsidy_revenue(path, + inputs, + setup, + EP) + println("Time elapsed for writing subsidy is") + println(elapsed_time_subsidy) + end + end + + if output_settings_d["WriteTimeWeights"] + elapsed_time_time_weights = @elapsed write_time_weights(path, inputs) + println("Time elapsed for writing time weights is") + println(elapsed_time_time_weights) + end + + dfESRRev = DataFrame() + if setup["EnergyShareRequirement"] == 1 && has_duals(EP) + dfESR = DataFrame() + if output_settings_d["WriteESRPrices"] || + output_settings_d["WriteESRRevenue"] || output_settings_d["WriteNetRevenue"] + elapsed_time_esr_prices = @elapsed dfESR = write_esr_prices(path, + inputs, + setup, + EP) + println("Time elapsed for writing esr prices is") + println(elapsed_time_esr_prices) + end + + if output_settings_d["WriteESRRevenue"] || output_settings_d["WriteNetRevenue"] + elapsed_time_esr_revenue = @elapsed dfESRRev = write_esr_revenue(path, + inputs, + setup, + dfPower, + dfESR, + EP) + println("Time elapsed for writing esr revenue is") + println(elapsed_time_esr_revenue) + end + end + + dfResRevenue = DataFrame() + if setup["CapacityReserveMargin"] == 1 && has_duals(EP) + if output_settings_d["WriteReserveMargin"] + elapsed_time_reserve_margin = @elapsed write_reserve_margin(path, setup, EP) + println("Time elapsed for writing reserve margin is") + println(elapsed_time_reserve_margin) + end + + if output_settings_d["WriteReserveMarginWithWeights"] + elapsed_time_rsv_margin_w = @elapsed write_reserve_margin_w(path, + inputs, + setup, + EP) + println("Time elapsed for writing reserve margin with weights is") + println(elapsed_time_rsv_margin_w) + end + + if output_settings_d["WriteVirtualDischarge"] + elapsed_time_virtual_discharge = @elapsed write_virtual_discharge(path, + inputs, + setup, + EP) + println("Time elapsed for writing virtual discharge is") + println(elapsed_time_virtual_discharge) + end + + if output_settings_d["WriteReserveMarginRevenue"] || + output_settings_d["WriteNetRevenue"] + elapsed_time_res_rev = @elapsed dfResRevenue = write_reserve_margin_revenue(path, + inputs, + setup, + EP) + println("Time elapsed for writing reserve revenue is") + println(elapsed_time_res_rev) + end + + if haskey(inputs, "dfCapRes_slack") && + output_settings_d["WriteReserveMarginSlack"] + elapsed_time_rsv_slack = @elapsed write_reserve_margin_slack(path, + inputs, + setup, + EP) + println("Time elapsed for writing reserve margin slack is") + println(elapsed_time_rsv_slack) + end + + if output_settings_d["WriteCapacityValue"] + elapsed_time_cap_value = @elapsed write_capacity_value(path, + inputs, + setup, + EP) + println("Time elapsed for writing capacity value is") + println(elapsed_time_cap_value) + end + end + + dfOpRegRevenue = DataFrame() + dfOpRsvRevenue = DataFrame() + if setup["OperationalReserves"] == 1 && has_duals(EP) + elapsed_time_op_res_rev = @elapsed dfOpRegRevenue, dfOpRsvRevenue = write_operating_reserve_regulation_revenue(path, + inputs, + setup, + EP) + println("Time elapsed for writing oerating reserve and regulation revenue is") + println(elapsed_time_op_res_rev) + end + + if setup["CO2Cap"] > 0 && has_duals(EP) == 1 && output_settings_d["WriteCO2Cap"] + elapsed_time_co2_cap = @elapsed write_co2_cap(path, inputs, setup, EP) + println("Time elapsed for writing co2 cap is") + println(elapsed_time_co2_cap) + end + if setup["MinCapReq"] == 1 && has_duals(EP) == 1 && + output_settings_d["WriteMinCapReq"] + elapsed_time_min_cap_req = @elapsed write_minimum_capacity_requirement(path, + inputs, + setup, + EP) + println("Time elapsed for writing minimum capacity requirement is") + println(elapsed_time_min_cap_req) + end + + if setup["MaxCapReq"] == 1 && has_duals(EP) == 1 && + output_settings_d["WriteMaxCapReq"] + elapsed_time_max_cap_req = @elapsed write_maximum_capacity_requirement(path, + inputs, + setup, + EP) + println("Time elapsed for writing maximum capacity requirement is") + println(elapsed_time_max_cap_req) + end + + if !isempty(inputs["ELECTROLYZER"]) && has_duals(EP) + if output_settings_d["WriteHydrogenPrices"] + elapsed_time_hydrogen_prices = @elapsed write_hydrogen_prices(path, + inputs, + setup, + EP) + println("Time elapsed for writing hydrogen prices is") + println(elapsed_time_hydrogen_prices) + end + if setup["HydrogenHourlyMatching"] == 1 && + output_settings_d["WriteHourlyMatchingPrices"] + elapsed_time_hourly_matching_prices = @elapsed write_hourly_matching_prices(path, + inputs, + setup, + EP) + println("Time elapsed for writing hourly matching prices is") + println(elapsed_time_hourly_matching_prices) + end + end + + if output_settings_d["WriteNetRevenue"] + elapsed_time_net_rev = @elapsed write_net_revenue(path, + inputs, + setup, + EP, + dfCap, + dfESRRev, + dfResRevenue, + dfChargingcost, + dfPower, + dfEnergyRevenue, + dfSubRevenue, + dfRegSubRevenue, + dfVreStor, + dfOpRegRevenue, + dfOpRsvRevenue) + println("Time elapsed for writing net revenue is") + println(elapsed_time_net_rev) + end + end + ## Print confirmation + println("Wrote outputs to $path") + + return path end # END output() - """ write_annual(fullpath::AbstractString, dfOut::DataFrame) Internal function for writing annual outputs. """ function write_annual(fullpath::AbstractString, dfOut::DataFrame) - push!(dfOut, ["Total" 0 sum(dfOut[!, :AnnualSum])]) - CSV.write(fullpath, dfOut) - return nothing + push!(dfOut, ["Total" 0 sum(dfOut[!, :AnnualSum])]) + CSV.write(fullpath, dfOut) + return nothing end """ @@ -401,16 +483,23 @@ end Internal function for writing full time series outputs. This function wraps the instructions for creating the full time series output files. """ -function write_fulltimeseries(fullpath::AbstractString, dataOut::Matrix{Float64}, dfOut::DataFrame) - T = size(dataOut, 2) - dfOut = hcat(dfOut, DataFrame(dataOut, :auto)) - auxNew_Names = [Symbol("Resource");Symbol("Zone");Symbol("AnnualSum");[Symbol("t$t") for t in 1:T]] - rename!(dfOut, auxNew_Names) - total = DataFrame(["Total" 0 sum(dfOut[!, :AnnualSum]) fill(0.0, (1, T))], auxNew_Names) - total[!, 4:T+3] .= sum(dataOut, dims=1) - dfOut = vcat(dfOut, total) - CSV.write(fullpath, dftranspose(dfOut, false), writeheader=false) - return nothing +function write_fulltimeseries(fullpath::AbstractString, + dataOut::Matrix{Float64}, + dfOut::DataFrame) + T = size(dataOut, 2) + dfOut = hcat(dfOut, DataFrame(dataOut, :auto)) + auxNew_Names = [Symbol("Resource"); + Symbol("Zone"); + Symbol("AnnualSum"); + [Symbol("t$t") for t in 1:T]] + rename!(dfOut, auxNew_Names) + total = DataFrame(["Total" 0 sum(dfOut[!, :AnnualSum]) fill(0.0, (1, T))], auxNew_Names) + total[!, 4:(T + 3)] .= sum(dataOut, dims = 1) + dfOut = vcat(dfOut, total) + CSV.write(fullpath, dftranspose(dfOut, false), writeheader = false) + return nothing end -write_settings_file(path, setup) = YAML.write_file(joinpath(path, "run_settings.yml"), setup) +function write_settings_file(path, setup) + YAML.write_file(joinpath(path, "run_settings.yml"), setup) +end diff --git a/src/write_outputs/write_power.jl b/src/write_outputs/write_power.jl index 3be5e83bf3..30e14048be 100644 --- a/src/write_outputs/write_power.jl +++ b/src/write_outputs/write_power.jl @@ -4,26 +4,28 @@ Function for writing the different values of power generated by the different technologies in operation. """ function write_power(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - gen = inputs["RESOURCES"] - zones = zone_id.(gen) + gen = inputs["RESOURCES"] + zones = zone_id.(gen) - G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) - T = inputs["T"] # Number of time steps (hours) + G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) + T = inputs["T"] # Number of time steps (hours) - # Power injected by each resource in each time step - dfPower = DataFrame(Resource = inputs["RESOURCE_NAMES"], Zone = zones, AnnualSum = Array{Union{Missing,Float64}}(undef, G)) - power = value.(EP[:vP]) - if setup["ParameterScale"] == 1 - power *= ModelScalingFactor - end - dfPower.AnnualSum .= power * inputs["omega"] + # Power injected by each resource in each time step + dfPower = DataFrame(Resource = inputs["RESOURCE_NAMES"], + Zone = zones, + AnnualSum = Array{Union{Missing, Float64}}(undef, G)) + power = value.(EP[:vP]) + if setup["ParameterScale"] == 1 + power *= ModelScalingFactor + end + dfPower.AnnualSum .= power * inputs["omega"] - filepath = joinpath(path, "power.csv") - if setup["WriteOutputs"] == "annual" - write_annual(filepath, dfPower) - else # setup["WriteOutputs"] == "full" - write_fulltimeseries(filepath, power, dfPower) - end + filepath = joinpath(path, "power.csv") + if setup["WriteOutputs"] == "annual" + write_annual(filepath, dfPower) + else # setup["WriteOutputs"] == "full" + write_fulltimeseries(filepath, power, dfPower) + end - return dfPower #Shouldn't this be return nothing + return dfPower #Shouldn't this be return nothing end diff --git a/src/write_outputs/write_power_balance.jl b/src/write_outputs/write_power_balance.jl index 627f3a4821..4661fb3faa 100644 --- a/src/write_outputs/write_power_balance.jl +++ b/src/write_outputs/write_power_balance.jl @@ -1,71 +1,94 @@ function write_power_balance(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - gen = inputs["RESOURCES"] - T = inputs["T"] # Number of time steps (hours) - Z = inputs["Z"] # Number of zones - SEG = inputs["SEG"] # Number of demand curtailment segments - THERM_ALL = inputs["THERM_ALL"] - VRE = inputs["VRE"] - MUST_RUN = inputs["MUST_RUN"] - HYDRO_RES = inputs["HYDRO_RES"] - STOR_ALL = inputs["STOR_ALL"] - FLEX = inputs["FLEX"] - ELECTROLYZER = inputs["ELECTROLYZER"] - VRE_STOR = inputs["VRE_STOR"] - Com_list = ["Generation", "Storage_Discharge", "Storage_Charge", - "Flexible_Demand_Defer", "Flexible_Demand_Stasify", - "Demand_Response", "Nonserved_Energy", - "Transmission_NetExport", "Transmission_Losses", - "Demand"] - if !isempty(ELECTROLYZER) - push!(Com_list, "Electrolyzer_Consumption") - end - L = length(Com_list) - dfPowerBalance = DataFrame(BalanceComponent = repeat(Com_list, outer = Z), Zone = repeat(1:Z, inner = L), AnnualSum = zeros(L * Z)) - powerbalance = zeros(Z * L, T) # following the same style of power/charge/storage/nse - for z in 1:Z - POWER_ZONE = intersect(resources_in_zone_by_rid(gen,z), union(THERM_ALL, VRE, MUST_RUN, HYDRO_RES)) - powerbalance[(z-1)*L+1, :] = sum(value.(EP[:vP][POWER_ZONE, :]), dims = 1) - if !isempty(intersect(resources_in_zone_by_rid(gen,z), STOR_ALL)) - STOR_ALL_ZONE = intersect(resources_in_zone_by_rid(gen,z), STOR_ALL) - powerbalance[(z-1)*L+2, :] = sum(value.(EP[:vP][STOR_ALL_ZONE, :]), dims = 1) - powerbalance[(z-1)*L+3, :] = (-1) * sum((value.(EP[:vCHARGE][STOR_ALL_ZONE, :]).data), dims = 1) - end - if !isempty(intersect(resources_in_zone_by_rid(gen,z), VRE_STOR)) - VS_ALL_ZONE = intersect(resources_in_zone_by_rid(gen,z), inputs["VS_STOR"]) - powerbalance[(z-1)*L+2, :] = sum(value.(EP[:vP][VS_ALL_ZONE, :]), dims = 1) - powerbalance[(z-1)*L+3, :] = (-1) * sum(value.(EP[:vCHARGE_VRE_STOR][VS_ALL_ZONE, :]).data, dims=1) - end - if !isempty(intersect(resources_in_zone_by_rid(gen,z), FLEX)) - FLEX_ZONE = intersect(resources_in_zone_by_rid(gen,z), FLEX) - powerbalance[(z-1)*L+4, :] = sum((value.(EP[:vCHARGE_FLEX][FLEX_ZONE, :]).data), dims = 1) - powerbalance[(z-1)*L+5, :] = (-1) * sum(value.(EP[:vP][FLEX_ZONE, :]), dims = 1) - end - if SEG > 1 - powerbalance[(z-1)*L+6, :] = sum(value.(EP[:vNSE][2:SEG, :, z]), dims = 1) - end - powerbalance[(z-1)*L+7, :] = value.(EP[:vNSE][1, :, z]) - if Z >= 2 - powerbalance[(z-1)*L+8, :] = (value.(EP[:ePowerBalanceNetExportFlows][:, z]))' # Transpose - powerbalance[(z-1)*L+9, :] = -(value.(EP[:eLosses_By_Zone][z, :])) - end - powerbalance[(z-1)*L+10, :] = (((-1) * inputs["pD"][:, z]))' # Transpose - if !isempty(ELECTROLYZER) - ELECTROLYZER_ZONE = intersect(resources_in_zone_by_rid(gen,z), ELECTROLYZER) - powerbalance[(z-1)*L+11, :] = (-1) * sum(value.(EP[:vUSE][ELECTROLYZER_ZONE, :].data), dims = 1) - end - end - if setup["ParameterScale"] == 1 - powerbalance *= ModelScalingFactor - end - dfPowerBalance.AnnualSum .= powerbalance * inputs["omega"] + gen = inputs["RESOURCES"] + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones + SEG = inputs["SEG"] # Number of demand curtailment segments + THERM_ALL = inputs["THERM_ALL"] + VRE = inputs["VRE"] + MUST_RUN = inputs["MUST_RUN"] + HYDRO_RES = inputs["HYDRO_RES"] + STOR_ALL = inputs["STOR_ALL"] + FLEX = inputs["FLEX"] + ELECTROLYZER = inputs["ELECTROLYZER"] + VRE_STOR = inputs["VRE_STOR"] + Com_list = ["Generation", "Storage_Discharge", "Storage_Charge", + "Flexible_Demand_Defer", "Flexible_Demand_Stasify", + "Demand_Response", "Nonserved_Energy", + "Transmission_NetExport", "Transmission_Losses", + "Demand"] + if !isempty(ELECTROLYZER) + push!(Com_list, "Electrolyzer_Consumption") + end + L = length(Com_list) + dfPowerBalance = DataFrame(BalanceComponent = repeat(Com_list, outer = Z), + Zone = repeat(1:Z, inner = L), + AnnualSum = zeros(L * Z)) + powerbalance = zeros(Z * L, T) # following the same style of power/charge/storage/nse + for z in 1:Z + POWER_ZONE = intersect(resources_in_zone_by_rid(gen, z), + union(THERM_ALL, VRE, MUST_RUN, HYDRO_RES)) + powerbalance[(z - 1) * L + 1, :] = sum(value.(EP[:vP][POWER_ZONE, :]), dims = 1) + if !isempty(intersect(resources_in_zone_by_rid(gen, z), STOR_ALL)) + STOR_ALL_ZONE = intersect(resources_in_zone_by_rid(gen, z), STOR_ALL) + powerbalance[(z - 1) * L + 2, :] = sum(value.(EP[:vP][STOR_ALL_ZONE, :]), + dims = 1) + powerbalance[(z - 1) * L + 3, :] = (-1) * + sum((value.(EP[:vCHARGE][STOR_ALL_ZONE, + :]).data), + dims = 1) + end + if !isempty(intersect(resources_in_zone_by_rid(gen, z), VRE_STOR)) + VS_ALL_ZONE = intersect(resources_in_zone_by_rid(gen, z), inputs["VS_STOR"]) + powerbalance[(z - 1) * L + 2, :] = sum(value.(EP[:vP][VS_ALL_ZONE, :]), + dims = 1) + powerbalance[(z - 1) * L + 3, :] = (-1) * + sum(value.(EP[:vCHARGE_VRE_STOR][VS_ALL_ZONE, + :]).data, + dims = 1) + end + if !isempty(intersect(resources_in_zone_by_rid(gen, z), FLEX)) + FLEX_ZONE = intersect(resources_in_zone_by_rid(gen, z), FLEX) + powerbalance[(z - 1) * L + 4, :] = sum((value.(EP[:vCHARGE_FLEX][FLEX_ZONE, + :]).data), + dims = 1) + powerbalance[(z - 1) * L + 5, :] = (-1) * + sum(value.(EP[:vP][FLEX_ZONE, :]), dims = 1) + end + if SEG > 1 + powerbalance[(z - 1) * L + 6, :] = sum(value.(EP[:vNSE][2:SEG, :, z]), dims = 1) + end + powerbalance[(z - 1) * L + 7, :] = value.(EP[:vNSE][1, :, z]) + if Z >= 2 + powerbalance[(z - 1) * L + 8, :] = (value.(EP[:ePowerBalanceNetExportFlows][:, + z]))' # Transpose + powerbalance[(z - 1) * L + 9, :] = -(value.(EP[:eLosses_By_Zone][z, :])) + end + powerbalance[(z - 1) * L + 10, :] = (((-1) * inputs["pD"][:, z]))' # Transpose + if !isempty(ELECTROLYZER) + ELECTROLYZER_ZONE = intersect(resources_in_zone_by_rid(gen, z), ELECTROLYZER) + powerbalance[(z - 1) * L + 11, :] = (-1) * + sum(value.(EP[:vUSE][ELECTROLYZER_ZONE, + :].data), + dims = 1) + end + end + if setup["ParameterScale"] == 1 + powerbalance *= ModelScalingFactor + end + dfPowerBalance.AnnualSum .= powerbalance * inputs["omega"] - if setup["WriteOutputs"] == "annual" - CSV.write(joinpath(path, "power_balance.csv"), dfPowerBalance) - else # setup["WriteOutputs"] == "full" - dfPowerBalance = hcat(dfPowerBalance, DataFrame(powerbalance, :auto)) - auxNew_Names = [Symbol("BalanceComponent"); Symbol("Zone"); Symbol("AnnualSum"); [Symbol("t$t") for t in 1:T]] - rename!(dfPowerBalance,auxNew_Names) - CSV.write(joinpath(path, "power_balance.csv"), dftranspose(dfPowerBalance, false), writeheader=false) - end - return nothing + if setup["WriteOutputs"] == "annual" + CSV.write(joinpath(path, "power_balance.csv"), dfPowerBalance) + else # setup["WriteOutputs"] == "full" + dfPowerBalance = hcat(dfPowerBalance, DataFrame(powerbalance, :auto)) + auxNew_Names = [Symbol("BalanceComponent"); + Symbol("Zone"); + Symbol("AnnualSum"); + [Symbol("t$t") for t in 1:T]] + rename!(dfPowerBalance, auxNew_Names) + CSV.write(joinpath(path, "power_balance.csv"), + dftranspose(dfPowerBalance, false), + writeheader = false) + end + return nothing end diff --git a/src/write_outputs/write_price.jl b/src/write_outputs/write_price.jl index 7d240a0f02..05943c8fa8 100644 --- a/src/write_outputs/write_price.jl +++ b/src/write_outputs/write_price.jl @@ -4,22 +4,24 @@ Function for reporting marginal electricity price for each model zone and time step. Marginal electricity price is equal to the dual variable of the power balance constraint. If GenX is configured as a mixed integer linear program, then this output is only generated if `WriteShadowPrices` flag is activated. If configured as a linear program (i.e. linearized unit commitment or economic dispatch) then output automatically available. """ function write_price(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - T = inputs["T"] # Number of time steps (hours) - Z = inputs["Z"] # Number of zones + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones - ## Extract dual variables of constraints - # Electricity price: Dual variable of hourly power balance constraint = hourly price - dfPrice = DataFrame(Zone = 1:Z) # The unit is $/MWh - # Dividing dual variable for each hour with corresponding hourly weight to retrieve marginal cost of generation + ## Extract dual variables of constraints + # Electricity price: Dual variable of hourly power balance constraint = hourly price + dfPrice = DataFrame(Zone = 1:Z) # The unit is $/MWh + # Dividing dual variable for each hour with corresponding hourly weight to retrieve marginal cost of generation price = locational_marginal_price(EP, inputs, setup) - dfPrice = hcat(dfPrice, DataFrame(transpose(price), :auto)) + dfPrice = hcat(dfPrice, DataFrame(transpose(price), :auto)) - auxNew_Names=[Symbol("Zone");[Symbol("t$t") for t in 1:T]] - rename!(dfPrice,auxNew_Names) + auxNew_Names = [Symbol("Zone"); [Symbol("t$t") for t in 1:T]] + rename!(dfPrice, auxNew_Names) - ## Linear configuration final output - CSV.write(joinpath(path, "prices.csv"), dftranspose(dfPrice, false), writeheader=false) - return nothing + ## Linear configuration final output + CSV.write(joinpath(path, "prices.csv"), + dftranspose(dfPrice, false), + writeheader = false) + return nothing end @doc raw""" diff --git a/src/write_outputs/write_reliability.jl b/src/write_outputs/write_reliability.jl index ce5cd34efd..876465f8b7 100644 --- a/src/write_outputs/write_reliability.jl +++ b/src/write_outputs/write_reliability.jl @@ -4,18 +4,20 @@ Function for reporting dual variable of maximum non-served energy constraint (shadow price of reliability constraint) for each model zone and time step. """ function write_reliability(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - T = inputs["T"] # Number of time steps (hours) - Z = inputs["Z"] # Number of zones + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones - # reliability: Dual variable of maximum NSE constraint = shadow value of reliability constraint - dfReliability = DataFrame(Zone = 1:Z) - # Dividing dual variable for each hour with corresponding hourly weight to retrieve marginal cost of generation - scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 - dfReliability = hcat(dfReliability, DataFrame(transpose(dual.(EP[:cMaxNSE])./inputs["omega"]*scale_factor), :auto)) + # reliability: Dual variable of maximum NSE constraint = shadow value of reliability constraint + dfReliability = DataFrame(Zone = 1:Z) + # Dividing dual variable for each hour with corresponding hourly weight to retrieve marginal cost of generation + scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 + dfReliability = hcat(dfReliability, + DataFrame(transpose(dual.(EP[:cMaxNSE]) ./ inputs["omega"] * scale_factor), :auto)) - auxNew_Names=[Symbol("Zone");[Symbol("t$t") for t in 1:T]] - rename!(dfReliability,auxNew_Names) - - CSV.write(joinpath(path, "reliability.csv"), dftranspose(dfReliability, false), header=false) + auxNew_Names = [Symbol("Zone"); [Symbol("t$t") for t in 1:T]] + rename!(dfReliability, auxNew_Names) + CSV.write(joinpath(path, "reliability.csv"), + dftranspose(dfReliability, false), + header = false) end diff --git a/src/write_outputs/write_status.jl b/src/write_outputs/write_status.jl index 32b6eee760..8558a21a50 100644 --- a/src/write_outputs/write_status.jl +++ b/src/write_outputs/write_status.jl @@ -5,16 +5,17 @@ Function for writing the final solve status of the optimization problem solved. """ function write_status(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - # https://jump.dev/MathOptInterface.jl/v0.9.10/apireference/#MathOptInterface.TerminationStatusCode - status = termination_status(EP) + # https://jump.dev/MathOptInterface.jl/v0.9.10/apireference/#MathOptInterface.TerminationStatusCode + status = termination_status(EP) - # Note: Gurobi excludes constants from solver reported objective function value - MIPGap calculated may be erroneous - if (setup["UCommit"] == 0 || setup["UCommit"] == 2) - dfStatus = DataFrame(Status = status, Solve = inputs["solve_time"], - Objval = objective_value(EP)) - else - dfStatus = DataFrame(Status = status, Solve = inputs["solve_time"], - Objval = objective_value(EP), Objbound= objective_bound(EP),FinalMIPGap =(objective_value(EP) -objective_bound(EP))/objective_value(EP) ) - end - CSV.write(joinpath(path, "status.csv"),dfStatus) + # Note: Gurobi excludes constants from solver reported objective function value - MIPGap calculated may be erroneous + if (setup["UCommit"] == 0 || setup["UCommit"] == 2) + dfStatus = DataFrame(Status = status, Solve = inputs["solve_time"], + Objval = objective_value(EP)) + else + dfStatus = DataFrame(Status = status, Solve = inputs["solve_time"], + Objval = objective_value(EP), Objbound = objective_bound(EP), + FinalMIPGap = (objective_value(EP) - objective_bound(EP)) / objective_value(EP)) + end + CSV.write(joinpath(path, "status.csv"), dfStatus) end diff --git a/src/write_outputs/write_storage.jl b/src/write_outputs/write_storage.jl index b8d2167dba..a34c470108 100644 --- a/src/write_outputs/write_storage.jl +++ b/src/write_outputs/write_storage.jl @@ -3,40 +3,40 @@ Function for writing the capacities of different storage technologies, including hydro reservoir, flexible storage tech etc. """ -function write_storage(path::AbstractString, inputs::Dict,setup::Dict, EP::Model) - gen = inputs["RESOURCES"] - zones = zone_id.(gen) +function write_storage(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) + gen = inputs["RESOURCES"] + zones = zone_id.(gen) - T = inputs["T"] # Number of time steps (hours) - G = inputs["G"] - STOR_ALL = inputs["STOR_ALL"] - HYDRO_RES = inputs["HYDRO_RES"] - FLEX = inputs["FLEX"] - VRE_STOR = inputs["VRE_STOR"] - VS_STOR = !isempty(VRE_STOR) ? inputs["VS_STOR"] : [] - - # Storage level (state of charge) of each resource in each time step - dfStorage = DataFrame(Resource = inputs["RESOURCE_NAMES"], Zone = zones) - storagevcapvalue = zeros(G,T) + T = inputs["T"] # Number of time steps (hours) + G = inputs["G"] + STOR_ALL = inputs["STOR_ALL"] + HYDRO_RES = inputs["HYDRO_RES"] + FLEX = inputs["FLEX"] + VRE_STOR = inputs["VRE_STOR"] + VS_STOR = !isempty(VRE_STOR) ? inputs["VS_STOR"] : [] - if !isempty(inputs["STOR_ALL"]) - storagevcapvalue[STOR_ALL, :] = value.(EP[:vS][STOR_ALL, :]) - end - if !isempty(inputs["HYDRO_RES"]) - storagevcapvalue[HYDRO_RES, :] = value.(EP[:vS_HYDRO][HYDRO_RES, :]) - end - if !isempty(inputs["FLEX"]) - storagevcapvalue[FLEX, :] = value.(EP[:vS_FLEX][FLEX, :]) - end - if !isempty(VS_STOR) - storagevcapvalue[VS_STOR, :] = value.(EP[:vS_VRE_STOR][VS_STOR, :]) - end - if setup["ParameterScale"] == 1 - storagevcapvalue *= ModelScalingFactor - end + # Storage level (state of charge) of each resource in each time step + dfStorage = DataFrame(Resource = inputs["RESOURCE_NAMES"], Zone = zones) + storagevcapvalue = zeros(G, T) - dfStorage = hcat(dfStorage, DataFrame(storagevcapvalue, :auto)) - auxNew_Names=[Symbol("Resource");Symbol("Zone");[Symbol("t$t") for t in 1:T]] - rename!(dfStorage,auxNew_Names) - CSV.write(joinpath(path, "storage.csv"), dftranspose(dfStorage, false), header=false) + if !isempty(inputs["STOR_ALL"]) + storagevcapvalue[STOR_ALL, :] = value.(EP[:vS][STOR_ALL, :]) + end + if !isempty(inputs["HYDRO_RES"]) + storagevcapvalue[HYDRO_RES, :] = value.(EP[:vS_HYDRO][HYDRO_RES, :]) + end + if !isempty(inputs["FLEX"]) + storagevcapvalue[FLEX, :] = value.(EP[:vS_FLEX][FLEX, :]) + end + if !isempty(VS_STOR) + storagevcapvalue[VS_STOR, :] = value.(EP[:vS_VRE_STOR][VS_STOR, :]) + end + if setup["ParameterScale"] == 1 + storagevcapvalue *= ModelScalingFactor + end + + dfStorage = hcat(dfStorage, DataFrame(storagevcapvalue, :auto)) + auxNew_Names = [Symbol("Resource"); Symbol("Zone"); [Symbol("t$t") for t in 1:T]] + rename!(dfStorage, auxNew_Names) + CSV.write(joinpath(path, "storage.csv"), dftranspose(dfStorage, false), header = false) end diff --git a/src/write_outputs/write_storagedual.jl b/src/write_outputs/write_storagedual.jl index 53a99f9603..a30414d4e7 100644 --- a/src/write_outputs/write_storagedual.jl +++ b/src/write_outputs/write_storagedual.jl @@ -4,60 +4,71 @@ Function for reporting dual of storage level (state of charge) balance of each resource in each time step. """ function write_storagedual(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - gen = inputs["RESOURCES"] - zones = zone_id.(gen) - - G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) - T = inputs["T"] # Number of time steps (hours) - - START_SUBPERIODS = inputs["START_SUBPERIODS"] - INTERIOR_SUBPERIODS = inputs["INTERIOR_SUBPERIODS"] - REP_PERIOD = inputs["REP_PERIOD"] - STOR_ALL = inputs["STOR_ALL"] - VRE_STOR = inputs["VRE_STOR"] - if !isempty(VRE_STOR) - VS_STOR = inputs["VS_STOR"] - VS_LDS = inputs["VS_LDS"] - VS_NONLDS = setdiff(VS_STOR, VS_LDS) - end - - # # Dual of storage level (state of charge) balance of each resource in each time step - dfStorageDual = DataFrame(Resource = inputs["RESOURCE_NAMES"], Zone = zones) - dual_values = zeros(G, T) - - # Loop over W separately hours_per_subperiod - if !isempty(STOR_ALL) - STOR_ALL_NONLDS = setdiff(STOR_ALL, inputs["STOR_LONG_DURATION"]) - STOR_ALL_LDS = intersect(STOR_ALL, inputs["STOR_LONG_DURATION"]) - dual_values[STOR_ALL, INTERIOR_SUBPERIODS] = (dual.(EP[:cSoCBalInterior][INTERIOR_SUBPERIODS, STOR_ALL]).data ./ inputs["omega"][INTERIOR_SUBPERIODS])' - dual_values[STOR_ALL_NONLDS, START_SUBPERIODS] = (dual.(EP[:cSoCBalStart][START_SUBPERIODS, STOR_ALL_NONLDS]).data ./ inputs["omega"][START_SUBPERIODS])' - if !isempty(STOR_ALL_LDS) - if inputs["REP_PERIOD"] > 1 - dual_values[STOR_ALL_LDS, START_SUBPERIODS] = (dual.(EP[:cSoCBalLongDurationStorageStart][1:REP_PERIOD, STOR_ALL_LDS]).data ./ inputs["omega"][START_SUBPERIODS])' - else - dual_values[STOR_ALL_LDS, START_SUBPERIODS] = (dual.(EP[:cSoCBalStart][START_SUBPERIODS, STOR_ALL_LDS]).data ./ inputs["omega"][START_SUBPERIODS])' - end - end - end - - if !isempty(VRE_STOR) - dual_values[VS_STOR, INTERIOR_SUBPERIODS] = ((dual.(EP[:cSoCBalInterior_VRE_STOR][VS_STOR, INTERIOR_SUBPERIODS]).data)' ./ inputs["omega"][INTERIOR_SUBPERIODS])' - dual_values[VS_NONLDS, START_SUBPERIODS] = ((dual.(EP[:cSoCBalStart_VRE_STOR][VS_NONLDS, START_SUBPERIODS]).data)' ./ inputs["omega"][START_SUBPERIODS])' - if !isempty(VS_LDS) - if inputs["REP_PERIOD"] > 1 - dual_values[VS_LDS, START_SUBPERIODS] = ((dual.(EP[:cVreStorSoCBalLongDurationStorageStart][VS_LDS, 1:REP_PERIOD]).data)' ./ inputs["omega"][START_SUBPERIODS])' - else - dual_values[VS_LDS, START_SUBPERIODS] = ((dual.(EP[:cSoCBalStart_VRE_STOR][VS_LDS, START_SUBPERIODS]).data)' ./ inputs["omega"][START_SUBPERIODS])' - end - end - end - - if setup["ParameterScale"] == 1 - dual_values *= ModelScalingFactor - end - - dfStorageDual=hcat(dfStorageDual, DataFrame(dual_values, :auto)) - rename!(dfStorageDual,[Symbol("Resource");Symbol("Zone");[Symbol("t$t") for t in 1:T]]) - - CSV.write(joinpath(path, "storagebal_duals.csv"), dftranspose(dfStorageDual, false), header=false) + gen = inputs["RESOURCES"] + zones = zone_id.(gen) + + G = inputs["G"] # Number of resources (generators, storage, DR, and DERs) + T = inputs["T"] # Number of time steps (hours) + + START_SUBPERIODS = inputs["START_SUBPERIODS"] + INTERIOR_SUBPERIODS = inputs["INTERIOR_SUBPERIODS"] + REP_PERIOD = inputs["REP_PERIOD"] + STOR_ALL = inputs["STOR_ALL"] + VRE_STOR = inputs["VRE_STOR"] + if !isempty(VRE_STOR) + VS_STOR = inputs["VS_STOR"] + VS_LDS = inputs["VS_LDS"] + VS_NONLDS = setdiff(VS_STOR, VS_LDS) + end + + # # Dual of storage level (state of charge) balance of each resource in each time step + dfStorageDual = DataFrame(Resource = inputs["RESOURCE_NAMES"], Zone = zones) + dual_values = zeros(G, T) + + # Loop over W separately hours_per_subperiod + if !isempty(STOR_ALL) + STOR_ALL_NONLDS = setdiff(STOR_ALL, inputs["STOR_LONG_DURATION"]) + STOR_ALL_LDS = intersect(STOR_ALL, inputs["STOR_LONG_DURATION"]) + dual_values[STOR_ALL, INTERIOR_SUBPERIODS] = (dual.(EP[:cSoCBalInterior][INTERIOR_SUBPERIODS, + STOR_ALL]).data ./ inputs["omega"][INTERIOR_SUBPERIODS])' + dual_values[STOR_ALL_NONLDS, START_SUBPERIODS] = (dual.(EP[:cSoCBalStart][START_SUBPERIODS, + STOR_ALL_NONLDS]).data ./ inputs["omega"][START_SUBPERIODS])' + if !isempty(STOR_ALL_LDS) + if inputs["REP_PERIOD"] > 1 + dual_values[STOR_ALL_LDS, START_SUBPERIODS] = (dual.(EP[:cSoCBalLongDurationStorageStart][1:REP_PERIOD, + STOR_ALL_LDS]).data ./ inputs["omega"][START_SUBPERIODS])' + else + dual_values[STOR_ALL_LDS, START_SUBPERIODS] = (dual.(EP[:cSoCBalStart][START_SUBPERIODS, + STOR_ALL_LDS]).data ./ inputs["omega"][START_SUBPERIODS])' + end + end + end + + if !isempty(VRE_STOR) + dual_values[VS_STOR, INTERIOR_SUBPERIODS] = ((dual.(EP[:cSoCBalInterior_VRE_STOR][VS_STOR, + INTERIOR_SUBPERIODS]).data)' ./ inputs["omega"][INTERIOR_SUBPERIODS])' + dual_values[VS_NONLDS, START_SUBPERIODS] = ((dual.(EP[:cSoCBalStart_VRE_STOR][VS_NONLDS, + START_SUBPERIODS]).data)' ./ inputs["omega"][START_SUBPERIODS])' + if !isempty(VS_LDS) + if inputs["REP_PERIOD"] > 1 + dual_values[VS_LDS, START_SUBPERIODS] = ((dual.(EP[:cVreStorSoCBalLongDurationStorageStart][VS_LDS, + 1:REP_PERIOD]).data)' ./ inputs["omega"][START_SUBPERIODS])' + else + dual_values[VS_LDS, START_SUBPERIODS] = ((dual.(EP[:cSoCBalStart_VRE_STOR][VS_LDS, + START_SUBPERIODS]).data)' ./ inputs["omega"][START_SUBPERIODS])' + end + end + end + + if setup["ParameterScale"] == 1 + dual_values *= ModelScalingFactor + end + + dfStorageDual = hcat(dfStorageDual, DataFrame(dual_values, :auto)) + rename!(dfStorageDual, + [Symbol("Resource"); Symbol("Zone"); [Symbol("t$t") for t in 1:T]]) + + CSV.write(joinpath(path, "storagebal_duals.csv"), + dftranspose(dfStorageDual, false), + header = false) end diff --git a/src/write_outputs/write_subsidy_revenue.jl b/src/write_outputs/write_subsidy_revenue.jl index b7702cd747..5be5d66b48 100644 --- a/src/write_outputs/write_subsidy_revenue.jl +++ b/src/write_outputs/write_subsidy_revenue.jl @@ -4,98 +4,120 @@ Function for reporting subsidy revenue earned if a generator specified `Min_Cap` is provided in the input file, or if a generator is subject to a Minimum Capacity Requirement constraint. The unit is \$. """ function write_subsidy_revenue(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - gen = inputs["RESOURCES"] - regions = region.(gen) - clusters = cluster.(gen) - zones = zone_id.(gen) - rid = resource_id.(gen) - - G = inputs["G"] + gen = inputs["RESOURCES"] + regions = region.(gen) + clusters = cluster.(gen) + zones = zone_id.(gen) + rid = resource_id.(gen) - dfSubRevenue = DataFrame(Region = regions, Resource = inputs["RESOURCE_NAMES"], Zone = zones, Cluster = clusters, R_ID=rid, SubsidyRevenue = zeros(G)) - MIN_CAP = ids_with_positive(gen, min_cap_mw) - if !isempty(inputs["VRE_STOR"]) - MIN_CAP_SOLAR = ids_with_positive(gen.VreStorage, min_cap_solar_mw) - MIN_CAP_WIND = ids_with_positive(gen.VreStorage, min_cap_wind_mw) - MIN_CAP_STOR = ids_with_positive(gen, min_cap_mwh) - if !isempty(MIN_CAP_SOLAR) - dfSubRevenue.SubsidyRevenue[MIN_CAP_SOLAR] .+= (value.(EP[:eTotalCap_SOLAR])[MIN_CAP_SOLAR]) .* (dual.(EP[:cMinCap_Solar][MIN_CAP_SOLAR])).data - end - if !isempty(MIN_CAP_WIND) - dfSubRevenue.SubsidyRevenue[MIN_CAP_WIND] .+= (value.(EP[:eTotalCap_WIND])[MIN_CAP_WIND]) .* (dual.(EP[:cMinCap_Wind][MIN_CAP_WIND])).data - end - if !isempty(MIN_CAP_STOR) - dfSubRevenue.SubsidyRevenue[MIN_CAP_STOR] .+= (value.(EP[:eTotalCap_STOR])[MIN_CAP_STOR]) .* (dual.(EP[:cMinCap_Stor][MIN_CAP_STOR])).data - end - end - dfSubRevenue.SubsidyRevenue[MIN_CAP] .= (value.(EP[:eTotalCap])[MIN_CAP]) .* (dual.(EP[:cMinCap][MIN_CAP])).data - ### calculating tech specific subsidy revenue - dfRegSubRevenue = DataFrame(Region = regions, Resource = inputs["RESOURCE_NAMES"], Zone = zones, Cluster = clusters, R_ID=rid, SubsidyRevenue = zeros(G)) - if (setup["MinCapReq"] >= 1) - for mincap in 1:inputs["NumberOfMinCapReqs"] # This key only exists if MinCapReq >= 1, so we can't get it at the top outside of this condition. - MIN_CAP_GEN = ids_with_policy(gen, min_cap, tag=mincap) - dfRegSubRevenue.SubsidyRevenue[MIN_CAP_GEN] .= dfRegSubRevenue.SubsidyRevenue[MIN_CAP_GEN] + (value.(EP[:eTotalCap][MIN_CAP_GEN])) * (dual.(EP[:cZoneMinCapReq][mincap])) - if !isempty(inputs["VRE_STOR"]) - gen_VRE_STOR = gen.VreStorage - HAS_MIN_CAP_STOR = ids_with_policy(gen_VRE_STOR, min_cap_stor, tag=mincap) - MIN_CAP_GEN_SOLAR = ids_with_policy(gen_VRE_STOR, min_cap_solar, tag=mincap) - MIN_CAP_GEN_WIND = ids_with_policy(gen_VRE_STOR, min_cap_wind, tag=mincap) - MIN_CAP_GEN_ASYM_DC_DIS = intersect(inputs["VS_ASYM_DC_DISCHARGE"], HAS_MIN_CAP_STOR) - MIN_CAP_GEN_ASYM_AC_DIS = intersect(inputs["VS_ASYM_AC_DISCHARGE"], HAS_MIN_CAP_STOR) - MIN_CAP_GEN_SYM_DC = intersect(inputs["VS_SYM_DC"], HAS_MIN_CAP_STOR) - MIN_CAP_GEN_SYM_AC = intersect(inputs["VS_SYM_AC"], HAS_MIN_CAP_STOR) - if !isempty(MIN_CAP_GEN_SOLAR) - dfRegSubRevenue.SubsidyRevenue[MIN_CAP_GEN_SOLAR] .+= ( - (value.(EP[:eTotalCap_SOLAR][MIN_CAP_GEN_SOLAR]).data) - .* etainverter.(gen[ids_with_policy(gen, min_cap_solar, tag=mincap)]) - * (dual.(EP[:cZoneMinCapReq][mincap])) - ) - end - if !isempty(MIN_CAP_GEN_WIND) - dfRegSubRevenue.SubsidyRevenue[MIN_CAP_GEN_WIND] .+= ( - (value.(EP[:eTotalCap_WIND][MIN_CAP_GEN_WIND]).data) - * (dual.(EP[:cZoneMinCapReq][mincap])) - ) - end - if !isempty(MIN_CAP_GEN_ASYM_DC_DIS) - MIN_CAP_GEN_ASYM_DC_DIS = intersect(inputs["VS_ASYM_DC_DISCHARGE"], HAS_MIN_CAP_STOR) - dfRegSubRevenue.SubsidyRevenue[MIN_CAP_GEN_ASYM_DC_DIS] .+= ( - (value.(EP[:eTotalCapDischarge_DC][MIN_CAP_GEN_ASYM_DC_DIS].data) - .* etainverter.(gen_VRE_STOR[min_cap_stor.(gen_VRE_STOR, tag=mincap).==1 .& (gen_VRE_STOR.stor_dc_discharge.==2)])) - * (dual.(EP[:cZoneMinCapReq][mincap])) - ) - end - if !isempty(MIN_CAP_GEN_ASYM_AC_DIS) - dfRegSubRevenue.SubsidyRevenue[MIN_CAP_GEN_ASYM_AC_DIS] .+= ( - (value.(EP[:eTotalCapDischarge_AC][MIN_CAP_GEN_ASYM_AC_DIS]).data) - * (dual.(EP[:cZoneMinCapReq][mincap])) - ) - end - if !isempty(MIN_CAP_GEN_SYM_DC) - dfRegSubRevenue.SubsidyRevenue[MIN_CAP_GEN_SYM_DC] .+= ( - (value.(EP[:eTotalCap_STOR][MIN_CAP_GEN_SYM_DC]).data - .* power_to_energy_dc.(gen_VRE_STOR[(min_cap_stor.(gen_VRE_STOR, tag=mincap).==1 .& (gen_VRE_STOR.stor_dc_discharge.==1))]) - .* etainverter.(gen_VRE_STOR[(min_cap_stor.(gen_VRE_STOR, tag=mincap).==1 .& (gen_VRE_STOR.stor_dc_discharge.==1))])) - * (dual.(EP[:cZoneMinCapReq][mincap])) - ) - end - if !isempty(MIN_CAP_GEN_SYM_AC) - dfRegSubRevenue.SubsidyRevenue[MIN_CAP_GEN_SYM_AC] .+= ( - (value.(EP[:eTotalCap_STOR][MIN_CAP_GEN_SYM_AC]).data - .* power_to_energy_ac.(gen_VRE_STOR[(min_cap_stor.(gen_VRE_STOR, tag=mincap).==1 .& (gen_VRE_STOR.stor_ac_discharge.==1))])) - * (dual.(EP[:cZoneMinCapReq][mincap])) - ) - end - end - end - end + G = inputs["G"] - if setup["ParameterScale"] == 1 - dfSubRevenue.SubsidyRevenue *= ModelScalingFactor^2 #convert from Million US$ to US$ - dfRegSubRevenue.SubsidyRevenue *= ModelScalingFactor^2 #convert from Million US$ to US$ - end + dfSubRevenue = DataFrame(Region = regions, + Resource = inputs["RESOURCE_NAMES"], + Zone = zones, + Cluster = clusters, + R_ID = rid, + SubsidyRevenue = zeros(G)) + MIN_CAP = ids_with_positive(gen, min_cap_mw) + if !isempty(inputs["VRE_STOR"]) + MIN_CAP_SOLAR = ids_with_positive(gen.VreStorage, min_cap_solar_mw) + MIN_CAP_WIND = ids_with_positive(gen.VreStorage, min_cap_wind_mw) + MIN_CAP_STOR = ids_with_positive(gen, min_cap_mwh) + if !isempty(MIN_CAP_SOLAR) + dfSubRevenue.SubsidyRevenue[MIN_CAP_SOLAR] .+= (value.(EP[:eTotalCap_SOLAR])[MIN_CAP_SOLAR]) .* + (dual.(EP[:cMinCap_Solar][MIN_CAP_SOLAR])).data + end + if !isempty(MIN_CAP_WIND) + dfSubRevenue.SubsidyRevenue[MIN_CAP_WIND] .+= (value.(EP[:eTotalCap_WIND])[MIN_CAP_WIND]) .* + (dual.(EP[:cMinCap_Wind][MIN_CAP_WIND])).data + end + if !isempty(MIN_CAP_STOR) + dfSubRevenue.SubsidyRevenue[MIN_CAP_STOR] .+= (value.(EP[:eTotalCap_STOR])[MIN_CAP_STOR]) .* + (dual.(EP[:cMinCap_Stor][MIN_CAP_STOR])).data + end + end + dfSubRevenue.SubsidyRevenue[MIN_CAP] .= (value.(EP[:eTotalCap])[MIN_CAP]) .* + (dual.(EP[:cMinCap][MIN_CAP])).data + ### calculating tech specific subsidy revenue + dfRegSubRevenue = DataFrame(Region = regions, + Resource = inputs["RESOURCE_NAMES"], + Zone = zones, + Cluster = clusters, + R_ID = rid, + SubsidyRevenue = zeros(G)) + if (setup["MinCapReq"] >= 1) + for mincap in 1:inputs["NumberOfMinCapReqs"] # This key only exists if MinCapReq >= 1, so we can't get it at the top outside of this condition. + MIN_CAP_GEN = ids_with_policy(gen, min_cap, tag = mincap) + dfRegSubRevenue.SubsidyRevenue[MIN_CAP_GEN] .= dfRegSubRevenue.SubsidyRevenue[MIN_CAP_GEN] + + (value.(EP[:eTotalCap][MIN_CAP_GEN])) * + (dual.(EP[:cZoneMinCapReq][mincap])) + if !isempty(inputs["VRE_STOR"]) + gen_VRE_STOR = gen.VreStorage + HAS_MIN_CAP_STOR = ids_with_policy(gen_VRE_STOR, min_cap_stor, tag = mincap) + MIN_CAP_GEN_SOLAR = ids_with_policy(gen_VRE_STOR, + min_cap_solar, + tag = mincap) + MIN_CAP_GEN_WIND = ids_with_policy(gen_VRE_STOR, min_cap_wind, tag = mincap) + MIN_CAP_GEN_ASYM_DC_DIS = intersect(inputs["VS_ASYM_DC_DISCHARGE"], + HAS_MIN_CAP_STOR) + MIN_CAP_GEN_ASYM_AC_DIS = intersect(inputs["VS_ASYM_AC_DISCHARGE"], + HAS_MIN_CAP_STOR) + MIN_CAP_GEN_SYM_DC = intersect(inputs["VS_SYM_DC"], HAS_MIN_CAP_STOR) + MIN_CAP_GEN_SYM_AC = intersect(inputs["VS_SYM_AC"], HAS_MIN_CAP_STOR) + if !isempty(MIN_CAP_GEN_SOLAR) + dfRegSubRevenue.SubsidyRevenue[MIN_CAP_GEN_SOLAR] .+= ((value.(EP[:eTotalCap_SOLAR][MIN_CAP_GEN_SOLAR]).data) + .* + etainverter.(gen[ids_with_policy(gen, + min_cap_solar, + tag = mincap)]) + * + (dual.(EP[:cZoneMinCapReq][mincap]))) + end + if !isempty(MIN_CAP_GEN_WIND) + dfRegSubRevenue.SubsidyRevenue[MIN_CAP_GEN_WIND] .+= ((value.(EP[:eTotalCap_WIND][MIN_CAP_GEN_WIND]).data) + * + (dual.(EP[:cZoneMinCapReq][mincap]))) + end + if !isempty(MIN_CAP_GEN_ASYM_DC_DIS) + MIN_CAP_GEN_ASYM_DC_DIS = intersect(inputs["VS_ASYM_DC_DISCHARGE"], + HAS_MIN_CAP_STOR) + dfRegSubRevenue.SubsidyRevenue[MIN_CAP_GEN_ASYM_DC_DIS] .+= ((value.(EP[:eTotalCapDischarge_DC][MIN_CAP_GEN_ASYM_DC_DIS].data) + .* + etainverter.(gen_VRE_STOR[min_cap_stor.(gen_VRE_STOR, tag = mincap) .== 1 .& (gen_VRE_STOR.stor_dc_discharge .== 2)])) + * + (dual.(EP[:cZoneMinCapReq][mincap]))) + end + if !isempty(MIN_CAP_GEN_ASYM_AC_DIS) + dfRegSubRevenue.SubsidyRevenue[MIN_CAP_GEN_ASYM_AC_DIS] .+= ((value.(EP[:eTotalCapDischarge_AC][MIN_CAP_GEN_ASYM_AC_DIS]).data) + * + (dual.(EP[:cZoneMinCapReq][mincap]))) + end + if !isempty(MIN_CAP_GEN_SYM_DC) + dfRegSubRevenue.SubsidyRevenue[MIN_CAP_GEN_SYM_DC] .+= ((value.(EP[:eTotalCap_STOR][MIN_CAP_GEN_SYM_DC]).data + .* + power_to_energy_dc.(gen_VRE_STOR[(min_cap_stor.(gen_VRE_STOR, tag = mincap) .== 1 .& (gen_VRE_STOR.stor_dc_discharge .== 1))]) + .* + etainverter.(gen_VRE_STOR[(min_cap_stor.(gen_VRE_STOR, tag = mincap) .== 1 .& (gen_VRE_STOR.stor_dc_discharge .== 1))])) + * + (dual.(EP[:cZoneMinCapReq][mincap]))) + end + if !isempty(MIN_CAP_GEN_SYM_AC) + dfRegSubRevenue.SubsidyRevenue[MIN_CAP_GEN_SYM_AC] .+= ((value.(EP[:eTotalCap_STOR][MIN_CAP_GEN_SYM_AC]).data + .* + power_to_energy_ac.(gen_VRE_STOR[(min_cap_stor.(gen_VRE_STOR, tag = mincap) .== 1 .& (gen_VRE_STOR.stor_ac_discharge .== 1))])) + * + (dual.(EP[:cZoneMinCapReq][mincap]))) + end + end + end + end - CSV.write(joinpath(path, "SubsidyRevenue.csv"), dfSubRevenue) - CSV.write(joinpath(path, "RegSubsidyRevenue.csv"), dfRegSubRevenue) - return dfSubRevenue, dfRegSubRevenue + if setup["ParameterScale"] == 1 + dfSubRevenue.SubsidyRevenue *= ModelScalingFactor^2 #convert from Million US$ to US$ + dfRegSubRevenue.SubsidyRevenue *= ModelScalingFactor^2 #convert from Million US$ to US$ + end + + CSV.write(joinpath(path, "SubsidyRevenue.csv"), dfSubRevenue) + CSV.write(joinpath(path, "RegSubsidyRevenue.csv"), dfRegSubRevenue) + return dfSubRevenue, dfRegSubRevenue end diff --git a/src/write_outputs/write_time_weights.jl b/src/write_outputs/write_time_weights.jl index 8f799478f0..b29bbdcb2f 100644 --- a/src/write_outputs/write_time_weights.jl +++ b/src/write_outputs/write_time_weights.jl @@ -1,6 +1,6 @@ function write_time_weights(path::AbstractString, inputs::Dict) - T = inputs["T"] # Number of time steps (hours) - # Save array of weights for each time period (when using time sampling) - dfTimeWeights = DataFrame(Time=1:T, Weight=inputs["omega"]) - CSV.write(joinpath(path, "time_weights.csv"), dfTimeWeights) + T = inputs["T"] # Number of time steps (hours) + # Save array of weights for each time period (when using time sampling) + dfTimeWeights = DataFrame(Time = 1:T, Weight = inputs["omega"]) + CSV.write(joinpath(path, "time_weights.csv"), dfTimeWeights) end diff --git a/src/write_outputs/write_vre_stor.jl b/src/write_outputs/write_vre_stor.jl index 6f7e617ec1..ef261d5196 100644 --- a/src/write_outputs/write_vre_stor.jl +++ b/src/write_outputs/write_vre_stor.jl @@ -5,16 +5,16 @@ Function for writing the vre-storage specific files. """ function write_vre_stor(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - ### CAPACITY DECISIONS ### - dfVreStor = write_vre_stor_capacity(path, inputs, setup, EP) + ### CAPACITY DECISIONS ### + dfVreStor = write_vre_stor_capacity(path, inputs, setup, EP) - ### CHARGING DECISIONS ### - write_vre_stor_charge(path, inputs, setup, EP) + ### CHARGING DECISIONS ### + write_vre_stor_charge(path, inputs, setup, EP) - ### DISCHARGING DECISIONS ### - write_vre_stor_discharge(path, inputs, setup, EP) + ### DISCHARGING DECISIONS ### + write_vre_stor_discharge(path, inputs, setup, EP) - return dfVreStor + return dfVreStor end @doc raw""" @@ -23,262 +23,289 @@ end Function for writing the vre-storage capacities. """ function write_vre_stor_capacity(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - gen = inputs["RESOURCES"] - gen_VRE_STOR = gen.VreStorage - - VRE_STOR = inputs["VRE_STOR"] - SOLAR = inputs["VS_SOLAR"] - WIND = inputs["VS_WIND"] - DC = inputs["VS_DC"] - STOR = inputs["VS_STOR"] - MultiStage = setup["MultiStage"] - size_vrestor_resources = size(inputs["RESOURCE_NAMES_VRE_STOR"]) - - # Solar capacity - capsolar = zeros(size_vrestor_resources) - retcapsolar = zeros(size_vrestor_resources) - existingcapsolar = zeros(size_vrestor_resources) - - # Wind capacity - capwind = zeros(size_vrestor_resources) - retcapwind = zeros(size_vrestor_resources) - existingcapwind = zeros(size_vrestor_resources) - - # Inverter capacity - capdc = zeros(size_vrestor_resources) - retcapdc = zeros(size_vrestor_resources) - existingcapdc = zeros(size_vrestor_resources) - - # Grid connection capacity - capgrid = zeros(size_vrestor_resources) - retcapgrid = zeros(size_vrestor_resources) - existingcapgrid = zeros(size_vrestor_resources) - - # Energy storage capacity - capenergy = zeros(size_vrestor_resources) - retcapenergy = zeros(size_vrestor_resources) - existingcapenergy = zeros(size_vrestor_resources) - - # Charge storage capacity DC - capchargedc = zeros(size_vrestor_resources) - retcapchargedc = zeros(size_vrestor_resources) - existingcapchargedc = zeros(size_vrestor_resources) - - # Charge storage capacity AC - capchargeac = zeros(size_vrestor_resources) - retcapchargeac = zeros(size_vrestor_resources) - existingcapchargeac = zeros(size_vrestor_resources) - - # Discharge storage capacity DC - capdischargedc = zeros(size_vrestor_resources) - retcapdischargedc = zeros(size_vrestor_resources) - existingcapdischargedc = zeros(size_vrestor_resources) - - # Discharge storage capacity AC - capdischargeac = zeros(size_vrestor_resources) - retcapdischargeac = zeros(size_vrestor_resources) - existingcapdischargeac = zeros(size_vrestor_resources) - - j = 1 - for i in VRE_STOR - existingcapgrid[j] = MultiStage == 1 ? value(EP[:vEXISTINGCAP][i]) : existing_cap_mw(gen[i]) - if i in inputs["NEW_CAP"] - capgrid[j] = value(EP[:vCAP][i]) - end - if i in inputs["RET_CAP"] - retcapgrid[j] = value(EP[:vRETCAP][i]) - end - - if i in SOLAR - existingcapsolar[j] = MultiStage == 1 ? value(EP[:vEXISTINGSOLARCAP][i]) : existing_cap_solar_mw(gen_VRE_STOR[j]) - if i in inputs["NEW_CAP_SOLAR"] - capsolar[j] = value(EP[:vSOLARCAP][i]) - end - if i in inputs["RET_CAP_SOLAR"] - retcapsolar[j] = first(value.(EP[:vRETSOLARCAP][i])) - end - end - - if i in WIND - existingcapwind[j] = MultiStage == 1 ? value(EP[:vEXISTINGWINDCAP][i]) : existing_cap_wind_mw(gen_VRE_STOR[j]) - if i in inputs["NEW_CAP_WIND"] - capwind[j] = value(EP[:vWINDCAP][i]) - end - if i in inputs["RET_CAP_WIND"] - retcapwind[j] = first(value.(EP[:vRETWINDCAP][i])) - end - end - - if i in DC - existingcapdc[j] = MultiStage == 1 ? value(EP[:vEXISTINGDCCAP][i]) : existing_cap_inverter_mw(gen_VRE_STOR[j]) - if i in inputs["NEW_CAP_DC"] - capdc[j] = value(EP[:vDCCAP][i]) - end - if i in inputs["RET_CAP_DC"] - retcapdc[j] = first(value.(EP[:vRETDCCAP][i])) - end - end - - if i in STOR - existingcapenergy[j] = MultiStage == 1 ? value(EP[:vEXISTINGCAPENERGY_VS][i]) : existing_cap_mwh(gen[i]) - if i in inputs["NEW_CAP_STOR"] - capenergy[j] = value(EP[:vCAPENERGY_VS][i]) - end - if i in inputs["RET_CAP_STOR"] - retcapenergy[j] = first(value.(EP[:vRETCAPENERGY_VS][i])) - end - - if i in inputs["VS_ASYM_DC_CHARGE"] - if i in inputs["NEW_CAP_CHARGE_DC"] - capchargedc[j] = value(EP[:vCAPCHARGE_DC][i]) - end - if i in inputs["RET_CAP_CHARGE_DC"] - retcapchargedc[j] = value(EP[:vRETCAPCHARGE_DC][i]) - end - existingcapchargedc[j] = MultiStage == 1 ? value(EP[:vEXISTINGCAPCHARGEDC][i]) : existing_cap_charge_dc_mw(gen_VRE_STOR[j]) - end - if i in inputs["VS_ASYM_AC_CHARGE"] - if i in inputs["NEW_CAP_CHARGE_AC"] - capchargeac[j] = value(EP[:vCAPCHARGE_AC][i]) - end - if i in inputs["RET_CAP_CHARGE_AC"] - retcapchargeac[j] = value(EP[:vRETCAPCHARGE_AC][i]) - end - existingcapchargeac[j] = MultiStage == 1 ? value(EP[:vEXISTINGCAPCHARGEAC][i]) : existing_cap_charge_ac_mw(gen_VRE_STOR[j]) - end - if i in inputs["VS_ASYM_DC_DISCHARGE"] - if i in inputs["NEW_CAP_DISCHARGE_DC"] - capdischargedc[j] = value(EP[:vCAPDISCHARGE_DC][i]) - end - if i in inputs["RET_CAP_DISCHARGE_DC"] - retcapdischargedc[j] = value(EP[:vRETCAPDISCHARGE_DC][i]) - end - existingcapdischargedc[j] = MultiStage == 1 ? value(EP[:vEXISTINGCAPDISCHARGEDC][i]) : existing_cap_discharge_dc_mw(gen_VRE_STOR[j]) - end - if i in inputs["VS_ASYM_AC_DISCHARGE"] - if i in inputs["NEW_CAP_DISCHARGE_AC"] - capdischargeac[j] = value(EP[:vCAPDISCHARGE_AC][i]) - end - if i in inputs["RET_CAP_DISCHARGE_AC"] - retcapdischargeac[j] = value(EP[:vRETCAPDISCHARGE_AC][i]) - end - existingcapdischargeac[j] = MultiStage == 1 ? value(EP[:vEXISTINGCAPDISCHARGEAC][i]) : existing_cap_discharge_ac_mw(gen_VRE_STOR[j]) - end - end - j += 1 - end - - technologies = resource_type_mga.(gen_VRE_STOR) - clusters = cluster.(gen_VRE_STOR) - zones = zone_id.(gen_VRE_STOR) - - dfCap = DataFrame( - Resource = inputs["RESOURCE_NAMES_VRE_STOR"], Zone = zones, Resource_Type = technologies, Cluster=clusters, - StartCapSolar = existingcapsolar[:], - RetCapSolar = retcapsolar[:], - NewCapSolar = capsolar[:], - EndCapSolar = existingcapsolar[:] - retcapsolar[:] + capsolar[:], - StartCapWind = existingcapwind[:], - RetCapWind = retcapwind[:], - NewCapWind = capwind[:], - EndCapWind = existingcapwind[:] - retcapwind[:] + capwind[:], - StartCapDC = existingcapdc[:], - RetCapDC = retcapdc[:], - NewCapDC = capdc[:], - EndCapDC = existingcapdc[:] - retcapdc[:] + capdc[:], - StartCapGrid = existingcapgrid[:], - RetCapGrid = retcapgrid[:], - NewCapGrid = capgrid[:], - EndCapGrid = existingcapgrid[:] - retcapgrid[:] + capgrid[:], - StartEnergyCap = existingcapenergy[:], - RetEnergyCap = retcapenergy[:], - NewEnergyCap = capenergy[:], - EndEnergyCap = existingcapenergy[:] - retcapenergy[:] + capenergy[:], - StartChargeDCCap = existingcapchargedc[:], - RetChargeDCCap = retcapchargedc[:], - NewChargeDCCap = capchargedc[:], - EndChargeDCCap = existingcapchargedc[:] - retcapchargedc[:] + capchargedc[:], - StartChargeACCap = existingcapchargeac[:], - RetChargeACCap = retcapchargeac[:], - NewChargeACCap = capchargeac[:], - EndChargeACCap = existingcapchargeac[:] - retcapchargeac[:] + capchargeac[:], - StartDischargeDCCap = existingcapdischargedc[:], - RetDischargeDCCap = retcapdischargedc[:], - NewDischargeDCCap = capdischargedc[:], - EndDischargeDCCap = existingcapdischargedc[:] - retcapdischargedc[:] + capdischargedc[:], - StartDischargeACCap = existingcapdischargeac[:], - RetDischargeACCap = retcapdischargeac[:], - NewDischargeACCap = capdischargeac[:], - EndDischargeACCap = existingcapdischargeac[:] - retcapdischargeac[:] + capdischargeac[:] - ) - - if setup["ParameterScale"] ==1 - columns_to_scale = [ - :StartCapSolar, - :RetCapSolar, - :NewCapSolar, - :EndCapSolar, - :StartCapWind, - :RetCapWind, - :NewCapWind, - :EndCapWind, - :StartCapDC, - :RetCapDC, - :NewCapDC, - :EndCapDC, - :StartCapGrid, - :RetCapGrid, - :NewCapGrid, - :EndCapGrid, - :StartEnergyCap, - :RetEnergyCap, - :NewEnergyCap, - :EndEnergyCap, - :StartChargeACCap, - :RetChargeACCap, - :NewChargeACCap, - :EndChargeACCap, - :StartChargeDCCap, - :RetChargeDCCap, - :NewChargeDCCap, - :EndChargeDCCap, - :StartDischargeDCCap, - :RetDischargeDCCap, - :NewDischargeDCCap, - :EndDischargeDCCap, - :StartDischargeACCap, - :RetDischargeACCap, - :NewDischargeACCap, - :EndDischargeACCap, - ] - dfCap[!, columns_to_scale] .*= ModelScalingFactor - end - - total = DataFrame( - Resource = "Total", Zone = "n/a", Resource_Type = "Total", Cluster= "n/a", - StartCapSolar = sum(dfCap[!,:StartCapSolar]), RetCapSolar = sum(dfCap[!,:RetCapSolar]), - NewCapSolar = sum(dfCap[!,:NewCapSolar]), EndCapSolar = sum(dfCap[!,:EndCapSolar]), - StartCapWind = sum(dfCap[!,:StartCapWind]), RetCapWind = sum(dfCap[!,:RetCapWind]), - NewCapWind = sum(dfCap[!,:NewCapWind]), EndCapWind = sum(dfCap[!,:EndCapWind]), - StartCapDC = sum(dfCap[!,:StartCapDC]), RetCapDC = sum(dfCap[!,:RetCapDC]), - NewCapDC = sum(dfCap[!,:NewCapDC]), EndCapDC = sum(dfCap[!,:EndCapDC]), - StartCapGrid = sum(dfCap[!,:StartCapGrid]), RetCapGrid = sum(dfCap[!,:RetCapGrid]), - NewCapGrid = sum(dfCap[!,:NewCapGrid]), EndCapGrid = sum(dfCap[!,:EndCapGrid]), - StartEnergyCap = sum(dfCap[!,:StartEnergyCap]), RetEnergyCap = sum(dfCap[!,:RetEnergyCap]), - NewEnergyCap = sum(dfCap[!,:NewEnergyCap]), EndEnergyCap = sum(dfCap[!,:EndEnergyCap]), - StartChargeACCap = sum(dfCap[!,:StartChargeACCap]), RetChargeACCap = sum(dfCap[!,:RetChargeACCap]), - NewChargeACCap = sum(dfCap[!,:NewChargeACCap]), EndChargeACCap = sum(dfCap[!,:EndChargeACCap]), - StartChargeDCCap = sum(dfCap[!,:StartChargeDCCap]), RetChargeDCCap = sum(dfCap[!,:RetChargeDCCap]), - NewChargeDCCap = sum(dfCap[!,:NewChargeDCCap]), EndChargeDCCap = sum(dfCap[!,:EndChargeDCCap]), - StartDischargeDCCap = sum(dfCap[!,:StartDischargeDCCap]), RetDischargeDCCap = sum(dfCap[!,:RetDischargeDCCap]), - NewDischargeDCCap = sum(dfCap[!,:NewDischargeDCCap]), EndDischargeDCCap = sum(dfCap[!,:EndDischargeDCCap]), - StartDischargeACCap = sum(dfCap[!,:StartDischargeACCap]), RetDischargeACCap = sum(dfCap[!,:RetDischargeACCap]), - NewDischargeACCap = sum(dfCap[!,:NewDischargeACCap]), EndDischargeACCap = sum(dfCap[!,:EndDischargeACCap]) - ) - - dfCap = vcat(dfCap, total) - CSV.write(joinpath(path, "vre_stor_capacity.csv"), dfCap) - return dfCap + gen = inputs["RESOURCES"] + gen_VRE_STOR = gen.VreStorage + + VRE_STOR = inputs["VRE_STOR"] + SOLAR = inputs["VS_SOLAR"] + WIND = inputs["VS_WIND"] + DC = inputs["VS_DC"] + STOR = inputs["VS_STOR"] + MultiStage = setup["MultiStage"] + size_vrestor_resources = size(inputs["RESOURCE_NAMES_VRE_STOR"]) + + # Solar capacity + capsolar = zeros(size_vrestor_resources) + retcapsolar = zeros(size_vrestor_resources) + existingcapsolar = zeros(size_vrestor_resources) + + # Wind capacity + capwind = zeros(size_vrestor_resources) + retcapwind = zeros(size_vrestor_resources) + existingcapwind = zeros(size_vrestor_resources) + + # Inverter capacity + capdc = zeros(size_vrestor_resources) + retcapdc = zeros(size_vrestor_resources) + existingcapdc = zeros(size_vrestor_resources) + + # Grid connection capacity + capgrid = zeros(size_vrestor_resources) + retcapgrid = zeros(size_vrestor_resources) + existingcapgrid = zeros(size_vrestor_resources) + + # Energy storage capacity + capenergy = zeros(size_vrestor_resources) + retcapenergy = zeros(size_vrestor_resources) + existingcapenergy = zeros(size_vrestor_resources) + + # Charge storage capacity DC + capchargedc = zeros(size_vrestor_resources) + retcapchargedc = zeros(size_vrestor_resources) + existingcapchargedc = zeros(size_vrestor_resources) + + # Charge storage capacity AC + capchargeac = zeros(size_vrestor_resources) + retcapchargeac = zeros(size_vrestor_resources) + existingcapchargeac = zeros(size_vrestor_resources) + + # Discharge storage capacity DC + capdischargedc = zeros(size_vrestor_resources) + retcapdischargedc = zeros(size_vrestor_resources) + existingcapdischargedc = zeros(size_vrestor_resources) + + # Discharge storage capacity AC + capdischargeac = zeros(size_vrestor_resources) + retcapdischargeac = zeros(size_vrestor_resources) + existingcapdischargeac = zeros(size_vrestor_resources) + + j = 1 + for i in VRE_STOR + existingcapgrid[j] = MultiStage == 1 ? value(EP[:vEXISTINGCAP][i]) : + existing_cap_mw(gen[i]) + if i in inputs["NEW_CAP"] + capgrid[j] = value(EP[:vCAP][i]) + end + if i in inputs["RET_CAP"] + retcapgrid[j] = value(EP[:vRETCAP][i]) + end + + if i in SOLAR + existingcapsolar[j] = MultiStage == 1 ? value(EP[:vEXISTINGSOLARCAP][i]) : + existing_cap_solar_mw(gen_VRE_STOR[j]) + if i in inputs["NEW_CAP_SOLAR"] + capsolar[j] = value(EP[:vSOLARCAP][i]) + end + if i in inputs["RET_CAP_SOLAR"] + retcapsolar[j] = first(value.(EP[:vRETSOLARCAP][i])) + end + end + + if i in WIND + existingcapwind[j] = MultiStage == 1 ? value(EP[:vEXISTINGWINDCAP][i]) : + existing_cap_wind_mw(gen_VRE_STOR[j]) + if i in inputs["NEW_CAP_WIND"] + capwind[j] = value(EP[:vWINDCAP][i]) + end + if i in inputs["RET_CAP_WIND"] + retcapwind[j] = first(value.(EP[:vRETWINDCAP][i])) + end + end + + if i in DC + existingcapdc[j] = MultiStage == 1 ? value(EP[:vEXISTINGDCCAP][i]) : + existing_cap_inverter_mw(gen_VRE_STOR[j]) + if i in inputs["NEW_CAP_DC"] + capdc[j] = value(EP[:vDCCAP][i]) + end + if i in inputs["RET_CAP_DC"] + retcapdc[j] = first(value.(EP[:vRETDCCAP][i])) + end + end + + if i in STOR + existingcapenergy[j] = MultiStage == 1 ? value(EP[:vEXISTINGCAPENERGY_VS][i]) : + existing_cap_mwh(gen[i]) + if i in inputs["NEW_CAP_STOR"] + capenergy[j] = value(EP[:vCAPENERGY_VS][i]) + end + if i in inputs["RET_CAP_STOR"] + retcapenergy[j] = first(value.(EP[:vRETCAPENERGY_VS][i])) + end + + if i in inputs["VS_ASYM_DC_CHARGE"] + if i in inputs["NEW_CAP_CHARGE_DC"] + capchargedc[j] = value(EP[:vCAPCHARGE_DC][i]) + end + if i in inputs["RET_CAP_CHARGE_DC"] + retcapchargedc[j] = value(EP[:vRETCAPCHARGE_DC][i]) + end + existingcapchargedc[j] = MultiStage == 1 ? + value(EP[:vEXISTINGCAPCHARGEDC][i]) : + existing_cap_charge_dc_mw(gen_VRE_STOR[j]) + end + if i in inputs["VS_ASYM_AC_CHARGE"] + if i in inputs["NEW_CAP_CHARGE_AC"] + capchargeac[j] = value(EP[:vCAPCHARGE_AC][i]) + end + if i in inputs["RET_CAP_CHARGE_AC"] + retcapchargeac[j] = value(EP[:vRETCAPCHARGE_AC][i]) + end + existingcapchargeac[j] = MultiStage == 1 ? + value(EP[:vEXISTINGCAPCHARGEAC][i]) : + existing_cap_charge_ac_mw(gen_VRE_STOR[j]) + end + if i in inputs["VS_ASYM_DC_DISCHARGE"] + if i in inputs["NEW_CAP_DISCHARGE_DC"] + capdischargedc[j] = value(EP[:vCAPDISCHARGE_DC][i]) + end + if i in inputs["RET_CAP_DISCHARGE_DC"] + retcapdischargedc[j] = value(EP[:vRETCAPDISCHARGE_DC][i]) + end + existingcapdischargedc[j] = MultiStage == 1 ? + value(EP[:vEXISTINGCAPDISCHARGEDC][i]) : + existing_cap_discharge_dc_mw(gen_VRE_STOR[j]) + end + if i in inputs["VS_ASYM_AC_DISCHARGE"] + if i in inputs["NEW_CAP_DISCHARGE_AC"] + capdischargeac[j] = value(EP[:vCAPDISCHARGE_AC][i]) + end + if i in inputs["RET_CAP_DISCHARGE_AC"] + retcapdischargeac[j] = value(EP[:vRETCAPDISCHARGE_AC][i]) + end + existingcapdischargeac[j] = MultiStage == 1 ? + value(EP[:vEXISTINGCAPDISCHARGEAC][i]) : + existing_cap_discharge_ac_mw(gen_VRE_STOR[j]) + end + end + j += 1 + end + + technologies = resource_type_mga.(gen_VRE_STOR) + clusters = cluster.(gen_VRE_STOR) + zones = zone_id.(gen_VRE_STOR) + + dfCap = DataFrame(Resource = inputs["RESOURCE_NAMES_VRE_STOR"], Zone = zones, + Resource_Type = technologies, Cluster = clusters, + StartCapSolar = existingcapsolar[:], + RetCapSolar = retcapsolar[:], + NewCapSolar = capsolar[:], + EndCapSolar = existingcapsolar[:] - retcapsolar[:] + capsolar[:], + StartCapWind = existingcapwind[:], + RetCapWind = retcapwind[:], + NewCapWind = capwind[:], + EndCapWind = existingcapwind[:] - retcapwind[:] + capwind[:], + StartCapDC = existingcapdc[:], + RetCapDC = retcapdc[:], + NewCapDC = capdc[:], + EndCapDC = existingcapdc[:] - retcapdc[:] + capdc[:], + StartCapGrid = existingcapgrid[:], + RetCapGrid = retcapgrid[:], + NewCapGrid = capgrid[:], + EndCapGrid = existingcapgrid[:] - retcapgrid[:] + capgrid[:], + StartEnergyCap = existingcapenergy[:], + RetEnergyCap = retcapenergy[:], + NewEnergyCap = capenergy[:], + EndEnergyCap = existingcapenergy[:] - retcapenergy[:] + capenergy[:], + StartChargeDCCap = existingcapchargedc[:], + RetChargeDCCap = retcapchargedc[:], + NewChargeDCCap = capchargedc[:], + EndChargeDCCap = existingcapchargedc[:] - retcapchargedc[:] + capchargedc[:], + StartChargeACCap = existingcapchargeac[:], + RetChargeACCap = retcapchargeac[:], + NewChargeACCap = capchargeac[:], + EndChargeACCap = existingcapchargeac[:] - retcapchargeac[:] + capchargeac[:], + StartDischargeDCCap = existingcapdischargedc[:], + RetDischargeDCCap = retcapdischargedc[:], + NewDischargeDCCap = capdischargedc[:], + EndDischargeDCCap = existingcapdischargedc[:] - retcapdischargedc[:] + + capdischargedc[:], + StartDischargeACCap = existingcapdischargeac[:], + RetDischargeACCap = retcapdischargeac[:], + NewDischargeACCap = capdischargeac[:], + EndDischargeACCap = existingcapdischargeac[:] - retcapdischargeac[:] + + capdischargeac[:]) + + if setup["ParameterScale"] == 1 + columns_to_scale = [ + :StartCapSolar, + :RetCapSolar, + :NewCapSolar, + :EndCapSolar, + :StartCapWind, + :RetCapWind, + :NewCapWind, + :EndCapWind, + :StartCapDC, + :RetCapDC, + :NewCapDC, + :EndCapDC, + :StartCapGrid, + :RetCapGrid, + :NewCapGrid, + :EndCapGrid, + :StartEnergyCap, + :RetEnergyCap, + :NewEnergyCap, + :EndEnergyCap, + :StartChargeACCap, + :RetChargeACCap, + :NewChargeACCap, + :EndChargeACCap, + :StartChargeDCCap, + :RetChargeDCCap, + :NewChargeDCCap, + :EndChargeDCCap, + :StartDischargeDCCap, + :RetDischargeDCCap, + :NewDischargeDCCap, + :EndDischargeDCCap, + :StartDischargeACCap, + :RetDischargeACCap, + :NewDischargeACCap, + :EndDischargeACCap, + ] + dfCap[!, columns_to_scale] .*= ModelScalingFactor + end + + total = DataFrame(Resource = "Total", Zone = "n/a", Resource_Type = "Total", + Cluster = "n/a", + StartCapSolar = sum(dfCap[!, :StartCapSolar]), + RetCapSolar = sum(dfCap[!, :RetCapSolar]), + NewCapSolar = sum(dfCap[!, :NewCapSolar]), + EndCapSolar = sum(dfCap[!, :EndCapSolar]), + StartCapWind = sum(dfCap[!, :StartCapWind]), + RetCapWind = sum(dfCap[!, :RetCapWind]), + NewCapWind = sum(dfCap[!, :NewCapWind]), EndCapWind = sum(dfCap[!, :EndCapWind]), + StartCapDC = sum(dfCap[!, :StartCapDC]), RetCapDC = sum(dfCap[!, :RetCapDC]), + NewCapDC = sum(dfCap[!, :NewCapDC]), EndCapDC = sum(dfCap[!, :EndCapDC]), + StartCapGrid = sum(dfCap[!, :StartCapGrid]), + RetCapGrid = sum(dfCap[!, :RetCapGrid]), + NewCapGrid = sum(dfCap[!, :NewCapGrid]), EndCapGrid = sum(dfCap[!, :EndCapGrid]), + StartEnergyCap = sum(dfCap[!, :StartEnergyCap]), + RetEnergyCap = sum(dfCap[!, :RetEnergyCap]), + NewEnergyCap = sum(dfCap[!, :NewEnergyCap]), + EndEnergyCap = sum(dfCap[!, :EndEnergyCap]), + StartChargeACCap = sum(dfCap[!, :StartChargeACCap]), + RetChargeACCap = sum(dfCap[!, :RetChargeACCap]), + NewChargeACCap = sum(dfCap[!, :NewChargeACCap]), + EndChargeACCap = sum(dfCap[!, :EndChargeACCap]), + StartChargeDCCap = sum(dfCap[!, :StartChargeDCCap]), + RetChargeDCCap = sum(dfCap[!, :RetChargeDCCap]), + NewChargeDCCap = sum(dfCap[!, :NewChargeDCCap]), + EndChargeDCCap = sum(dfCap[!, :EndChargeDCCap]), + StartDischargeDCCap = sum(dfCap[!, :StartDischargeDCCap]), + RetDischargeDCCap = sum(dfCap[!, :RetDischargeDCCap]), + NewDischargeDCCap = sum(dfCap[!, :NewDischargeDCCap]), + EndDischargeDCCap = sum(dfCap[!, :EndDischargeDCCap]), + StartDischargeACCap = sum(dfCap[!, :StartDischargeACCap]), + RetDischargeACCap = sum(dfCap[!, :RetDischargeACCap]), + NewDischargeACCap = sum(dfCap[!, :NewDischargeACCap]), + EndDischargeACCap = sum(dfCap[!, :EndDischargeACCap])) + + dfCap = vcat(dfCap, total) + CSV.write(joinpath(path, "vre_stor_capacity.csv"), dfCap) + return dfCap end @doc raw""" @@ -287,43 +314,49 @@ end Function for writing the vre-storage charging decision variables/expressions. """ function write_vre_stor_charge(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - gen = inputs["RESOURCES"] - gen_VRE_STOR = gen.VreStorage - T = inputs["T"] + gen = inputs["RESOURCES"] + gen_VRE_STOR = gen.VreStorage + T = inputs["T"] DC_CHARGE = inputs["VS_STOR_DC_CHARGE"] AC_CHARGE = inputs["VS_STOR_AC_CHARGE"] - # DC charging of battery dataframe - if !isempty(DC_CHARGE) - dfCharge_DC = DataFrame(Resource = inputs["RESOURCE_NAMES_DC_CHARGE"], Zone = inputs["ZONES_DC_CHARGE"], AnnualSum = Array{Union{Missing,Float32}}(undef, size(DC_CHARGE)[1])) - charge_dc = zeros(size(DC_CHARGE)[1], T) - charge_dc = value.(EP[:vP_DC_CHARGE]).data ./ etainverter.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_discharge.!=0)]) * (setup["ParameterScale"]==1 ? ModelScalingFactor : 1) - dfCharge_DC.AnnualSum .= charge_dc * inputs["omega"] - - - filepath = joinpath(path,"vre_stor_dc_charge.csv") - if setup["WriteOutputs"] == "annual" - write_annual(filepath, dfCharge_DC) - else # setup["WriteOutputs"] == "full" - write_fulltimeseries(filepath, charge_dc, dfCharge_DC) - end - end - - # AC charging of battery dataframe - if !isempty(AC_CHARGE) - dfCharge_AC = DataFrame(Resource = inputs["RESOURCE_NAMES_AC_CHARGE"], Zone = inputs["ZONES_AC_CHARGE"], AnnualSum = Array{Union{Missing,Float32}}(undef, size(AC_CHARGE)[1])) - charge_ac = zeros(size(AC_CHARGE)[1], T) - charge_ac = value.(EP[:vP_AC_CHARGE]).data * (setup["ParameterScale"]==1 ? ModelScalingFactor : 1) - dfCharge_AC.AnnualSum .= charge_ac * inputs["omega"] - - filepath = joinpath(path,"vre_stor_ac_charge.csv") - if setup["WriteOutputs"] == "annual" - write_annual(filepath, dfCharge_AC) - else # setup["WriteOutputs"] == "full" - write_fulltimeseries(filepath, charge_ac, dfCharge_AC) - end - end - return nothing + # DC charging of battery dataframe + if !isempty(DC_CHARGE) + dfCharge_DC = DataFrame(Resource = inputs["RESOURCE_NAMES_DC_CHARGE"], + Zone = inputs["ZONES_DC_CHARGE"], + AnnualSum = Array{Union{Missing, Float32}}(undef, size(DC_CHARGE)[1])) + charge_dc = zeros(size(DC_CHARGE)[1], T) + charge_dc = value.(EP[:vP_DC_CHARGE]).data ./ + etainverter.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_discharge .!= 0)]) * + (setup["ParameterScale"] == 1 ? ModelScalingFactor : 1) + dfCharge_DC.AnnualSum .= charge_dc * inputs["omega"] + + filepath = joinpath(path, "vre_stor_dc_charge.csv") + if setup["WriteOutputs"] == "annual" + write_annual(filepath, dfCharge_DC) + else # setup["WriteOutputs"] == "full" + write_fulltimeseries(filepath, charge_dc, dfCharge_DC) + end + end + + # AC charging of battery dataframe + if !isempty(AC_CHARGE) + dfCharge_AC = DataFrame(Resource = inputs["RESOURCE_NAMES_AC_CHARGE"], + Zone = inputs["ZONES_AC_CHARGE"], + AnnualSum = Array{Union{Missing, Float32}}(undef, size(AC_CHARGE)[1])) + charge_ac = zeros(size(AC_CHARGE)[1], T) + charge_ac = value.(EP[:vP_AC_CHARGE]).data * + (setup["ParameterScale"] == 1 ? ModelScalingFactor : 1) + dfCharge_AC.AnnualSum .= charge_ac * inputs["omega"] + + filepath = joinpath(path, "vre_stor_ac_charge.csv") + if setup["WriteOutputs"] == "annual" + write_annual(filepath, dfCharge_AC) + else # setup["WriteOutputs"] == "full" + write_fulltimeseries(filepath, charge_ac, dfCharge_AC) + end + end + return nothing end @doc raw""" @@ -331,81 +364,94 @@ end Function for writing the vre-storage discharging decision variables/expressions. """ -function write_vre_stor_discharge(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) - gen = inputs["RESOURCES"] - gen_VRE_STOR = gen.VreStorage - T = inputs["T"] - DC_DISCHARGE = inputs["VS_STOR_DC_DISCHARGE"] +function write_vre_stor_discharge(path::AbstractString, + inputs::Dict, + setup::Dict, + EP::Model) + gen = inputs["RESOURCES"] + gen_VRE_STOR = gen.VreStorage + T = inputs["T"] + DC_DISCHARGE = inputs["VS_STOR_DC_DISCHARGE"] AC_DISCHARGE = inputs["VS_STOR_AC_DISCHARGE"] - WIND = inputs["VS_WIND"] - SOLAR = inputs["VS_SOLAR"] - - # DC discharging of battery dataframe - if !isempty(DC_DISCHARGE) - dfDischarge_DC = DataFrame(Resource = inputs["RESOURCE_NAMES_DC_DISCHARGE"], Zone = inputs["ZONES_DC_DISCHARGE"], AnnualSum = Array{Union{Missing,Float32}}(undef, size(DC_DISCHARGE)[1])) - power_vre_stor = value.(EP[:vP_DC_DISCHARGE]).data .* etainverter.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_discharge.!=0)]) - if setup["ParameterScale"] == 1 - power_vre_stor *= ModelScalingFactor - end - dfDischarge_DC.AnnualSum .= power_vre_stor * inputs["omega"] - - filepath = joinpath(path,"vre_stor_dc_discharge.csv") - if setup["WriteOutputs"] == "annual" - write_annual(filepath, dfDischarge_DC) - else # setup["WriteOutputs"] == "full" - write_fulltimeseries(filepath, power_vre_stor, dfDischarge_DC) - end - end - - # AC discharging of battery dataframe - if !isempty(AC_DISCHARGE) - dfDischarge_AC = DataFrame(Resource = inputs["RESOURCE_NAMES_AC_DISCHARGE"], Zone = inputs["ZONES_AC_DISCHARGE"], AnnualSum = Array{Union{Missing,Float32}}(undef, size(AC_DISCHARGE)[1])) - power_vre_stor = value.(EP[:vP_AC_DISCHARGE]).data - if setup["ParameterScale"] == 1 - power_vre_stor *= ModelScalingFactor - end - dfDischarge_AC.AnnualSum .= power_vre_stor * inputs["omega"] - - filepath = joinpath(path,"vre_stor_ac_discharge.csv") - if setup["WriteOutputs"] == "annual" - write_annual(filepath, dfDischarge_AC) - else # setup["WriteOutputs"] == "full" - write_fulltimeseries(filepath, power_vre_stor, dfDischarge_AC) - end - end - - # Wind generation of co-located resource dataframe - if !isempty(WIND) - dfVP_VRE_STOR = DataFrame(Resource = inputs["RESOURCE_NAMES_WIND"], Zone = inputs["ZONES_WIND"], AnnualSum = Array{Union{Missing,Float32}}(undef, size(WIND)[1])) - vre_vre_stor = value.(EP[:vP_WIND]).data - if setup["ParameterScale"] == 1 - vre_vre_stor *= ModelScalingFactor - end - dfVP_VRE_STOR.AnnualSum .= vre_vre_stor * inputs["omega"] - - filepath = joinpath(path,"vre_stor_wind_power.csv") - if setup["WriteOutputs"] == "annual" - write_annual(filepath, dfVP_VRE_STOR) - else # setup["WriteOutputs"] == "full" - write_fulltimeseries(filepath, vre_vre_stor, dfVP_VRE_STOR) - end - end - - # Solar generation of co-located resource dataframe - if !isempty(SOLAR) - dfVP_VRE_STOR = DataFrame(Resource = inputs["RESOURCE_NAMES_SOLAR"], Zone = inputs["ZONES_SOLAR"], AnnualSum = Array{Union{Missing,Float32}}(undef, size(SOLAR)[1])) - vre_vre_stor = value.(EP[:vP_SOLAR]).data .* etainverter.(gen_VRE_STOR[(gen_VRE_STOR.solar.!=0)]) - if setup["ParameterScale"] == 1 - vre_vre_stor *= ModelScalingFactor - end - dfVP_VRE_STOR.AnnualSum .= vre_vre_stor * inputs["omega"] - - filepath = joinpath(path,"vre_stor_solar_power.csv") - if setup["WriteOutputs"] == "annual" - write_annual(filepath, dfVP_VRE_STOR) - else # setup["WriteOutputs"] == "full" - write_fulltimeseries(filepath, vre_vre_stor, dfVP_VRE_STOR) - end - end - return nothing + WIND = inputs["VS_WIND"] + SOLAR = inputs["VS_SOLAR"] + + # DC discharging of battery dataframe + if !isempty(DC_DISCHARGE) + dfDischarge_DC = DataFrame(Resource = inputs["RESOURCE_NAMES_DC_DISCHARGE"], + Zone = inputs["ZONES_DC_DISCHARGE"], + AnnualSum = Array{Union{Missing, Float32}}(undef, size(DC_DISCHARGE)[1])) + power_vre_stor = value.(EP[:vP_DC_DISCHARGE]).data .* + etainverter.(gen_VRE_STOR[(gen_VRE_STOR.stor_dc_discharge .!= 0)]) + if setup["ParameterScale"] == 1 + power_vre_stor *= ModelScalingFactor + end + dfDischarge_DC.AnnualSum .= power_vre_stor * inputs["omega"] + + filepath = joinpath(path, "vre_stor_dc_discharge.csv") + if setup["WriteOutputs"] == "annual" + write_annual(filepath, dfDischarge_DC) + else # setup["WriteOutputs"] == "full" + write_fulltimeseries(filepath, power_vre_stor, dfDischarge_DC) + end + end + + # AC discharging of battery dataframe + if !isempty(AC_DISCHARGE) + dfDischarge_AC = DataFrame(Resource = inputs["RESOURCE_NAMES_AC_DISCHARGE"], + Zone = inputs["ZONES_AC_DISCHARGE"], + AnnualSum = Array{Union{Missing, Float32}}(undef, size(AC_DISCHARGE)[1])) + power_vre_stor = value.(EP[:vP_AC_DISCHARGE]).data + if setup["ParameterScale"] == 1 + power_vre_stor *= ModelScalingFactor + end + dfDischarge_AC.AnnualSum .= power_vre_stor * inputs["omega"] + + filepath = joinpath(path, "vre_stor_ac_discharge.csv") + if setup["WriteOutputs"] == "annual" + write_annual(filepath, dfDischarge_AC) + else # setup["WriteOutputs"] == "full" + write_fulltimeseries(filepath, power_vre_stor, dfDischarge_AC) + end + end + + # Wind generation of co-located resource dataframe + if !isempty(WIND) + dfVP_VRE_STOR = DataFrame(Resource = inputs["RESOURCE_NAMES_WIND"], + Zone = inputs["ZONES_WIND"], + AnnualSum = Array{Union{Missing, Float32}}(undef, size(WIND)[1])) + vre_vre_stor = value.(EP[:vP_WIND]).data + if setup["ParameterScale"] == 1 + vre_vre_stor *= ModelScalingFactor + end + dfVP_VRE_STOR.AnnualSum .= vre_vre_stor * inputs["omega"] + + filepath = joinpath(path, "vre_stor_wind_power.csv") + if setup["WriteOutputs"] == "annual" + write_annual(filepath, dfVP_VRE_STOR) + else # setup["WriteOutputs"] == "full" + write_fulltimeseries(filepath, vre_vre_stor, dfVP_VRE_STOR) + end + end + + # Solar generation of co-located resource dataframe + if !isempty(SOLAR) + dfVP_VRE_STOR = DataFrame(Resource = inputs["RESOURCE_NAMES_SOLAR"], + Zone = inputs["ZONES_SOLAR"], + AnnualSum = Array{Union{Missing, Float32}}(undef, size(SOLAR)[1])) + vre_vre_stor = value.(EP[:vP_SOLAR]).data .* + etainverter.(gen_VRE_STOR[(gen_VRE_STOR.solar .!= 0)]) + if setup["ParameterScale"] == 1 + vre_vre_stor *= ModelScalingFactor + end + dfVP_VRE_STOR.AnnualSum .= vre_vre_stor * inputs["omega"] + + filepath = joinpath(path, "vre_stor_solar_power.csv") + if setup["WriteOutputs"] == "annual" + write_annual(filepath, dfVP_VRE_STOR) + else # setup["WriteOutputs"] == "full" + write_fulltimeseries(filepath, vre_vre_stor, dfVP_VRE_STOR) + end + end + return nothing end diff --git a/test/expression_manipulation_test.jl b/test/expression_manipulation_test.jl index aae5d442ec..71891d80ac 100644 --- a/test/expression_manipulation_test.jl +++ b/test/expression_manipulation_test.jl @@ -3,11 +3,11 @@ using HiGHS function setup_sum_model() EP = Model(HiGHS.Optimizer) - @variable(EP, x[i=1:100,j=1:4:200]>=0) - @variable(EP, y[i=1:100,j=1:50]>=0) - @expression(EP, eX[i=1:100,j=1:4:200], 2.0*x[i,j]+i+10.0*j) - @expression(EP, eY[i=1:100,j=1:50], 3.0*y[i,j]+2*i+j) - @expression(EP, eZ[i=1:100,j=1:50], 2.0*x[i,(j-1)*4+1] + 4.0*y[i,j]) + @variable(EP, x[i = 1:100, j = 1:4:200]>=0) + @variable(EP, y[i = 1:100, j = 1:50]>=0) + @expression(EP, eX[i = 1:100, j = 1:4:200], 2.0*x[i, j]+i+10.0*j) + @expression(EP, eY[i = 1:100, j = 1:50], 3.0*y[i, j]+2*i+j) + @expression(EP, eZ[i = 1:100, j = 1:50], 2.0 * x[i, (j - 1) * 4 + 1]+4.0 * y[i, j]) return EP end @@ -61,21 +61,21 @@ function sum_combo_expr() return true end -let +let EP = Model(HiGHS.Optimizer) # Test fill_with_zeros! - small_zeros_expr = Array{AffExpr,2}(undef,(2,3)) + small_zeros_expr = Array{AffExpr, 2}(undef, (2, 3)) GenX.fill_with_zeros!(small_zeros_expr) @test small_zeros_expr == AffExpr.([0.0 0.0 0.0; 0.0 0.0 0.0]) # Test fill_with_const! - small_const_expr = Array{AffExpr,2}(undef,(3,2)) + small_const_expr = Array{AffExpr, 2}(undef, (3, 2)) GenX.fill_with_const!(small_const_expr, 6.0) @test small_const_expr == AffExpr.([6.0 6.0; 6.0 6.0; 6.0 6.0]) # Test create_empty_expression! with fill_with_const! - large_dims = (2,10,20) + large_dims = (2, 10, 20) GenX.create_empty_expression!(EP, :large_expr, large_dims) @test all(EP[:large_expr] .== 0.0) @@ -93,11 +93,12 @@ let @test all(EP[:large_expr][:] .== 18.0) # Test add_similar_to_expression! returns an error if the dimensions don't match - GenX.create_empty_expression!(EP, :small_expr, (2,3)) - @test_throws ErrorException GenX.add_similar_to_expression!(EP[:large_expr], EP[:small_expr]) + GenX.create_empty_expression!(EP, :small_expr, (2, 3)) + @test_throws ErrorException GenX.add_similar_to_expression!(EP[:large_expr], + EP[:small_expr]) # Test we can add variables to an expression using add_similar_to_expression! - @variable(EP, test_var[1:large_dims[1], 1:large_dims[2], 1:large_dims[3]] >= 0) + @variable(EP, test_var[1:large_dims[1], 1:large_dims[2], 1:large_dims[3]]>=0) GenX.add_similar_to_expression!(EP[:large_expr], test_var) @test EP[:large_expr][100] == test_var[100] + 18.0 @@ -117,7 +118,7 @@ let @test sum_combo_expr() == true # Test add_term_to_expression! for variable - @variable(EP, single_var >= 0) + @variable(EP, single_var>=0) GenX.add_term_to_expression!(EP[:large_expr], single_var) @test EP[:large_expr][100] == test_var[100] + 22.0 + single_var @@ -144,12 +145,12 @@ let unregister(EP, :var_denseaxisarray) end - ###### ###### ###### ###### ###### ###### ###### - ###### ###### ###### ###### ###### ###### ###### - # Performance tests we can perhaps add later - # These require the BenchmarkTests.jl package - ###### ###### ###### ###### ###### ###### ###### - ###### ###### ###### ###### ###### ###### ###### +###### ###### ###### ###### ###### ###### ###### +###### ###### ###### ###### ###### ###### ###### +# Performance tests we can perhaps add later +# These require the BenchmarkTests.jl package +###### ###### ###### ###### ###### ###### ###### +###### ###### ###### ###### ###### ###### ###### # function test_performance(expr_dims) # EP = Model(HiGHS.Optimizer) @@ -165,4 +166,3 @@ end # small_benchmark = test_performance((2,3)) # medium_benchmark = test_performance((2,10,20)) # large_benchmark = test_performance((2,20,40)) - diff --git a/test/resource_test.jl b/test/resource_test.jl index 2203af17e8..7824187bfa 100644 --- a/test/resource_test.jl +++ b/test/resource_test.jl @@ -3,109 +3,109 @@ let check_resource = GenX.check_resource therm = Resource(:Resource => "my_therm", - :THERM => 1, - :FLEX => 0, - :HYDRO => 0, - :VRE => 0, - :MUST_RUN => 0, - :STOR => 0, - :LDS => 0) + :THERM => 1, + :FLEX => 0, + :HYDRO => 0, + :VRE => 0, + :MUST_RUN => 0, + :STOR => 0, + :LDS => 0) stor_lds = Resource(:Resource => "stor_lds", - :THERM => 0, - :FLEX => 0, - :HYDRO => 0, - :VRE => 0, - :MUST_RUN => 0, - :STOR => 1, - :LDS => 1) + :THERM => 0, + :FLEX => 0, + :HYDRO => 0, + :VRE => 0, + :MUST_RUN => 0, + :STOR => 1, + :LDS => 1) hydro_lds = Resource(:Resource => "hydro_lds", - :THERM => 0, - :FLEX => 0, - :HYDRO => 1, - :VRE => 0, - :MUST_RUN => 0, - :STOR => 0, - :LDS => 1) + :THERM => 0, + :FLEX => 0, + :HYDRO => 1, + :VRE => 0, + :MUST_RUN => 0, + :STOR => 0, + :LDS => 1) bad_lds = Resource(:Resource => "bad lds combo", - :THERM => 0, - :FLEX => 1, - :HYDRO => 0, - :VRE => 0, - :MUST_RUN => 0, - :STOR => 0, - :LDS => 1) + :THERM => 0, + :FLEX => 1, + :HYDRO => 0, + :VRE => 0, + :MUST_RUN => 0, + :STOR => 0, + :LDS => 1) bad_none = Resource(:Resource => "none", - :THERM => 0, - :FLEX => 0, - :HYDRO => 0, - :VRE => 0, - :MUST_RUN => 0, - :STOR => 0, - :LDS => 0) + :THERM => 0, + :FLEX => 0, + :HYDRO => 0, + :VRE => 0, + :MUST_RUN => 0, + :STOR => 0, + :LDS => 0) bad_twotypes = Resource(:Resource => "too many", - :THERM => 1, - :FLEX => 1, - :HYDRO => 0, - :VRE => 0, - :MUST_RUN => 0, - :STOR => 0, - :LDS => 0) + :THERM => 1, + :FLEX => 1, + :HYDRO => 0, + :VRE => 0, + :MUST_RUN => 0, + :STOR => 0, + :LDS => 0) bad_multiple = Resource(:Resource => "multiple_bad", - :THERM => 1, - :FLEX => 1, - :HYDRO => 0, - :VRE => 0, - :MUST_RUN => 0, - :STOR => 0, - :LDS => 1) + :THERM => 1, + :FLEX => 1, + :HYDRO => 0, + :VRE => 0, + :MUST_RUN => 0, + :STOR => 0, + :LDS => 1) # MUST_RUN units contribution to reserves must_run = Resource(:Resource => "must_run", - :THERM => 0, - :FLEX => 0, - :HYDRO => 0, - :VRE => 0, - :MUST_RUN => 1, - :STOR => 0, - :LDS => 0, - :Reg_Max => 0, - :Rsv_Max => 0) + :THERM => 0, + :FLEX => 0, + :HYDRO => 0, + :VRE => 0, + :MUST_RUN => 1, + :STOR => 0, + :LDS => 0, + :Reg_Max => 0, + :Rsv_Max => 0) bad_must_run = Resource(:Resource => "bad_must_run", - :THERM => 0, - :FLEX => 0, - :HYDRO => 0, - :VRE => 0, - :MUST_RUN => 1, - :STOR => 0, - :LDS => 0, - :Reg_Max => 0.083333333, - :Rsv_Max => 0.166666667) + :THERM => 0, + :FLEX => 0, + :HYDRO => 0, + :VRE => 0, + :MUST_RUN => 1, + :STOR => 0, + :LDS => 0, + :Reg_Max => 0.083333333, + :Rsv_Max => 0.166666667) bad_mustrun_reg = Resource(:Resource => "bad_mustrun_reg", - :THERM => 0, - :FLEX => 0, - :HYDRO => 0, - :VRE => 0, - :MUST_RUN => 1, - :STOR => 0, - :LDS => 0, - :Reg_Max => 0.083333333, - :Rsv_Max => 0) + :THERM => 0, + :FLEX => 0, + :HYDRO => 0, + :VRE => 0, + :MUST_RUN => 1, + :STOR => 0, + :LDS => 0, + :Reg_Max => 0.083333333, + :Rsv_Max => 0) bad_mustrun_rsv = Resource(:Resource => "bad_mustrun_rsv", - :THERM => 0, - :FLEX => 0, - :HYDRO => 0, - :VRE => 0, - :MUST_RUN => 1, - :STOR => 0, - :LDS => 0, - :Reg_Max => 0, - :Rsv_Max => 0.166666667) + :THERM => 0, + :FLEX => 0, + :HYDRO => 0, + :VRE => 0, + :MUST_RUN => 1, + :STOR => 0, + :LDS => 0, + :Reg_Max => 0, + :Rsv_Max => 0.166666667) function check_okay(resource) e = check_resource(resource) @@ -143,6 +143,4 @@ let end test_validate_bad(multiple_bad_resources) - - end diff --git a/test/runtests.jl b/test/runtests.jl index 1722acabce..ff624f632e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,7 +4,6 @@ using Logging include("utilities.jl") - @testset "Expr manipulation" begin include("expression_manipulation_test.jl") end @@ -16,7 +15,7 @@ if VERSION ≥ v"1.7" end # Test GenX modules -@testset verbose = true "GenX modules" begin +@testset verbose=true "GenX modules" begin @testset "Three zones" begin include("test_threezones.jl") end diff --git a/test/test_DCOPF.jl b/test/test_DCOPF.jl index bbac42ff62..ca15bef686 100644 --- a/test/test_DCOPF.jl +++ b/test/test_DCOPF.jl @@ -8,11 +8,9 @@ obj_true = 395.171391 test_path = "DCOPF" # Define test inputs -genx_setup = Dict( - "Trans_Loss_Segments" => 0, +genx_setup = Dict("Trans_Loss_Segments" => 0, "StorageLosses" => 0, - "DC_OPF" => 1, -) + "DC_OPF" => 1) # Run the case and get the objective value and tolerance EP, _, _ = redirect_stdout(devnull) do @@ -23,11 +21,11 @@ optimal_tol_rel = get_attribute(EP, "ipm_optimality_tolerance") optimal_tol = optimal_tol_rel * obj_test # Convert to absolute tolerance # Test the objective value -test_result = @test obj_test ≈ obj_true atol = optimal_tol +test_result = @test obj_test≈obj_true atol=optimal_tol # Round objective value and tolerance. Write to test log. obj_test = round_from_tol!(obj_test, optimal_tol) optimal_tol = round_from_tol!(optimal_tol, optimal_tol) write_testlog(test_path, obj_test, optimal_tol, test_result) -end # module TestDCOPF \ No newline at end of file +end # module TestDCOPF diff --git a/test/test_VRE_storage.jl b/test/test_VRE_storage.jl index 10ce1b9d31..2765283f6e 100644 --- a/test/test_VRE_storage.jl +++ b/test/test_VRE_storage.jl @@ -7,8 +7,7 @@ obj_true = 92376.060123 test_path = "VRE_storage" # Define test inputs -genx_setup = Dict( - "NetworkExpansion" => 1, +genx_setup = Dict("NetworkExpansion" => 1, "UCommit" => 2, "CapacityReserveMargin" => 1, "MinCapReq" => 1, @@ -16,8 +15,7 @@ genx_setup = Dict( "CO2Cap" => 1, "StorageLosses" => 1, "VirtualChargeDischargeCost" => 1, - "ParameterScale" => 1, -) + "ParameterScale" => 1) # Run the case and get the objective value and tolerance EP, inputs, _ = redirect_stdout(devnull) do @@ -28,7 +26,7 @@ optimal_tol_rel = get_attribute(EP, "dual_feasibility_tolerance") optimal_tol = optimal_tol_rel * obj_test # Convert to absolute tolerance # Test the objective value -test_result = @test obj_test ≈ obj_true atol = optimal_tol +test_result = @test obj_test≈obj_true atol=optimal_tol # Round objective value and tolerance. Write to test log. obj_test = round_from_tol!(obj_test, optimal_tol) diff --git a/test/test_compute_conflicts.jl b/test/test_compute_conflicts.jl index c8c4c88f72..e02780eec8 100644 --- a/test/test_compute_conflicts.jl +++ b/test/test_compute_conflicts.jl @@ -3,22 +3,22 @@ module TestConflicts using Test include(joinpath(@__DIR__, "utilities.jl")) -test_path = joinpath(@__DIR__,"compute_conflicts"); +test_path = joinpath(@__DIR__, "compute_conflicts") # Define test inputs -genx_setup = Dict{Any,Any}( - "Trans_Loss_Segments" => 1, +genx_setup = Dict{Any, Any}("Trans_Loss_Segments" => 1, "CO2Cap" => 1, "StorageLosses" => 1, "MaxCapReq" => 1, - "ComputeConflicts" => 1 -) + "ComputeConflicts" => 1) genxoutput = redirect_stdout(devnull) do run_genx_case_conflict_testing(test_path, genx_setup) end -test_result = @test length(genxoutput)==2 -write_testlog(test_path,"Testing that the infeasible model is correctly handled",test_result) +test_result = @test length(genxoutput) == 2 +write_testlog(test_path, + "Testing that the infeasible model is correctly handled", + test_result) -end \ No newline at end of file +end diff --git a/test/test_electrolyzer.jl b/test/test_electrolyzer.jl index 426540eef1..7789751e86 100644 --- a/test/test_electrolyzer.jl +++ b/test/test_electrolyzer.jl @@ -8,13 +8,11 @@ obj_true = 6946.9618 test_path = "electrolyzer" # Define test inputs -genx_setup = Dict( - "Trans_Loss_Segments" => 1, +genx_setup = Dict("Trans_Loss_Segments" => 1, "UCommit" => 2, "StorageLosses" => 1, "HydrogenHourlyMatching" => 1, - "ParameterScale" => 1, -) + "ParameterScale" => 1) # Run the case and get the objective value and tolerance EP, _, _ = redirect_stdout(devnull) do @@ -26,7 +24,7 @@ optimal_tol_rel = get_attribute(EP, "ipm_optimality_tolerance") optimal_tol = optimal_tol_rel * obj_test # Convert to absolute tolerance # Test the objective value -test_result = @test obj_test ≈ obj_true atol = optimal_tol +test_result = @test obj_test≈obj_true atol=optimal_tol # Round objective value and tolerance. Write to test log. obj_test = round_from_tol!(obj_test, optimal_tol) diff --git a/test/test_examples.jl b/test/test_examples.jl index ed060f9ca1..a15b5ef17b 100644 --- a/test/test_examples.jl +++ b/test/test_examples.jl @@ -5,13 +5,12 @@ using GenX include(joinpath(@__DIR__, "utilities.jl")) - # Test that the examples in the example_systems directory run without error function test_examples() base_path = Base.dirname(Base.dirname(pathof(GenX))) examples_path = joinpath(base_path, "example_systems") - examples_dir = readdir(examples_path, join=true) + examples_dir = readdir(examples_path, join = true) for example_dir in examples_dir if isdir(example_dir) && isfile(joinpath(example_dir, "Run.jl")) @info "Running example in $example_dir" @@ -25,4 +24,4 @@ end test_examples() end -end # module \ No newline at end of file +end # module diff --git a/test/test_load_resource_data.jl b/test/test_load_resource_data.jl index fec45f8c8e..d03674fffe 100644 --- a/test/test_load_resource_data.jl +++ b/test/test_load_resource_data.jl @@ -11,32 +11,33 @@ struct InputsTrue inputs_filename::AbstractString end - function test_macro_interface(attr::Symbol, gen, dfGen) f = getfield(GenX, attr) @test f.(gen) == dfGen[!, attr] end function test_ids_with(attr::Symbol, gen, dfGen) - @test GenX.ids_with(gen,attr) == dfGen[dfGen[!, attr] .!= 0, :r_id] + @test GenX.ids_with(gen, attr) == dfGen[dfGen[!, attr] .!= 0, :r_id] end function test_ids_with_nonneg(attr::Symbol, gen, dfGen) - @test GenX.ids_with_nonneg(gen,attr) == dfGen[dfGen[!, attr] .>= 0, :r_id] + @test GenX.ids_with_nonneg(gen, attr) == dfGen[dfGen[!, attr] .>= 0, :r_id] end function test_ids_with_positive(attr::Symbol, gen, dfGen) - @test GenX.ids_with_positive(gen,attr) == dfGen[dfGen[!, attr] .> 0, :r_id] + @test GenX.ids_with_positive(gen, attr) == dfGen[dfGen[!, attr] .> 0, :r_id] end -function prepare_inputs_true(test_path::AbstractString, in_filenames::InputsTrue, setup::Dict) +function prepare_inputs_true(test_path::AbstractString, + in_filenames::InputsTrue, + setup::Dict) gen_filename = in_filenames.gen_filename inputs_filename = in_filenames.inputs_filename dfGen = GenX.load_dataframe(joinpath(test_path, gen_filename)) - scale_factor = setup["ParameterScale"] == 1 ? GenX.ModelScalingFactor : 1. + scale_factor = setup["ParameterScale"] == 1 ? GenX.ModelScalingFactor : 1.0 GenX.rename!(dfGen, lowercase.(names(dfGen))) GenX.scale_resources_data!(dfGen, scale_factor) - dfGen[!,:r_id] = 1:size(dfGen,1) + dfGen[!, :r_id] = 1:size(dfGen, 1) inputs_true = load(joinpath(test_path, inputs_filename)) return dfGen, inputs_true end @@ -88,27 +89,44 @@ function test_load_scaled_resources_data(gen, dfGen) @test GenX.fuel.(gen) == dfGen.fuel @test GenX.co2_capture_fraction.(gen) == dfGen.co2_capture_fraction @test GenX.co2_capture_fraction_startup.(gen) == dfGen.co2_capture_fraction_startup - @test GenX.ccs_disposal_cost_per_metric_ton.(gen) == dfGen.ccs_disposal_cost_per_metric_ton + @test GenX.ccs_disposal_cost_per_metric_ton.(gen) == + dfGen.ccs_disposal_cost_per_metric_ton @test GenX.biomass.(gen) == dfGen.biomass ## multi-fuel flags - @test GenX.ids_with_fuel(gen) == dfGen[(dfGen[!,:fuel] .!= "None"),:r_id] - @test GenX.ids_with_positive(gen, GenX.co2_capture_fraction) == dfGen[dfGen.co2_capture_fraction .>0,:r_id] - @test GenX.ids_with_singlefuel(gen) == dfGen[dfGen.multi_fuels.!=1,:r_id] - @test GenX.ids_with_multifuels(gen) == dfGen[dfGen.multi_fuels.==1,:r_id] + @test GenX.ids_with_fuel(gen) == dfGen[(dfGen[!, :fuel] .!= "None"), :r_id] + @test GenX.ids_with_positive(gen, GenX.co2_capture_fraction) == + dfGen[dfGen.co2_capture_fraction .> 0, :r_id] + @test GenX.ids_with_singlefuel(gen) == dfGen[dfGen.multi_fuels .!= 1, :r_id] + @test GenX.ids_with_multifuels(gen) == dfGen[dfGen.multi_fuels .== 1, :r_id] if !isempty(GenX.ids_with_multifuels(gen)) MULTI_FUELS = GenX.ids_with_multifuels(gen) max_fuels = maximum(GenX.num_fuels.(gen)) for i in 1:max_fuels - @test findall(g -> GenX.max_cofire_cols(g, tag=i) < 1, gen[MULTI_FUELS]) == dfGen[dfGen[!, Symbol(string("fuel",i, "_max_cofire_level"))].< 1, :][!, :r_id] - @test findall(g -> GenX.max_cofire_start_cols(g, tag=i) < 1, gen[MULTI_FUELS]) == dfGen[dfGen[!, Symbol(string("fuel",i, "_max_cofire_level_start"))].< 1, :][!, :r_id] - @test findall(g -> GenX.min_cofire_cols(g, tag=i) > 0, gen[MULTI_FUELS]) == dfGen[dfGen[!, Symbol(string("fuel",i, "_min_cofire_level"))].> 0, :][!, :r_id] - @test findall(g -> GenX.min_cofire_start_cols(g, tag=i) > 0, gen[MULTI_FUELS]) == dfGen[dfGen[!, Symbol(string("fuel",i, "_min_cofire_level_start"))].> 0, :][!, :r_id] - @test GenX.fuel_cols.(gen, tag=i) == dfGen[!,Symbol(string("fuel",i))] - @test GenX.heat_rate_cols.(gen, tag=i) == dfGen[!,Symbol(string("heat_rate",i, "_mmbtu_per_mwh"))] - @test GenX.max_cofire_cols.(gen, tag=i) == dfGen[!,Symbol(string("fuel",i, "_max_cofire_level"))] - @test GenX.min_cofire_cols.(gen, tag=i) == dfGen[!,Symbol(string("fuel",i, "_min_cofire_level"))] - @test GenX.max_cofire_start_cols.(gen, tag=i) == dfGen[!,Symbol(string("fuel",i, "_max_cofire_level_start"))] - @test GenX.min_cofire_start_cols.(gen, tag=i) == dfGen[!,Symbol(string("fuel",i, "_min_cofire_level_start"))] + @test findall(g -> GenX.max_cofire_cols(g, tag = i) < 1, gen[MULTI_FUELS]) == + dfGen[dfGen[!, Symbol(string("fuel", i, "_max_cofire_level"))] .< 1, :][!, + :r_id] + @test findall(g -> GenX.max_cofire_start_cols(g, tag = i) < 1, + gen[MULTI_FUELS]) == dfGen[dfGen[!, Symbol(string("fuel", i, "_max_cofire_level_start"))] .< 1, + :][!, + :r_id] + @test findall(g -> GenX.min_cofire_cols(g, tag = i) > 0, gen[MULTI_FUELS]) == + dfGen[dfGen[!, Symbol(string("fuel", i, "_min_cofire_level"))] .> 0, :][!, + :r_id] + @test findall(g -> GenX.min_cofire_start_cols(g, tag = i) > 0, + gen[MULTI_FUELS]) == dfGen[dfGen[!, Symbol(string("fuel", i, "_min_cofire_level_start"))] .> 0, + :][!, + :r_id] + @test GenX.fuel_cols.(gen, tag = i) == dfGen[!, Symbol(string("fuel", i))] + @test GenX.heat_rate_cols.(gen, tag = i) == + dfGen[!, Symbol(string("heat_rate", i, "_mmbtu_per_mwh"))] + @test GenX.max_cofire_cols.(gen, tag = i) == + dfGen[!, Symbol(string("fuel", i, "_max_cofire_level"))] + @test GenX.min_cofire_cols.(gen, tag = i) == + dfGen[!, Symbol(string("fuel", i, "_min_cofire_level"))] + @test GenX.max_cofire_start_cols.(gen, tag = i) == + dfGen[!, Symbol(string("fuel", i, "_max_cofire_level_start"))] + @test GenX.min_cofire_start_cols.(gen, tag = i) == + dfGen[!, Symbol(string("fuel", i, "_min_cofire_level_start"))] end end @test GenX.ids_with_mga(gen) == dfGen[dfGen.mga .== 1, :r_id] @@ -118,12 +136,12 @@ function test_load_scaled_resources_data(gen, dfGen) end function test_add_policies_to_resources(gen, dfGen) - @test GenX.esr.(gen, tag=1) == dfGen.esr_1 - @test GenX.esr.(gen, tag=2) == dfGen.esr_2 - @test GenX.min_cap.(gen, tag=1) == dfGen.mincaptag_1 - @test GenX.min_cap.(gen, tag=2) == dfGen.mincaptag_2 - @test GenX.min_cap.(gen, tag=3) == dfGen.mincaptag_3 - @test GenX.derating_factor.(gen, tag=1) == dfGen.capres_1 + @test GenX.esr.(gen, tag = 1) == dfGen.esr_1 + @test GenX.esr.(gen, tag = 2) == dfGen.esr_2 + @test GenX.min_cap.(gen, tag = 1) == dfGen.mincaptag_1 + @test GenX.min_cap.(gen, tag = 2) == dfGen.mincaptag_2 + @test GenX.min_cap.(gen, tag = 3) == dfGen.mincaptag_3 + @test GenX.derating_factor.(gen, tag = 1) == dfGen.capres_1 end function test_add_modules_to_resources(gen, dfGen) @@ -136,7 +154,6 @@ function test_add_modules_to_resources(gen, dfGen) end function test_inputs_keys(inputs, inputs_true) - @test inputs["G"] == inputs_true["G"] @test inputs["HYDRO_RES"] == inputs_true["HYDRO_RES"] @@ -159,7 +176,7 @@ function test_inputs_keys(inputs, inputs_true) @test inputs["THERM_NO_COMMIT"] == inputs_true["THERM_NO_COMMIT"] @test inputs["COMMIT"] == inputs_true["COMMIT"] @test inputs["C_Start"] == inputs_true["C_Start"] - + @test Set(inputs["RET_CAP"]) == inputs_true["RET_CAP"] @test Set(inputs["RET_CAP_CHARGE"]) == inputs_true["RET_CAP_CHARGE"] @test Set(inputs["RET_CAP_ENERGY"]) == inputs_true["RET_CAP_ENERGY"] @@ -167,14 +184,17 @@ function test_inputs_keys(inputs, inputs_true) @test Set(inputs["NEW_CAP_ENERGY"]) == inputs_true["NEW_CAP_ENERGY"] @test Set(inputs["NEW_CAP_CHARGE"]) == inputs_true["NEW_CAP_CHARGE"] - if isempty(inputs["MULTI_FUELS"]) - @test string.(inputs["slope_cols"]) == lowercase.(string.(inputs_true["slope_cols"])) - @test string.(inputs["intercept_cols"]) == lowercase.(string.(inputs_true["intercept_cols"])) - @test inputs["PWFU_data"] == rename!(inputs_true["PWFU_data"], lowercase.(names(inputs_true["PWFU_data"]))) + if isempty(inputs["MULTI_FUELS"]) + @test string.(inputs["slope_cols"]) == + lowercase.(string.(inputs_true["slope_cols"])) + @test string.(inputs["intercept_cols"]) == + lowercase.(string.(inputs_true["intercept_cols"])) + @test inputs["PWFU_data"] == + rename!(inputs_true["PWFU_data"], lowercase.(names(inputs_true["PWFU_data"]))) @test inputs["PWFU_Num_Segments"] == inputs_true["PWFU_Num_Segments"] @test inputs["THERM_COMMIT_PWFU"] == inputs_true["THERM_COMMIT_PWFU"] end - + @test inputs["R_ZONES"] == inputs_true["R_ZONES"] @test inputs["RESOURCE_ZONES"] == inputs_true["RESOURCE_ZONES"] @test inputs["RESOURCE_NAMES"] == inputs_true["RESOURCES"] @@ -183,7 +203,7 @@ end function test_resource_specific_attributes(gen, dfGen, inputs) @test GenX.is_buildable(gen) == dfGen[dfGen.new_build .== 1, :r_id] @test GenX.is_retirable(gen) == dfGen[dfGen.can_retire .== 1, :r_id] - + rs = GenX.ids_with_positive(gen, GenX.max_cap_mwh) @test rs == dfGen[dfGen.max_cap_mwh .> 0, :r_id] @test GenX.max_cap_mwh.(rs) == dfGen[dfGen.max_cap_mwh .> 0, :r_id] @@ -192,7 +212,7 @@ function test_resource_specific_attributes(gen, dfGen, inputs) @test GenX.max_charge_cap_mw.(rs) == dfGen[dfGen.max_charge_cap_mw .> 0, :r_id] rs = GenX.ids_with_unit_commitment(gen) @test rs == dfGen[dfGen.therm .== 1, :r_id] - @test GenX.cap_size.(gen[rs]) == dfGen[dfGen.therm.==1,:cap_size] + @test GenX.cap_size.(gen[rs]) == dfGen[dfGen.therm .== 1, :cap_size] rs = setdiff(inputs["HAS_FUEL"], inputs["THERM_COMMIT"]) @test GenX.heat_rate_mmbtu_per_mwh.(gen[rs]) == dfGen[rs, :heat_rate_mmbtu_per_mwh] rs = setdiff(inputs["THERM_COMMIT"], inputs["THERM_COMMIT_PWFU"]) @@ -211,23 +231,23 @@ function test_resource_specific_attributes(gen, dfGen, inputs) @test GenX.min_charge_cap_mw.(gen[rs]) == dfGen[rs, :min_charge_cap_mw] @test GenX.existing_charge_cap_mw.(gen[rs]) == dfGen[rs, :existing_charge_cap_mw] @test GenX.inv_cost_charge_per_mwyr.(gen[rs]) == dfGen[rs, :inv_cost_charge_per_mwyr] - @test GenX.fixed_om_cost_charge_per_mwyr.(gen[rs]) == dfGen[rs, :fixed_om_cost_charge_per_mwyr] + @test GenX.fixed_om_cost_charge_per_mwyr.(gen[rs]) == + dfGen[rs, :fixed_om_cost_charge_per_mwyr] rs = union(inputs["HYDRO_RES_KNOWN_CAP"], inputs["STOR_HYDRO_LONG_DURATION"]) - @test GenX.hydro_energy_to_power_ratio.(gen[rs]) == dfGen[rs, :hydro_energy_to_power_ratio] + @test GenX.hydro_energy_to_power_ratio.(gen[rs]) == + dfGen[rs, :hydro_energy_to_power_ratio] end function test_load_resources_data() - setup = Dict( - "ParameterScale" => 0, + setup = Dict("ParameterScale" => 0, "OperationalReserves" => 1, "UCommit" => 2, - "MultiStage" => 1, - ) + "MultiStage" => 1) # Merge the setup with the default settings settings = GenX.default_settings() merge!(settings, setup) - + test_path = joinpath("load_resources", "test_gen_non_colocated") # load dfGen and inputs_true to compare against @@ -269,25 +289,22 @@ function test_load_resources_data() end function test_load_VRE_STOR_data() - - setup = Dict( - "ParameterScale" => 0, + setup = Dict("ParameterScale" => 0, "OperationalReserves" => 1, "UCommit" => 2, - "MultiStage" => 0, - ) + "MultiStage" => 0) # Merge the setup with the default settings settings = GenX.default_settings() merge!(settings, setup) - - test_path = joinpath("load_resources","test_gen_vre_stor") + + test_path = joinpath("load_resources", "test_gen_vre_stor") input_true_filenames = InputsTrue("generators_data.csv", "inputs_after_loadgen.jld2") dfGen, inputs_true = prepare_inputs_true(test_path, input_true_filenames, settings) dfVRE_STOR = GenX.load_dataframe(joinpath(test_path, "Vre_and_stor_data.csv")) dfVRE_STOR = GenX.rename!(dfVRE_STOR, lowercase.(names(dfVRE_STOR))) - scale_factor = settings["ParameterScale"] == 1 ? GenX.ModelScalingFactor : 1. + scale_factor = settings["ParameterScale"] == 1 ? GenX.ModelScalingFactor : 1.0 GenX.scale_vre_stor_data!(dfVRE_STOR, scale_factor) resources_path = joinpath(test_path, settings["ResourcesFolder"]) @@ -304,27 +321,36 @@ function test_load_VRE_STOR_data() rs = inputs["VRE_STOR"] @test GenX.solar(gen) == dfVRE_STOR[dfVRE_STOR.solar .== 1, :r_id] @test GenX.wind(gen) == dfVRE_STOR[dfVRE_STOR.wind .== 1, :r_id] - @test GenX.storage_dc_discharge(gen) == dfVRE_STOR[dfVRE_STOR.stor_dc_discharge .>= 1, :r_id] - @test GenX.storage_sym_dc_discharge(gen) == dfVRE_STOR[dfVRE_STOR.stor_dc_discharge .== 1, :r_id] - @test GenX.storage_asym_dc_discharge(gen) == dfVRE_STOR[dfVRE_STOR.stor_dc_discharge .== 2, :r_id] + @test GenX.storage_dc_discharge(gen) == + dfVRE_STOR[dfVRE_STOR.stor_dc_discharge .>= 1, :r_id] + @test GenX.storage_sym_dc_discharge(gen) == + dfVRE_STOR[dfVRE_STOR.stor_dc_discharge .== 1, :r_id] + @test GenX.storage_asym_dc_discharge(gen) == + dfVRE_STOR[dfVRE_STOR.stor_dc_discharge .== 2, :r_id] @test GenX.storage_dc_charge(gen) == dfVRE_STOR[dfVRE_STOR.stor_dc_charge .>= 1, :r_id] - @test GenX.storage_sym_dc_charge(gen) == dfVRE_STOR[dfVRE_STOR.stor_dc_charge .== 1, :r_id] - @test GenX.storage_asym_dc_charge(gen) == dfVRE_STOR[dfVRE_STOR.stor_dc_charge .== 2, :r_id] - - @test GenX.storage_ac_discharge(gen) == dfVRE_STOR[dfVRE_STOR.stor_ac_discharge .>= 1, :r_id] - @test GenX.storage_sym_ac_discharge(gen) == dfVRE_STOR[dfVRE_STOR.stor_ac_discharge .== 1, :r_id] - @test GenX.storage_asym_ac_discharge(gen) == dfVRE_STOR[dfVRE_STOR.stor_ac_discharge .== 2, :r_id] + @test GenX.storage_sym_dc_charge(gen) == + dfVRE_STOR[dfVRE_STOR.stor_dc_charge .== 1, :r_id] + @test GenX.storage_asym_dc_charge(gen) == + dfVRE_STOR[dfVRE_STOR.stor_dc_charge .== 2, :r_id] + + @test GenX.storage_ac_discharge(gen) == + dfVRE_STOR[dfVRE_STOR.stor_ac_discharge .>= 1, :r_id] + @test GenX.storage_sym_ac_discharge(gen) == + dfVRE_STOR[dfVRE_STOR.stor_ac_discharge .== 1, :r_id] + @test GenX.storage_asym_ac_discharge(gen) == + dfVRE_STOR[dfVRE_STOR.stor_ac_discharge .== 2, :r_id] @test GenX.storage_ac_charge(gen) == dfVRE_STOR[dfVRE_STOR.stor_ac_charge .>= 1, :r_id] - @test GenX.storage_sym_ac_charge(gen) == dfVRE_STOR[dfVRE_STOR.stor_ac_charge .== 1, :r_id] - @test GenX.storage_asym_ac_charge(gen) == dfVRE_STOR[dfVRE_STOR.stor_ac_charge .== 2, :r_id] + @test GenX.storage_sym_ac_charge(gen) == + dfVRE_STOR[dfVRE_STOR.stor_ac_charge .== 1, :r_id] + @test GenX.storage_asym_ac_charge(gen) == + dfVRE_STOR[dfVRE_STOR.stor_ac_charge .== 2, :r_id] @test GenX.technology.(gen[rs]) == dfVRE_STOR.technology - @test GenX.is_LDS_VRE_STOR(gen) == dfVRE_STOR[dfVRE_STOR.lds_vre_stor .!= 0, :r_id] - + @test GenX.is_LDS_VRE_STOR(gen) == dfVRE_STOR[dfVRE_STOR.lds_vre_stor .!= 0, :r_id] - for attr in (:existing_cap_solar_mw, + for attr in (:existing_cap_solar_mw, :existing_cap_wind_mw, :existing_cap_inverter_mw, :existing_cap_charge_dc_mw, @@ -335,140 +361,201 @@ function test_load_VRE_STOR_data() test_ids_with_nonneg(attr, gen[rs], dfVRE_STOR) end - for attr in (:max_cap_solar_mw, - :max_cap_wind_mw, - :max_cap_inverter_mw, - :max_cap_charge_dc_mw, - :max_cap_charge_ac_mw, - :max_cap_discharge_dc_mw, - :max_cap_discharge_ac_mw) + for attr in (:max_cap_solar_mw, + :max_cap_wind_mw, + :max_cap_inverter_mw, + :max_cap_charge_dc_mw, + :max_cap_charge_ac_mw, + :max_cap_discharge_dc_mw, + :max_cap_discharge_ac_mw) test_macro_interface(attr, gen[rs], dfVRE_STOR) test_ids_with_nonneg(attr, gen[rs], dfVRE_STOR) test_ids_with(attr, gen[rs], dfVRE_STOR) end - for attr in (:min_cap_solar_mw, - :min_cap_wind_mw, - :min_cap_inverter_mw, - :min_cap_charge_dc_mw, - :min_cap_charge_ac_mw, - :min_cap_discharge_dc_mw, - :min_cap_discharge_ac_mw, - :inverter_ratio_solar, - :inverter_ratio_wind,) + for attr in (:min_cap_solar_mw, + :min_cap_wind_mw, + :min_cap_inverter_mw, + :min_cap_charge_dc_mw, + :min_cap_charge_ac_mw, + :min_cap_discharge_dc_mw, + :min_cap_discharge_ac_mw, + :inverter_ratio_solar, + :inverter_ratio_wind) test_macro_interface(attr, gen[rs], dfVRE_STOR) test_ids_with_positive(attr, gen[rs], dfVRE_STOR) end for attr in (:etainverter, - :inv_cost_inverter_per_mwyr, - :inv_cost_solar_per_mwyr, - :inv_cost_wind_per_mwyr, - :inv_cost_discharge_dc_per_mwyr, - :inv_cost_charge_dc_per_mwyr, - :inv_cost_discharge_ac_per_mwyr, - :inv_cost_charge_ac_per_mwyr, - :fixed_om_inverter_cost_per_mwyr, - :fixed_om_solar_cost_per_mwyr, - :fixed_om_wind_cost_per_mwyr, - :fixed_om_cost_discharge_dc_per_mwyr, - :fixed_om_cost_charge_dc_per_mwyr, - :fixed_om_cost_discharge_ac_per_mwyr, - :fixed_om_cost_charge_ac_per_mwyr, - :var_om_cost_per_mwh_solar, - :var_om_cost_per_mwh_wind, - :var_om_cost_per_mwh_charge_dc, - :var_om_cost_per_mwh_discharge_dc, - :var_om_cost_per_mwh_charge_ac, - :var_om_cost_per_mwh_discharge_ac, - :eff_up_ac, - :eff_down_ac, - :eff_up_dc, - :eff_down_dc, - :power_to_energy_ac, - :power_to_energy_dc) + :inv_cost_inverter_per_mwyr, + :inv_cost_solar_per_mwyr, + :inv_cost_wind_per_mwyr, + :inv_cost_discharge_dc_per_mwyr, + :inv_cost_charge_dc_per_mwyr, + :inv_cost_discharge_ac_per_mwyr, + :inv_cost_charge_ac_per_mwyr, + :fixed_om_inverter_cost_per_mwyr, + :fixed_om_solar_cost_per_mwyr, + :fixed_om_wind_cost_per_mwyr, + :fixed_om_cost_discharge_dc_per_mwyr, + :fixed_om_cost_charge_dc_per_mwyr, + :fixed_om_cost_discharge_ac_per_mwyr, + :fixed_om_cost_charge_ac_per_mwyr, + :var_om_cost_per_mwh_solar, + :var_om_cost_per_mwh_wind, + :var_om_cost_per_mwh_charge_dc, + :var_om_cost_per_mwh_discharge_dc, + :var_om_cost_per_mwh_charge_ac, + :var_om_cost_per_mwh_discharge_ac, + :eff_up_ac, + :eff_down_ac, + :eff_up_dc, + :eff_down_dc, + :power_to_energy_ac, + :power_to_energy_dc) test_macro_interface(attr, gen[rs], dfVRE_STOR) end # policies - @test GenX.esr_vrestor.(gen[rs], tag=1) == dfVRE_STOR.esr_vrestor_1 - @test GenX.esr_vrestor.(gen[rs], tag=2) == dfVRE_STOR.esr_vrestor_2 - @test GenX.min_cap_stor.(gen[rs], tag=1) == dfVRE_STOR.mincaptagstor_1 - @test GenX.min_cap_stor.(gen[rs], tag=2) == dfVRE_STOR.mincaptagstor_2 - @test GenX.derating_factor.(gen[rs], tag=1) == dfVRE_STOR.capresvrestor_1 - @test GenX.derating_factor.(gen[rs], tag=2) == dfVRE_STOR.capresvrestor_2 - @test GenX.max_cap_stor.(gen[rs], tag=1) == dfVRE_STOR.maxcaptagstor_1 - @test GenX.max_cap_stor.(gen[rs], tag=2) == dfVRE_STOR.maxcaptagstor_2 - @test GenX.min_cap_solar.(gen[rs], tag=1) == dfVRE_STOR.mincaptagsolar_1 - @test GenX.max_cap_solar.(gen[rs], tag=1) == dfVRE_STOR.maxcaptagsolar_1 - @test GenX.min_cap_wind.(gen[rs], tag=1) == dfVRE_STOR.mincaptagwind_1 - @test GenX.max_cap_wind.(gen[rs], tag=1) == dfVRE_STOR.maxcaptagwind_1 - - @test GenX.ids_with_policy(gen, GenX.min_cap_solar, tag=1) == dfVRE_STOR[dfVRE_STOR.mincaptagsolar_1 .== 1, :r_id] - @test GenX.ids_with_policy(gen, GenX.min_cap_wind, tag=1) == dfVRE_STOR[dfVRE_STOR.mincaptagwind_1 .== 1, :r_id] - @test GenX.ids_with_policy(gen, GenX.min_cap_stor, tag=1) == dfVRE_STOR[dfVRE_STOR.mincaptagstor_1 .== 1, :r_id] - @test GenX.ids_with_policy(gen, GenX.max_cap_solar, tag=1) == dfVRE_STOR[dfVRE_STOR.maxcaptagsolar_1 .== 1, :r_id] - @test GenX.ids_with_policy(gen, GenX.max_cap_wind, tag=1) == dfVRE_STOR[dfVRE_STOR.maxcaptagwind_1 .== 1, :r_id] - @test GenX.ids_with_policy(gen, GenX.max_cap_stor, tag=1) == dfVRE_STOR[dfVRE_STOR.maxcaptagstor_1 .== 1, :r_id] + @test GenX.esr_vrestor.(gen[rs], tag = 1) == dfVRE_STOR.esr_vrestor_1 + @test GenX.esr_vrestor.(gen[rs], tag = 2) == dfVRE_STOR.esr_vrestor_2 + @test GenX.min_cap_stor.(gen[rs], tag = 1) == dfVRE_STOR.mincaptagstor_1 + @test GenX.min_cap_stor.(gen[rs], tag = 2) == dfVRE_STOR.mincaptagstor_2 + @test GenX.derating_factor.(gen[rs], tag = 1) == dfVRE_STOR.capresvrestor_1 + @test GenX.derating_factor.(gen[rs], tag = 2) == dfVRE_STOR.capresvrestor_2 + @test GenX.max_cap_stor.(gen[rs], tag = 1) == dfVRE_STOR.maxcaptagstor_1 + @test GenX.max_cap_stor.(gen[rs], tag = 2) == dfVRE_STOR.maxcaptagstor_2 + @test GenX.min_cap_solar.(gen[rs], tag = 1) == dfVRE_STOR.mincaptagsolar_1 + @test GenX.max_cap_solar.(gen[rs], tag = 1) == dfVRE_STOR.maxcaptagsolar_1 + @test GenX.min_cap_wind.(gen[rs], tag = 1) == dfVRE_STOR.mincaptagwind_1 + @test GenX.max_cap_wind.(gen[rs], tag = 1) == dfVRE_STOR.maxcaptagwind_1 + + @test GenX.ids_with_policy(gen, GenX.min_cap_solar, tag = 1) == + dfVRE_STOR[dfVRE_STOR.mincaptagsolar_1 .== 1, :r_id] + @test GenX.ids_with_policy(gen, GenX.min_cap_wind, tag = 1) == + dfVRE_STOR[dfVRE_STOR.mincaptagwind_1 .== 1, :r_id] + @test GenX.ids_with_policy(gen, GenX.min_cap_stor, tag = 1) == + dfVRE_STOR[dfVRE_STOR.mincaptagstor_1 .== 1, :r_id] + @test GenX.ids_with_policy(gen, GenX.max_cap_solar, tag = 1) == + dfVRE_STOR[dfVRE_STOR.maxcaptagsolar_1 .== 1, :r_id] + @test GenX.ids_with_policy(gen, GenX.max_cap_wind, tag = 1) == + dfVRE_STOR[dfVRE_STOR.maxcaptagwind_1 .== 1, :r_id] + @test GenX.ids_with_policy(gen, GenX.max_cap_stor, tag = 1) == + dfVRE_STOR[dfVRE_STOR.maxcaptagstor_1 .== 1, :r_id] # inputs keys - @test inputs["VRE_STOR"] == dfGen[dfGen.vre_stor.==1,:r_id] - @test inputs["VS_SOLAR"] == dfVRE_STOR[(dfVRE_STOR.solar.!=0),:r_id] - @test inputs["VS_WIND"] == dfVRE_STOR[(dfVRE_STOR.wind.!=0),:r_id] - @test inputs["VS_DC"] == union(dfVRE_STOR[dfVRE_STOR.stor_dc_discharge.>=1,:r_id], dfVRE_STOR[dfVRE_STOR.stor_dc_charge.>=1,:r_id], dfVRE_STOR[dfVRE_STOR.solar.!=0,:r_id]) - - @test inputs["VS_STOR"] == union(dfVRE_STOR[dfVRE_STOR.stor_dc_charge.>=1,:r_id], dfVRE_STOR[dfVRE_STOR.stor_ac_charge.>=1,:r_id], - dfVRE_STOR[dfVRE_STOR.stor_dc_discharge.>=1,:r_id], dfVRE_STOR[dfVRE_STOR.stor_ac_discharge.>=1,:r_id]) - STOR = inputs["VS_STOR"] - @test inputs["VS_STOR_DC_DISCHARGE"] == dfVRE_STOR[(dfVRE_STOR.stor_dc_discharge.>=1),:r_id] - @test inputs["VS_SYM_DC_DISCHARGE"] == dfVRE_STOR[dfVRE_STOR.stor_dc_discharge.==1,:r_id] - @test inputs["VS_ASYM_DC_DISCHARGE"] == dfVRE_STOR[dfVRE_STOR.stor_dc_discharge.==2,:r_id] - @test inputs["VS_STOR_DC_CHARGE"] == dfVRE_STOR[(dfVRE_STOR.stor_dc_charge.>=1),:r_id] - @test inputs["VS_SYM_DC_CHARGE"] == dfVRE_STOR[dfVRE_STOR.stor_dc_charge.==1,:r_id] - @test inputs["VS_ASYM_DC_CHARGE"] == dfVRE_STOR[dfVRE_STOR.stor_dc_charge.==2,:r_id] - @test inputs["VS_STOR_AC_DISCHARGE"] == dfVRE_STOR[(dfVRE_STOR.stor_ac_discharge.>=1),:r_id] - @test inputs["VS_SYM_AC_DISCHARGE"] == dfVRE_STOR[dfVRE_STOR.stor_ac_discharge.==1,:r_id] - @test inputs["VS_ASYM_AC_DISCHARGE"] == dfVRE_STOR[dfVRE_STOR.stor_ac_discharge.==2,:r_id] - @test inputs["VS_STOR_AC_CHARGE"] == dfVRE_STOR[(dfVRE_STOR.stor_ac_charge.>=1),:r_id] - @test inputs["VS_SYM_AC_CHARGE"] == dfVRE_STOR[dfVRE_STOR.stor_ac_charge.==1,:r_id] - @test inputs["VS_ASYM_AC_CHARGE"] == dfVRE_STOR[dfVRE_STOR.stor_ac_charge.==2,:r_id] - @test inputs["VS_LDS"] == dfVRE_STOR[(dfVRE_STOR.lds_vre_stor.!=0),:r_id] - @test inputs["VS_nonLDS"] == setdiff(STOR, inputs["VS_LDS"]) - @test inputs["VS_ASYM"] == union(inputs["VS_ASYM_DC_CHARGE"], inputs["VS_ASYM_DC_DISCHARGE"], inputs["VS_ASYM_AC_DISCHARGE"], inputs["VS_ASYM_AC_CHARGE"]) - @test inputs["VS_SYM_DC"] == intersect(inputs["VS_SYM_DC_CHARGE"], inputs["VS_SYM_DC_DISCHARGE"]) - @test inputs["VS_SYM_AC"] == intersect(inputs["VS_SYM_AC_CHARGE"], inputs["VS_SYM_AC_DISCHARGE"]) + @test inputs["VRE_STOR"] == dfGen[dfGen.vre_stor .== 1, :r_id] + @test inputs["VS_SOLAR"] == dfVRE_STOR[(dfVRE_STOR.solar .!= 0), :r_id] + @test inputs["VS_WIND"] == dfVRE_STOR[(dfVRE_STOR.wind .!= 0), :r_id] + @test inputs["VS_DC"] == union(dfVRE_STOR[dfVRE_STOR.stor_dc_discharge .>= 1, :r_id], + dfVRE_STOR[dfVRE_STOR.stor_dc_charge .>= 1, :r_id], + dfVRE_STOR[dfVRE_STOR.solar .!= 0, :r_id]) + + @test inputs["VS_STOR"] == union(dfVRE_STOR[dfVRE_STOR.stor_dc_charge .>= 1, :r_id], + dfVRE_STOR[dfVRE_STOR.stor_ac_charge .>= 1, :r_id], + dfVRE_STOR[dfVRE_STOR.stor_dc_discharge .>= 1, :r_id], + dfVRE_STOR[dfVRE_STOR.stor_ac_discharge .>= 1, :r_id]) + STOR = inputs["VS_STOR"] + @test inputs["VS_STOR_DC_DISCHARGE"] == + dfVRE_STOR[(dfVRE_STOR.stor_dc_discharge .>= 1), :r_id] + @test inputs["VS_SYM_DC_DISCHARGE"] == + dfVRE_STOR[dfVRE_STOR.stor_dc_discharge .== 1, :r_id] + @test inputs["VS_ASYM_DC_DISCHARGE"] == + dfVRE_STOR[dfVRE_STOR.stor_dc_discharge .== 2, :r_id] + @test inputs["VS_STOR_DC_CHARGE"] == + dfVRE_STOR[(dfVRE_STOR.stor_dc_charge .>= 1), :r_id] + @test inputs["VS_SYM_DC_CHARGE"] == dfVRE_STOR[dfVRE_STOR.stor_dc_charge .== 1, :r_id] + @test inputs["VS_ASYM_DC_CHARGE"] == dfVRE_STOR[dfVRE_STOR.stor_dc_charge .== 2, :r_id] + @test inputs["VS_STOR_AC_DISCHARGE"] == + dfVRE_STOR[(dfVRE_STOR.stor_ac_discharge .>= 1), :r_id] + @test inputs["VS_SYM_AC_DISCHARGE"] == + dfVRE_STOR[dfVRE_STOR.stor_ac_discharge .== 1, :r_id] + @test inputs["VS_ASYM_AC_DISCHARGE"] == + dfVRE_STOR[dfVRE_STOR.stor_ac_discharge .== 2, :r_id] + @test inputs["VS_STOR_AC_CHARGE"] == + dfVRE_STOR[(dfVRE_STOR.stor_ac_charge .>= 1), :r_id] + @test inputs["VS_SYM_AC_CHARGE"] == dfVRE_STOR[dfVRE_STOR.stor_ac_charge .== 1, :r_id] + @test inputs["VS_ASYM_AC_CHARGE"] == dfVRE_STOR[dfVRE_STOR.stor_ac_charge .== 2, :r_id] + @test inputs["VS_LDS"] == dfVRE_STOR[(dfVRE_STOR.lds_vre_stor .!= 0), :r_id] + @test inputs["VS_nonLDS"] == setdiff(STOR, inputs["VS_LDS"]) + @test inputs["VS_ASYM"] == union(inputs["VS_ASYM_DC_CHARGE"], + inputs["VS_ASYM_DC_DISCHARGE"], + inputs["VS_ASYM_AC_DISCHARGE"], + inputs["VS_ASYM_AC_CHARGE"]) + @test inputs["VS_SYM_DC"] == + intersect(inputs["VS_SYM_DC_CHARGE"], inputs["VS_SYM_DC_DISCHARGE"]) + @test inputs["VS_SYM_AC"] == + intersect(inputs["VS_SYM_AC_CHARGE"], inputs["VS_SYM_AC_DISCHARGE"]) buildable = dfGen[dfGen.new_build .== 1, :r_id] retirable = dfGen[dfGen.can_retire .== 1, :r_id] - @test inputs["NEW_CAP_SOLAR"] == intersect(buildable, dfVRE_STOR[dfVRE_STOR.solar.!=0,:r_id], dfVRE_STOR[dfVRE_STOR.max_cap_solar_mw.!=0,:r_id]) - @test inputs["RET_CAP_SOLAR"] == intersect(retirable, dfVRE_STOR[dfVRE_STOR.solar.!=0,:r_id], dfVRE_STOR[dfVRE_STOR.existing_cap_solar_mw.>=0,:r_id]) - @test inputs["NEW_CAP_WIND"] == intersect(buildable, dfVRE_STOR[dfVRE_STOR.wind.!=0,:r_id], dfVRE_STOR[dfVRE_STOR.max_cap_wind_mw.!=0,:r_id]) - @test inputs["RET_CAP_WIND"] == intersect(retirable, dfVRE_STOR[dfVRE_STOR.wind.!=0,:r_id], dfVRE_STOR[dfVRE_STOR.existing_cap_wind_mw.>=0,:r_id]) - @test inputs["NEW_CAP_DC"] == intersect(buildable, dfVRE_STOR[dfVRE_STOR.max_cap_inverter_mw.!=0,:r_id], inputs["VS_DC"]) - @test inputs["RET_CAP_DC"] == intersect(retirable, dfVRE_STOR[dfVRE_STOR.existing_cap_inverter_mw.>=0,:r_id], inputs["VS_DC"]) - @test inputs["NEW_CAP_STOR"] == intersect(buildable, dfGen[dfGen.max_cap_mwh.!=0,:r_id], inputs["VS_STOR"]) - @test inputs["RET_CAP_STOR"] == intersect(retirable, dfGen[dfGen.existing_cap_mwh.>=0,:r_id], inputs["VS_STOR"]) - @test inputs["NEW_CAP_CHARGE_DC"] == intersect(buildable, dfVRE_STOR[dfVRE_STOR.max_cap_charge_dc_mw.!=0,:r_id], inputs["VS_ASYM_DC_CHARGE"]) - @test inputs["RET_CAP_CHARGE_DC"] == intersect(retirable, dfVRE_STOR[dfVRE_STOR.existing_cap_charge_dc_mw.>=0,:r_id], inputs["VS_ASYM_DC_CHARGE"]) - @test inputs["NEW_CAP_DISCHARGE_DC"] == intersect(buildable, dfVRE_STOR[dfVRE_STOR.max_cap_discharge_dc_mw.!=0,:r_id], inputs["VS_ASYM_DC_DISCHARGE"]) - @test inputs["RET_CAP_DISCHARGE_DC"] == intersect(retirable, dfVRE_STOR[dfVRE_STOR.existing_cap_discharge_dc_mw.>=0,:r_id], inputs["VS_ASYM_DC_DISCHARGE"]) - @test inputs["NEW_CAP_CHARGE_AC"] == intersect(buildable, dfVRE_STOR[dfVRE_STOR.max_cap_charge_ac_mw.!=0,:r_id], inputs["VS_ASYM_AC_CHARGE"]) - @test inputs["RET_CAP_CHARGE_AC"] == intersect(retirable, dfVRE_STOR[dfVRE_STOR.existing_cap_charge_ac_mw.>=0,:r_id], inputs["VS_ASYM_AC_CHARGE"]) - @test inputs["NEW_CAP_DISCHARGE_AC"] == intersect(buildable, dfVRE_STOR[dfVRE_STOR.max_cap_discharge_ac_mw.!=0,:r_id], inputs["VS_ASYM_AC_DISCHARGE"]) - @test inputs["RET_CAP_DISCHARGE_AC"] == intersect(retirable, dfVRE_STOR[dfVRE_STOR.existing_cap_discharge_ac_mw.>=0,:r_id], inputs["VS_ASYM_AC_DISCHARGE"]) - @test inputs["RESOURCE_NAMES_VRE_STOR"] == collect(skipmissing(dfVRE_STOR[!,:resource][1:size(inputs["VRE_STOR"])[1]])) - @test inputs["RESOURCE_NAMES_SOLAR"] == dfVRE_STOR[(dfVRE_STOR.solar.!=0), :resource] - @test inputs["RESOURCE_NAMES_WIND"] == dfVRE_STOR[(dfVRE_STOR.wind.!=0), :resource] - @test inputs["RESOURCE_NAMES_DC_DISCHARGE"] == dfVRE_STOR[(dfVRE_STOR.stor_dc_discharge.!=0), :resource] - @test inputs["RESOURCE_NAMES_AC_DISCHARGE"] == dfVRE_STOR[(dfVRE_STOR.stor_ac_discharge.!=0), :resource] - @test inputs["RESOURCE_NAMES_DC_CHARGE"] == dfVRE_STOR[(dfVRE_STOR.stor_dc_charge.!=0), :resource] - @test inputs["RESOURCE_NAMES_AC_CHARGE"] == dfVRE_STOR[(dfVRE_STOR.stor_ac_charge.!=0), :resource] - @test inputs["ZONES_SOLAR"] == dfVRE_STOR[(dfVRE_STOR.solar.!=0), :zone] - @test inputs["ZONES_WIND"] == dfVRE_STOR[(dfVRE_STOR.wind.!=0), :zone] - @test inputs["ZONES_DC_DISCHARGE"] == dfVRE_STOR[(dfVRE_STOR.stor_dc_discharge.!=0), :zone] - @test inputs["ZONES_AC_DISCHARGE"] == dfVRE_STOR[(dfVRE_STOR.stor_ac_discharge.!=0), :zone] - @test inputs["ZONES_DC_CHARGE"] == dfVRE_STOR[(dfVRE_STOR.stor_dc_charge.!=0), :zone] - @test inputs["ZONES_AC_CHARGE"] == dfVRE_STOR[(dfVRE_STOR.stor_ac_charge.!=0), :zone] + @test inputs["NEW_CAP_SOLAR"] == intersect(buildable, + dfVRE_STOR[dfVRE_STOR.solar .!= 0, :r_id], + dfVRE_STOR[dfVRE_STOR.max_cap_solar_mw .!= 0, :r_id]) + @test inputs["RET_CAP_SOLAR"] == intersect(retirable, + dfVRE_STOR[dfVRE_STOR.solar .!= 0, :r_id], + dfVRE_STOR[dfVRE_STOR.existing_cap_solar_mw .>= 0, :r_id]) + @test inputs["NEW_CAP_WIND"] == intersect(buildable, + dfVRE_STOR[dfVRE_STOR.wind .!= 0, :r_id], + dfVRE_STOR[dfVRE_STOR.max_cap_wind_mw .!= 0, :r_id]) + @test inputs["RET_CAP_WIND"] == intersect(retirable, + dfVRE_STOR[dfVRE_STOR.wind .!= 0, :r_id], + dfVRE_STOR[dfVRE_STOR.existing_cap_wind_mw .>= 0, :r_id]) + @test inputs["NEW_CAP_DC"] == intersect(buildable, + dfVRE_STOR[dfVRE_STOR.max_cap_inverter_mw .!= 0, :r_id], + inputs["VS_DC"]) + @test inputs["RET_CAP_DC"] == intersect(retirable, + dfVRE_STOR[dfVRE_STOR.existing_cap_inverter_mw .>= 0, :r_id], + inputs["VS_DC"]) + @test inputs["NEW_CAP_STOR"] == + intersect(buildable, dfGen[dfGen.max_cap_mwh .!= 0, :r_id], inputs["VS_STOR"]) + @test inputs["RET_CAP_STOR"] == intersect(retirable, + dfGen[dfGen.existing_cap_mwh .>= 0, :r_id], + inputs["VS_STOR"]) + @test inputs["NEW_CAP_CHARGE_DC"] == intersect(buildable, + dfVRE_STOR[dfVRE_STOR.max_cap_charge_dc_mw .!= 0, :r_id], + inputs["VS_ASYM_DC_CHARGE"]) + @test inputs["RET_CAP_CHARGE_DC"] == intersect(retirable, + dfVRE_STOR[dfVRE_STOR.existing_cap_charge_dc_mw .>= 0, :r_id], + inputs["VS_ASYM_DC_CHARGE"]) + @test inputs["NEW_CAP_DISCHARGE_DC"] == intersect(buildable, + dfVRE_STOR[dfVRE_STOR.max_cap_discharge_dc_mw .!= 0, :r_id], + inputs["VS_ASYM_DC_DISCHARGE"]) + @test inputs["RET_CAP_DISCHARGE_DC"] == intersect(retirable, + dfVRE_STOR[dfVRE_STOR.existing_cap_discharge_dc_mw .>= 0, :r_id], + inputs["VS_ASYM_DC_DISCHARGE"]) + @test inputs["NEW_CAP_CHARGE_AC"] == intersect(buildable, + dfVRE_STOR[dfVRE_STOR.max_cap_charge_ac_mw .!= 0, :r_id], + inputs["VS_ASYM_AC_CHARGE"]) + @test inputs["RET_CAP_CHARGE_AC"] == intersect(retirable, + dfVRE_STOR[dfVRE_STOR.existing_cap_charge_ac_mw .>= 0, :r_id], + inputs["VS_ASYM_AC_CHARGE"]) + @test inputs["NEW_CAP_DISCHARGE_AC"] == intersect(buildable, + dfVRE_STOR[dfVRE_STOR.max_cap_discharge_ac_mw .!= 0, :r_id], + inputs["VS_ASYM_AC_DISCHARGE"]) + @test inputs["RET_CAP_DISCHARGE_AC"] == intersect(retirable, + dfVRE_STOR[dfVRE_STOR.existing_cap_discharge_ac_mw .>= 0, :r_id], + inputs["VS_ASYM_AC_DISCHARGE"]) + @test inputs["RESOURCE_NAMES_VRE_STOR"] == + collect(skipmissing(dfVRE_STOR[!, :resource][1:size(inputs["VRE_STOR"])[1]])) + @test inputs["RESOURCE_NAMES_SOLAR"] == dfVRE_STOR[(dfVRE_STOR.solar .!= 0), :resource] + @test inputs["RESOURCE_NAMES_WIND"] == dfVRE_STOR[(dfVRE_STOR.wind .!= 0), :resource] + @test inputs["RESOURCE_NAMES_DC_DISCHARGE"] == + dfVRE_STOR[(dfVRE_STOR.stor_dc_discharge .!= 0), :resource] + @test inputs["RESOURCE_NAMES_AC_DISCHARGE"] == + dfVRE_STOR[(dfVRE_STOR.stor_ac_discharge .!= 0), :resource] + @test inputs["RESOURCE_NAMES_DC_CHARGE"] == + dfVRE_STOR[(dfVRE_STOR.stor_dc_charge .!= 0), :resource] + @test inputs["RESOURCE_NAMES_AC_CHARGE"] == + dfVRE_STOR[(dfVRE_STOR.stor_ac_charge .!= 0), :resource] + @test inputs["ZONES_SOLAR"] == dfVRE_STOR[(dfVRE_STOR.solar .!= 0), :zone] + @test inputs["ZONES_WIND"] == dfVRE_STOR[(dfVRE_STOR.wind .!= 0), :zone] + @test inputs["ZONES_DC_DISCHARGE"] == + dfVRE_STOR[(dfVRE_STOR.stor_dc_discharge .!= 0), :zone] + @test inputs["ZONES_AC_DISCHARGE"] == + dfVRE_STOR[(dfVRE_STOR.stor_ac_discharge .!= 0), :zone] + @test inputs["ZONES_DC_CHARGE"] == dfVRE_STOR[(dfVRE_STOR.stor_dc_charge .!= 0), :zone] + @test inputs["ZONES_AC_CHARGE"] == dfVRE_STOR[(dfVRE_STOR.stor_ac_charge .!= 0), :zone] end with_logger(ConsoleLogger(stderr, Logging.Warn)) do @@ -476,4 +563,4 @@ with_logger(ConsoleLogger(stderr, Logging.Warn)) do test_load_VRE_STOR_data() end -end # module TestLoadResourceData \ No newline at end of file +end # module TestLoadResourceData diff --git a/test/test_multifuels.jl b/test/test_multifuels.jl index 050f908509..ff1a0efdac 100644 --- a/test/test_multifuels.jl +++ b/test/test_multifuels.jl @@ -8,8 +8,7 @@ obj_true = 5494.7919354 test_path = "multi_fuels" # Define test inputs -genx_setup = Dict( - "Trans_Loss_Segments" => 1, +genx_setup = Dict("Trans_Loss_Segments" => 1, "EnergyShareRequirement" => 1, "CapacityReserveMargin" => 1, "StorageLosses" => 1, @@ -17,8 +16,7 @@ genx_setup = Dict( "MaxCapReq" => 1, "ParameterScale" => 1, "WriteShadowPrices" => 1, - "UCommit" => 2, -) + "UCommit" => 2) # Run the case and get the objective value and tolerance EP, _, _ = redirect_stdout(devnull) do @@ -29,11 +27,11 @@ optimal_tol_rel = get_attribute(EP, "ipm_optimality_tolerance") optimal_tol = optimal_tol_rel * obj_test # Convert to absolute tolerance # Test the objective value -test_result = @test obj_test ≈ obj_true atol = optimal_tol +test_result = @test obj_test≈obj_true atol=optimal_tol # Round objective value and tolerance. Write to test log. obj_test = round_from_tol!(obj_test, optimal_tol) optimal_tol = round_from_tol!(optimal_tol, optimal_tol) write_testlog(test_path, obj_test, optimal_tol, test_result) -end # module TestMultiFuels \ No newline at end of file +end # module TestMultiFuels diff --git a/test/test_multistage.jl b/test/test_multistage.jl index c7f8d83631..4215bca5fc 100644 --- a/test/test_multistage.jl +++ b/test/test_multistage.jl @@ -5,38 +5,31 @@ using Test include(joinpath(@__DIR__, "utilities.jl")) obj_true = [79734.80032, 41630.03494, 27855.20631] -test_path = joinpath(@__DIR__, "multi_stage"); +test_path = joinpath(@__DIR__, "multi_stage") # Define test inputs -multistage_setup = Dict( - "NumStages" => 3, +multistage_setup = Dict("NumStages" => 3, "StageLengths" => [10, 10, 10], "WACC" => 0.045, "ConvergenceTolerance" => 0.01, - "Myopic" => 0, -) + "Myopic" => 0) -genx_setup = Dict( - "Trans_Loss_Segments" => 1, +genx_setup = Dict("Trans_Loss_Segments" => 1, "OperationalReserves" => 1, "CO2Cap" => 2, "StorageLosses" => 1, "ParameterScale" => 1, "UCommit" => 2, "MultiStage" => 1, - "MultiStageSettingsDict" => multistage_setup, -) + "MultiStageSettingsDict" => multistage_setup) # Run the case and get the objective value and tolerance EP, _, _ = redirect_stdout(devnull) do run_genx_case_testing(test_path, genx_setup) end -obj_test = objective_value.(EP[i] for i = 1:multistage_setup["NumStages"]) -optimal_tol_rel = - get_attribute.( - (EP[i] for i = 1:multistage_setup["NumStages"]), - "ipm_optimality_tolerance", - ) +obj_test = objective_value.(EP[i] for i in 1:multistage_setup["NumStages"]) +optimal_tol_rel = get_attribute.((EP[i] for i in 1:multistage_setup["NumStages"]), + "ipm_optimality_tolerance") optimal_tol = optimal_tol_rel .* obj_test # Convert to absolute tolerance # Test the objective value @@ -47,17 +40,18 @@ obj_test = round_from_tol!.(obj_test, optimal_tol) optimal_tol = round_from_tol!.(optimal_tol, optimal_tol) write_testlog(test_path, obj_test, optimal_tol, test_result) -function test_new_build(EP::Dict,inputs::Dict) +function test_new_build(EP::Dict, inputs::Dict) ### Test that the resource with New_Build = 0 did not expand capacity - a = true; + a = true for t in keys(EP) - if t==1 - a = value(EP[t][:eTotalCap][1]) <= GenX.existing_cap_mw(inputs[1]["RESOURCES"][1])[1] + if t == 1 + a = value(EP[t][:eTotalCap][1]) <= + GenX.existing_cap_mw(inputs[1]["RESOURCES"][1])[1] else - a = value(EP[t][:eTotalCap][1]) <= value(EP[t-1][:eTotalCap][1]) + a = value(EP[t][:eTotalCap][1]) <= value(EP[t - 1][:eTotalCap][1]) end - if a==false + if a == false break end end @@ -65,17 +59,18 @@ function test_new_build(EP::Dict,inputs::Dict) return a end -function test_can_retire(EP::Dict,inputs::Dict) +function test_can_retire(EP::Dict, inputs::Dict) ### Test that the resource with Can_Retire = 0 did not retire capacity - a = true; - + a = true + for t in keys(EP) - if t==1 - a = value(EP[t][:eTotalCap][1]) >= GenX.existing_cap_mw(inputs[1]["RESOURCES"][1])[1] + if t == 1 + a = value(EP[t][:eTotalCap][1]) >= + GenX.existing_cap_mw(inputs[1]["RESOURCES"][1])[1] else - a = value(EP[t][:eTotalCap][1]) >= value(EP[t-1][:eTotalCap][1]) + a = value(EP[t][:eTotalCap][1]) >= value(EP[t - 1][:eTotalCap][1]) end - if a==false + if a == false break end end @@ -83,49 +78,59 @@ function test_can_retire(EP::Dict,inputs::Dict) return a end -test_path_new_build = joinpath(test_path, "new_build"); +test_path_new_build = joinpath(test_path, "new_build") EP, inputs, _ = redirect_stdout(devnull) do - run_genx_case_testing(test_path_new_build, genx_setup); + run_genx_case_testing(test_path_new_build, genx_setup) end -new_build_test_result = @test test_new_build(EP,inputs) -write_testlog(test_path,"Testing that the resource with New_Build = 0 did not expand capacity",new_build_test_result) +new_build_test_result = @test test_new_build(EP, inputs) +write_testlog(test_path, + "Testing that the resource with New_Build = 0 did not expand capacity", + new_build_test_result) -test_path_can_retire = joinpath(test_path, "can_retire"); +test_path_can_retire = joinpath(test_path, "can_retire") EP, inputs, _ = redirect_stdout(devnull) do - run_genx_case_testing(test_path_can_retire, genx_setup); + run_genx_case_testing(test_path_can_retire, genx_setup) end -can_retire_test_result = @test test_can_retire(EP,inputs) -write_testlog(test_path,"Testing that the resource with Can_Retire = 0 did not expand capacity",can_retire_test_result) - +can_retire_test_result = @test test_can_retire(EP, inputs) +write_testlog(test_path, + "Testing that the resource with Can_Retire = 0 did not expand capacity", + can_retire_test_result) function test_update_cumulative_min_ret!() # Merge the genx_setup with the default settings settings = GenX.default_settings() - for ParameterScale ∈ [0,1] + for ParameterScale in [0, 1] genx_setup["ParameterScale"] = ParameterScale merge!(settings, genx_setup) inputs_dict = Dict() true_min_retirements = Dict() - + scale_factor = settings["ParameterScale"] == 1 ? GenX.ModelScalingFactor : 1.0 redirect_stdout(devnull) do warnerror_logger = ConsoleLogger(stderr, Logging.Warn) with_logger(warnerror_logger) do for t in 1:3 - inpath_sub = joinpath(test_path, "cum_min_ret", string("inputs_p",t)) - - true_min_retirements[t] = CSV.read(joinpath(inpath_sub, "resources", "Resource_multistage_data.csv"), DataFrame) - rename!(true_min_retirements[t], lowercase.(names(true_min_retirements[t]))) + inpath_sub = joinpath(test_path, "cum_min_ret", string("inputs_p", t)) + + true_min_retirements[t] = CSV.read(joinpath(inpath_sub, + "resources", + "Resource_multistage_data.csv"), + DataFrame) + rename!(true_min_retirements[t], + lowercase.(names(true_min_retirements[t]))) GenX.scale_multistage_data!(true_min_retirements[t], scale_factor) inputs_dict[t] = Dict() inputs_dict[t]["Z"] = 1 GenX.load_demand_data!(settings, inpath_sub, inputs_dict[t]) - GenX.load_resources_data!(inputs_dict[t], settings, inpath_sub, joinpath(inpath_sub, settings["ResourcesFolder"])) - compute_cumulative_min_retirements!(inputs_dict,t) + GenX.load_resources_data!(inputs_dict[t], + settings, + inpath_sub, + joinpath(inpath_sub, settings["ResourcesFolder"])) + compute_cumulative_min_retirements!(inputs_dict, t) end end end @@ -133,27 +138,47 @@ function test_update_cumulative_min_ret!() for t in 1:3 # Test that the cumulative min retirements are updated correctly gen = inputs_dict[t]["RESOURCES"] - @test GenX.min_retired_cap_mw.(gen) == true_min_retirements[t].min_retired_cap_mw - @test GenX.min_retired_energy_cap_mw.(gen) == true_min_retirements[t].min_retired_energy_cap_mw - @test GenX.min_retired_charge_cap_mw.(gen) == true_min_retirements[t].min_retired_charge_cap_mw - @test GenX.min_retired_cap_inverter_mw.(gen) == true_min_retirements[t].min_retired_cap_inverter_mw - @test GenX.min_retired_cap_solar_mw.(gen) == true_min_retirements[t].min_retired_cap_solar_mw - @test GenX.min_retired_cap_wind_mw.(gen) == true_min_retirements[t].min_retired_cap_wind_mw - @test GenX.min_retired_cap_discharge_dc_mw.(gen) == true_min_retirements[t].min_retired_cap_discharge_dc_mw - @test GenX.min_retired_cap_charge_dc_mw.(gen) == true_min_retirements[t].min_retired_cap_charge_dc_mw - @test GenX.min_retired_cap_discharge_ac_mw.(gen) == true_min_retirements[t].min_retired_cap_discharge_ac_mw - @test GenX.min_retired_cap_charge_ac_mw.(gen) == true_min_retirements[t].min_retired_cap_charge_ac_mw - - @test GenX.cum_min_retired_cap_mw.(gen) == sum(true_min_retirements[i].min_retired_cap_mw for i in 1:t) - @test GenX.cum_min_retired_energy_cap_mw.(gen) == sum(true_min_retirements[i].min_retired_energy_cap_mw for i in 1:t) - @test GenX.cum_min_retired_charge_cap_mw.(gen) == sum(true_min_retirements[i].min_retired_charge_cap_mw for i in 1:t) - @test GenX.cum_min_retired_cap_inverter_mw.(gen) == sum(true_min_retirements[i].min_retired_cap_inverter_mw for i in 1:t) - @test GenX.cum_min_retired_cap_solar_mw.(gen) == sum(true_min_retirements[i].min_retired_cap_solar_mw for i in 1:t) - @test GenX.cum_min_retired_cap_wind_mw.(gen) == sum(true_min_retirements[i].min_retired_cap_wind_mw for i in 1:t) - @test GenX.cum_min_retired_cap_discharge_dc_mw.(gen) == sum(true_min_retirements[i].min_retired_cap_discharge_dc_mw for i in 1:t) - @test GenX.cum_min_retired_cap_charge_dc_mw.(gen) == sum(true_min_retirements[i].min_retired_cap_charge_dc_mw for i in 1:t) - @test GenX.cum_min_retired_cap_discharge_ac_mw.(gen) == sum(true_min_retirements[i].min_retired_cap_discharge_ac_mw for i in 1:t) - @test GenX.cum_min_retired_cap_charge_ac_mw.(gen) == sum(true_min_retirements[i].min_retired_cap_charge_ac_mw for i in 1:t) + @test GenX.min_retired_cap_mw.(gen) == + true_min_retirements[t].min_retired_cap_mw + @test GenX.min_retired_energy_cap_mw.(gen) == + true_min_retirements[t].min_retired_energy_cap_mw + @test GenX.min_retired_charge_cap_mw.(gen) == + true_min_retirements[t].min_retired_charge_cap_mw + @test GenX.min_retired_cap_inverter_mw.(gen) == + true_min_retirements[t].min_retired_cap_inverter_mw + @test GenX.min_retired_cap_solar_mw.(gen) == + true_min_retirements[t].min_retired_cap_solar_mw + @test GenX.min_retired_cap_wind_mw.(gen) == + true_min_retirements[t].min_retired_cap_wind_mw + @test GenX.min_retired_cap_discharge_dc_mw.(gen) == + true_min_retirements[t].min_retired_cap_discharge_dc_mw + @test GenX.min_retired_cap_charge_dc_mw.(gen) == + true_min_retirements[t].min_retired_cap_charge_dc_mw + @test GenX.min_retired_cap_discharge_ac_mw.(gen) == + true_min_retirements[t].min_retired_cap_discharge_ac_mw + @test GenX.min_retired_cap_charge_ac_mw.(gen) == + true_min_retirements[t].min_retired_cap_charge_ac_mw + + @test GenX.cum_min_retired_cap_mw.(gen) == + sum(true_min_retirements[i].min_retired_cap_mw for i in 1:t) + @test GenX.cum_min_retired_energy_cap_mw.(gen) == + sum(true_min_retirements[i].min_retired_energy_cap_mw for i in 1:t) + @test GenX.cum_min_retired_charge_cap_mw.(gen) == + sum(true_min_retirements[i].min_retired_charge_cap_mw for i in 1:t) + @test GenX.cum_min_retired_cap_inverter_mw.(gen) == + sum(true_min_retirements[i].min_retired_cap_inverter_mw for i in 1:t) + @test GenX.cum_min_retired_cap_solar_mw.(gen) == + sum(true_min_retirements[i].min_retired_cap_solar_mw for i in 1:t) + @test GenX.cum_min_retired_cap_wind_mw.(gen) == + sum(true_min_retirements[i].min_retired_cap_wind_mw for i in 1:t) + @test GenX.cum_min_retired_cap_discharge_dc_mw.(gen) == + sum(true_min_retirements[i].min_retired_cap_discharge_dc_mw for i in 1:t) + @test GenX.cum_min_retired_cap_charge_dc_mw.(gen) == + sum(true_min_retirements[i].min_retired_cap_charge_dc_mw for i in 1:t) + @test GenX.cum_min_retired_cap_discharge_ac_mw.(gen) == + sum(true_min_retirements[i].min_retired_cap_discharge_ac_mw for i in 1:t) + @test GenX.cum_min_retired_cap_charge_ac_mw.(gen) == + sum(true_min_retirements[i].min_retired_cap_charge_ac_mw for i in 1:t) end end end diff --git a/test/test_piecewisefuel.jl b/test/test_piecewisefuel.jl index a9630ce320..db52aaf0da 100644 --- a/test/test_piecewisefuel.jl +++ b/test/test_piecewisefuel.jl @@ -7,11 +7,9 @@ obj_true = 2341.82308 test_path = "piecewise_fuel" # Define test inputs -genx_setup = Dict( - "UCommit" => 2, +genx_setup = Dict("UCommit" => 2, "CO2Cap" => 1, - "ParameterScale" => 1, -) + "ParameterScale" => 1) # Run the case and get the objective value and tolerance EP, _, _ = redirect_stdout(devnull) do @@ -22,7 +20,7 @@ optimal_tol_rel = get_attribute(EP, "dual_feasibility_tolerance") optimal_tol = optimal_tol_rel * obj_test # Convert to absolute tolerance # Test the objective value -test_result = @test obj_test ≈ obj_true atol = optimal_tol +test_result = @test obj_test≈obj_true atol=optimal_tol # Round objective value and tolerance. Write to test log. obj_test = round_from_tol!(obj_test, optimal_tol) diff --git a/test/test_retrofit.jl b/test/test_retrofit.jl index 20ce1c2ea0..54ae82ad5a 100644 --- a/test/test_retrofit.jl +++ b/test/test_retrofit.jl @@ -8,8 +8,7 @@ obj_true = 3179.6244 test_path = "retrofit" # Define test inputs -genx_setup = Dict( - "CO2Cap" => 2, +genx_setup = Dict("CO2Cap" => 2, "StorageLosses" => 1, "MinCapReq" => 1, "MaxCapReq" => 1, @@ -17,8 +16,7 @@ genx_setup = Dict( "UCommit" => 2, "EnergyShareRequirement" => 1, "CapacityReserveMargin" => 1, - "MultiStage" => 0, -) + "MultiStage" => 0) # Run the case and get the objective value and tolerance EP, inputs, _ = redirect_stdout(devnull) do @@ -29,7 +27,7 @@ optimal_tol_rel = get_attribute(EP, "ipm_optimality_tolerance") optimal_tol = optimal_tol_rel * obj_test # Convert to absolute tolerance # Test the objective value -test_result = @test obj_test ≈ obj_true atol = optimal_tol +test_result = @test obj_test≈obj_true atol=optimal_tol # Round objective value and tolerance. Write to test log. obj_test = round_from_tol!(obj_test, optimal_tol) diff --git a/test/test_threezones.jl b/test/test_threezones.jl index c533da8770..5d608e0f96 100644 --- a/test/test_threezones.jl +++ b/test/test_threezones.jl @@ -8,15 +8,13 @@ obj_true = 6960.20855 test_path = "three_zones" # Define test inputs -genx_setup = Dict( - "NetworkExpansion" => 1, +genx_setup = Dict("NetworkExpansion" => 1, "Trans_Loss_Segments" => 1, "CO2Cap" => 2, "StorageLosses" => 1, "MinCapReq" => 1, "ParameterScale" => 1, - "UCommit" => 2, -) + "UCommit" => 2) # Run the case and get the objective value and tolerance EP, inputs, _ = redirect_stdout(devnull) do @@ -27,7 +25,7 @@ optimal_tol_rel = get_attribute(EP, "ipm_optimality_tolerance") optimal_tol = optimal_tol_rel * obj_test # Convert to absolute tolerance # Test the objective value -test_result = @test obj_test ≈ obj_true atol = optimal_tol +test_result = @test obj_test≈obj_true atol=optimal_tol # Round objective value and tolerance. Write to test log. obj_test = round_from_tol!(obj_test, optimal_tol) diff --git a/test/test_time_domain_reduction.jl b/test/test_time_domain_reduction.jl index 90dedfc17f..7a70df7425 100644 --- a/test/test_time_domain_reduction.jl +++ b/test/test_time_domain_reduction.jl @@ -1,6 +1,5 @@ module TestTDR - import GenX import Test import JLD2, Clustering @@ -17,7 +16,7 @@ TDR_Results_test = joinpath(test_folder, "TDR_results_test") # Folder with true clustering results for LTS and non-LTS versions TDR_Results_true = if VERSION == v"1.6.7" joinpath(test_folder, "TDR_results_true_LTS") -else +else joinpath(test_folder, "TDR_results_true") end @@ -27,23 +26,21 @@ if isdir(TDR_Results_test) end # Inputs for cluster_inputs function -genx_setup = Dict( - "TimeDomainReduction" => 1, +genx_setup = Dict("TimeDomainReduction" => 1, "TimeDomainReductionFolder" => "TDR_results_test", "UCommit" => 2, "CapacityReserveMargin" => 1, "MinCapReq" => 1, "MaxCapReq" => 1, "EnergyShareRequirement" => 1, - "CO2Cap" => 2, -) + "CO2Cap" => 2) settings = GenX.default_settings() merge!(settings, genx_setup) clustering_test = with_logger(ConsoleLogger(stderr, Logging.Warn)) do GenX.cluster_inputs(test_folder, settings_path, settings, random = false)["ClusterObject"] -end +end # Load true clustering clustering_true = JLD2.load(joinpath(TDR_Results_true, "clusters_true.jld2"))["ClusterObject"] diff --git a/test/utilities.jl b/test/utilities.jl index 43417f5462..300be0f613 100644 --- a/test/utilities.jl +++ b/test/utilities.jl @@ -4,8 +4,7 @@ using Dates using CSV, DataFrames using Logging, LoggingExtras - -const TestResult = Union{Test.Result,String} +const TestResult = Union{Test.Result, String} # Exception to throw if a csv file is not found struct CSVFileNotFound <: Exception @@ -13,11 +12,9 @@ struct CSVFileNotFound <: Exception end Base.showerror(io::IO, e::CSVFileNotFound) = print(io, e.filefullpath, " not found") -function run_genx_case_testing( - test_path::AbstractString, +function run_genx_case_testing(test_path::AbstractString, test_setup::Dict, - optimizer::Any = HiGHS.Optimizer, -) + optimizer::Any = HiGHS.Optimizer) # Merge the genx_setup with the default settings settings = GenX.default_settings() merge!(settings, test_setup) @@ -36,11 +33,9 @@ function run_genx_case_testing( return EP, inputs, OPTIMIZER end -function run_genx_case_conflict_testing( - test_path::AbstractString, +function run_genx_case_conflict_testing(test_path::AbstractString, test_setup::Dict, - optimizer::Any = HiGHS.Optimizer, -) + optimizer::Any = HiGHS.Optimizer) # Merge the genx_setup with the default settings settings = GenX.default_settings() @@ -59,11 +54,9 @@ function run_genx_case_conflict_testing( return output end -function run_genx_case_simple_testing( - test_path::AbstractString, +function run_genx_case_simple_testing(test_path::AbstractString, genx_setup::Dict, - optimizer::Any, -) + optimizer::Any) # Run the case OPTIMIZER = configure_solver(test_path, optimizer) inputs = load_inputs(genx_setup, test_path) @@ -72,29 +65,25 @@ function run_genx_case_simple_testing( return EP, inputs, OPTIMIZER end -function run_genx_case_multistage_testing( - test_path::AbstractString, +function run_genx_case_multistage_testing(test_path::AbstractString, genx_setup::Dict, - optimizer::Any, -) + optimizer::Any) # Run the case OPTIMIZER = configure_solver(test_path, optimizer) model_dict = Dict() inputs_dict = Dict() - for t = 1:genx_setup["MultiStageSettingsDict"]["NumStages"] + for t in 1:genx_setup["MultiStageSettingsDict"]["NumStages"] # Step 0) Set Model Year genx_setup["MultiStageSettingsDict"]["CurStage"] = t # Step 1) Load Inputs inpath_sub = joinpath(test_path, string("inputs_p", t)) inputs_dict[t] = load_inputs(genx_setup, inpath_sub) - inputs_dict[t] = configure_multi_stage_inputs( - inputs_dict[t], + inputs_dict[t] = configure_multi_stage_inputs(inputs_dict[t], genx_setup["MultiStageSettingsDict"], - genx_setup["NetworkExpansion"], - ) + genx_setup["NetworkExpansion"]) compute_cumulative_min_retirements!(inputs_dict, t) @@ -105,16 +94,13 @@ function run_genx_case_multistage_testing( return model_dict, inputs_dict, OPTIMIZER end - -function write_testlog( - test_path::AbstractString, +function write_testlog(test_path::AbstractString, message::AbstractString, - test_result::TestResult, -) + test_result::TestResult) # Save the results to a log file # Format: datetime, message, test result - Log_path = joinpath(@__DIR__,"Logs") + Log_path = joinpath(@__DIR__, "Logs") if !isdir(Log_path) mkdir(Log_path) end @@ -132,24 +118,20 @@ function write_testlog( end end -function write_testlog( - test_path::AbstractString, +function write_testlog(test_path::AbstractString, obj_test::Real, optimal_tol::Real, - test_result::TestResult, -) + test_result::TestResult) # Save the results to a log file # Format: datetime, objective value ± tolerance, test result message = "$obj_test ± $optimal_tol" write_testlog(test_path, message, test_result) end -function write_testlog( - test_path::AbstractString, +function write_testlog(test_path::AbstractString, obj_test::Vector{<:Real}, optimal_tol::Vector{<:Real}, - test_result::TestResult, -) + test_result::TestResult) # Save the results to a log file # Format: datetime, [objective value ± tolerance], test result @assert length(obj_test) == length(optimal_tol) @@ -227,13 +209,15 @@ Compare two columns of a DataFrame. Return true if they are identical or approxi function isapprox_col(col1, col2) if isequal(col1, col2) || (eltype(col1) <: Float64 && isapprox(col1, col2)) return true - elseif eltype(col1) <: AbstractString + elseif eltype(col1) <: AbstractString isapprox_col = true for i in eachindex(col1) - if !isapprox_col + if !isapprox_col break - elseif !isnothing(tryparse(Float64, col1[i])) && !isnothing(tryparse(Float64, col2[i])) - isapprox_col = isapprox_col && isapprox(parse(Float64, col1[i]), parse(Float64, col2[i])) + elseif !isnothing(tryparse(Float64, col1[i])) && + !isnothing(tryparse(Float64, col2[i])) + isapprox_col = isapprox_col && + isapprox(parse(Float64, col1[i]), parse(Float64, col2[i])) else isapprox_col = isapprox_col && isequal(col1[i], col2[i]) end @@ -243,7 +227,6 @@ function isapprox_col(col1, col2) return false end - macro warn_error_logger(block) quote result = nothing @@ -256,4 +239,4 @@ macro warn_error_logger(block) end result end -end \ No newline at end of file +end From 7c174e4103d7b184d838b074fa449c30f2578c35 Mon Sep 17 00:00:00 2001 From: lbonaldo Date: Thu, 4 Apr 2024 10:40:23 -0400 Subject: [PATCH 23/59] Update git-blame-ignore-revs --- .git-blame-ignore-revs | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000000..536e3289aa --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,3 @@ +# .git-blame-ignore-revs +# Standardize code formatting across project (#673) +ee3f08756584ba16a57bb701492270a7bf129b4d From c53eb01965df2cfecb0493a341672f966db619d2 Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Thu, 4 Apr 2024 11:53:06 -0400 Subject: [PATCH 24/59] Update configure_multi_stage_inputs.jl (#666) Add check on capital recovery period with non-zero investment cost --- CHANGELOG.md | 2 ++ .../configure_multi_stage_inputs.jl | 22 ++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c372739aaa..6bdeda7065 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New settings parameter, StorageVirtualDischarge, to turn storage virtual charging and discharging off if desired by the user (#638). - Add module to retrofit existing resources with new technologies (#600). - Formatted the code and added a format check to the CI pipeline (#673). +- Add check when capital recovery period is zero and investment costs are + non-zero in multi-stage GenX (#666) ### Fixed - Set MUST_RUN=1 for RealSystemExample/small_hydro plants (#517). diff --git a/src/multi_stage/configure_multi_stage_inputs.jl b/src/multi_stage/configure_multi_stage_inputs.jl index 0bd5d45928..bbf4bb3431 100644 --- a/src/multi_stage/configure_multi_stage_inputs.jl +++ b/src/multi_stage/configure_multi_stage_inputs.jl @@ -22,16 +22,24 @@ NOTE: The inv\_costs\_yr and crp arrays must be the same length; values with the returns: array object containing overnight capital costs, the discounted sum of annual investment costs incured within the model horizon. """ function compute_overnight_capital_cost(settings_d::Dict, - inv_costs_yr::Array, - crp::Array, - tech_wacc::Array) + inv_costs_yr::Array, + crp::Array, + tech_wacc::Array) + + # Check for resources with non-zero investment costs and a Capital_Recovery_Period value of 0 years + if any((crp .== 0) .& (inv_costs_yr .> 0)) + msg = "You have some resources with non-zero investment costs and a Capital_Recovery_Period value of 0 years.\n" * + "These resources will have a calculated overnight capital cost of \$0. Correct your inputs if this is a mistake.\n" + error(msg) + end + cur_stage = settings_d["CurStage"] # Current model num_stages = settings_d["NumStages"] # Total number of model stages stage_lens = settings_d["StageLengths"] # 1) For each resource, find the minimum of the capital recovery period and the end of the model horizon # Total time between the end of the final model stage and the start of the current stage - model_yrs_remaining = sum(stage_lens[cur_stage:end]) + model_yrs_remaining = sum(stage_lens[cur_stage:end]; init = 0) # We will sum annualized costs through the full capital recovery period or the end of planning horizon, whichever comes first payment_yrs_remaining = min.(crp, model_yrs_remaining) @@ -41,8 +49,10 @@ function compute_overnight_capital_cost(settings_d::Dict, # (Factor to adjust discounting to year 0 for capital cost is included in the discounting coefficient applied to all terms in the objective function value.) occ = zeros(length(inv_costs_yr)) for i in 1:length(occ) - occ[i] = sum(inv_costs_yr[i] / (1 + tech_wacc[i]) .^ (p) - for p in 1:payment_yrs_remaining[i]) + occ[i] = sum( + inv_costs_yr[i] / (1 + tech_wacc[i]) .^ (p) + for p in 1:payment_yrs_remaining[i]; + init = 0) end # 3) Return the overnight capital cost (discounted sum of annual investment costs incured within the model horizon) From 3d2438adb5f6490d15f8216b493a14abecfe4fbe Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Thu, 4 Apr 2024 13:43:20 -0400 Subject: [PATCH 25/59] Cleanup of the doc rendering issues * Table rendering for Generators_data.csv fixed * Fixed rendering of transmission reinforcement cost term in the description of objective_function.md * Fixed doc rendering issue with fuel.jl and investment_transmission.jl * Removed twice repeated Retrofit page * Fixed the write output doc fix and API doc fix; * Corrected write-up for capacity value * Deleted Write Setting File, since there's no such file * Doc rendering fixed on Write_Outputs --- docs/make.jl | 1 - .../objective_function.md | 2 +- docs/src/Model_Reference/write_outputs.md | 9 ++-- docs/src/User_Guide/model_input.md | 25 ++++++----- src/model/core/fuel.jl | 4 +- .../transmission/investment_transmission.jl | 44 +++++++++---------- src/model/generate_model.jl | 2 +- .../effective_capacity.jl | 2 +- .../write_capacity_value.jl | 27 +++++++++--- src/write_outputs/choose_output_dir.jl | 4 +- src/write_outputs/dftranspose.jl | 6 +-- src/write_outputs/write_outputs.jl | 5 +++ 12 files changed, 74 insertions(+), 57 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index f31c297eeb..70479f3adb 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -70,7 +70,6 @@ pages = OrderedDict("Welcome Page" => [ "Thermal No Commit" => "Model_Reference/Resources/thermal_no_commit.md", ], "Hydrogen Electrolyzers" => "Model_Reference/Resources/electrolyzers.md", - "Retrofit" => "Model_Reference/Resources/retrofit.md", "Scheduled maintenance for various resources" => "Model_Reference/Resources/maintenance.md", "Resource types" => "Model_Reference/Resources/resource.md", ], diff --git a/docs/src/Model_Concept_Overview/objective_function.md b/docs/src/Model_Concept_Overview/objective_function.md index 53a8a29ed1..40f90e7d4a 100644 --- a/docs/src/Model_Concept_Overview/objective_function.md +++ b/docs/src/Model_Concept_Overview/objective_function.md @@ -56,7 +56,7 @@ The seventh summation represents the total cost of not meeting hourly operating The eighth summation corresponds to the startup costs incurred by technologies to which unit commitment decisions apply (e.g. $y \in \mathcal{UC}$), equal to the cost of start-up, $\pi^{START}_{y,z}$, times the number of startup events, $\chi_{y,z,t}$, for the cluster of units in each zone and time step (weighted by $\omega_t$). The ninth term corresponds to the transmission reinforcement or construction costs, for each transmission line (if modeled). -Transmission reinforcement costs are equal to the sum across all lines of the product between the transmission reinforcement/construction cost, $pi^{TCAP}_{l}$, times the additional transmission capacity variable, $\bigtriangleup\varphi^{max}_{l}$. Note that fixed O&M and replacement capital costs (depreciation) for existing transmission capacity is treated as a sunk cost and not included explicitly in the GenX objective function. +Transmission reinforcement costs are equal to the sum across all lines of the product between the transmission reinforcement/construction cost, $\pi^{TCAP}_{l}$, times the additional transmission capacity variable, $\bigtriangleup\varphi^{max}_{l}$. Note that fixed O&M and replacement capital costs (depreciation) for existing transmission capacity is treated as a sunk cost and not included explicitly in the GenX objective function. The tenth term onwards specifically relates to the breakdown investment, fixed O&M, and variable O&M costs associated with each configurable component of a co-located VRE and storage resource. The tenth term represents to the fixed cost of installed inverter capacity and is summed over only the co-located resources with an inverter component ($y \in \mathcal{VS}^{inv}$). diff --git a/docs/src/Model_Reference/write_outputs.md b/docs/src/Model_Reference/write_outputs.md index 2b9ae15b3d..1c50d00dcb 100644 --- a/docs/src/Model_Reference/write_outputs.md +++ b/docs/src/Model_Reference/write_outputs.md @@ -38,7 +38,7 @@ Modules = [GenX] Pages = ["write_capacity.jl"] ``` -## Write Capacity Value # TODO: add it +## Write Capacity Value ```@autodocs Modules = [GenX] Pages = ["write_capacity_value.jl"] @@ -162,8 +162,7 @@ Pages = ["write_maintenance.jl"] GenX.write_angles ``` -## Write Settings files -```@autodocs -Modules = [GenX] -Pages = ["write_settings.jl"] +## Write Settings Files +```@docs +GenX.write_settings_file ``` diff --git a/docs/src/User_Guide/model_input.md b/docs/src/User_Guide/model_input.md index c30a595dcf..a789eab93a 100644 --- a/docs/src/User_Guide/model_input.md +++ b/docs/src/User_Guide/model_input.md @@ -560,17 +560,20 @@ In addition to the files described above, the `resources` folder can contain add This file contains the time-series of capacity factors / availability of each resource included in the resource `.csv` file in the `resources` folder for each time step (e.g. hour) modeled. -• First column: The first column contains the time index of each row (starting in the second row) from 1 to N. - -• Second column onwards: Resources are listed from the second column onward with headers matching each resource name in the resource `.csv` file in the `resources` folder in any order. The availability for each resource at each time step is defined as a fraction of installed capacity and should be between 0 and 1. Note that for this reason, resource names specified in the resource `.csv` file must be unique. Note that for Hydro reservoir resources (i.e. `Hydro.csv`), values in this file correspond to inflows (in MWhs) to the hydro reservoir as a fraction of installed power capacity, rather than hourly capacity factor. Note that for co-located VRE and storage resources, solar PV and wind resource profiles should not be located in this file but rather in separate variability files (these variabilities can be in the `Generators_variability.csv` if time domain reduction functionalities will be utilized because the time domain reduction functionalities will separate the files after the clustering is completed). +1) First column: The first column contains the time index of each row (starting in the second row) from 1 to N. +2) Second column onwards: Resources are listed from the second column onward with headers matching each resource name in the resource `.csv` file in the `resources` folder in any order. The availability for each resource at each time step is defined as a fraction of installed capacity and should be between 0 and 1. Note that for this reason, resource names specified in the resource `.csv` file must be unique. Note that for Hydro reservoir resources (i.e. `Hydro.csv`), values in this file correspond to inflows (in MWhs) to the hydro reservoir as a fraction of installed power capacity, rather than hourly capacity factor. Note that for co-located VRE and storage resources, solar PV and wind resource profiles should not be located in this file but rather in separate variability files (these variabilities can be in the `Generators_variability.csv` if time domain reduction functionalities will be utilized because the time domain reduction functionalities will separate the files after the clustering is completed). +###### Table 17: Structure of the Generator\_variability.csv file +--- +|**Column Name** | **Description**| +| :------------ | :-----------| +|Resource| Resource name corresponding to a resource in one of the resource data files described above.| |Self\_Disch |[0,1], The power loss of storage technologies per hour (fraction loss per hour)- only applies to storage techs. Note that for co-located VRE-STOR resources, this value applies to the storage component of each resource.| |Min\_Power |[0,1], The minimum generation level for a unit as a fraction of total capacity. This value cannot be higher than the smallest time-dependent CF value for a resource in `Generators_variability.csv`. Applies to thermal plants, and reservoir hydro resource (`HYDRO = 1`).| |Ramp\_Up\_Percentage |[0,1], Maximum increase in power output from between two periods (typically hours), reported as a fraction of nameplate capacity. Applies to thermal plants, and reservoir hydro resource (`HYDRO = 1`).| |Ramp\_Dn\_Percentage |[0,1], Maximum decrease in power output from between two periods (typically hours), reported as a fraction of nameplate capacity. Applies to thermal plants, and reservoir hydro resource (`HYDRO = 1`).| |Eff\_Up |[0,1], Efficiency of charging storage – applies to storage technologies (all STOR types except co-located storage resources).| |Eff\_Down |[0,1], Efficiency of discharging storage – applies to storage technologies (all STOR types except co-located storage resources). | - |Min\_Duration |Specifies the minimum ratio of installed energy to discharged power capacity that can be installed. Applies to STOR types 1 and 2 (hours). Note that for co-located VRE-STOR resources, this value does not apply. | |Max\_Duration |Specifies the maximum ratio of installed energy to discharged power capacity that can be installed. Applies to STOR types 1 and 2 (hours). Note that for co-located VRE-STOR resources, this value does not apply. | |Max\_Flexible\_Demand\_Delay |Maximum number of hours that demand can be deferred or delayed. Applies to resources with FLEX type 1 (hours). | @@ -599,7 +602,7 @@ This file contains the time-series of capacity factors / availability of the win This file includes parameter inputs needed to model time-dependent procurement of regulation and spinning reserves. This file is needed if `OperationalReserves` flag is activated in the YAML file `genx_settings.yml`. -###### Table 7: Structure of the Operational_reserves.csv file +###### Table 18: Structure of the Operational_reserves.csv file --- |**Column Name** | **Description**| | :------------ | :-----------| @@ -621,7 +624,7 @@ This file contains inputs specifying minimum energy share requirement policies, Note: this file should use the same region name as specified in the the resource `.csv` file (inside the `Resource`). -###### Table 8: Structure of the Energy\_share\_requirement.csv file +###### Table 19: Structure of the Energy\_share\_requirement.csv file --- |**Column Name** | **Description**| | :------------ | :-----------| @@ -635,7 +638,7 @@ Note: this file should use the same region name as specified in the the resource This file contains inputs specifying CO2 emission limits policies (e.g. emissions cap and permit trading programs). This file is needed if `CO2Cap` flag is activated in the YAML file `genx_settings.yml`. `CO2Cap` flag set to 1 represents mass-based (tCO2 ) emission target. `CO2Cap` flag set to 2 is specified when emission target is given in terms of rate (tCO2/MWh) and is based on total demand met. `CO2Cap` flag set to 3 is specified when emission target is given in terms of rate (tCO2 /MWh) and is based on total generation. -###### Table 9: Structure of the CO2\_cap.csv file +###### Table 20: Structure of the CO2\_cap.csv file --- |**Column Name** | **Description**| | :------------ | :-----------| @@ -652,7 +655,7 @@ This file contains the regional capacity reserve margin requirements. This file Note: this file should use the same region name as specified in the resource `.csv` file (inside the `Resource`). -###### Table 10: Structure of the Capacity\_reserve\_margin.csv file +###### Table 21: Structure of the Capacity\_reserve\_margin.csv file --- |**Column Name** | **Description**| | :------------ | :-----------| @@ -666,7 +669,7 @@ Note: this file should use the same region name as specified in the resource `.c This file contains the minimum capacity carve-out requirement to be imposed (e.g. a storage capacity mandate or offshore wind capacity mandate). This file is needed if the `MinCapReq` flag has a non-zero value in the YAML file `genx_settings.yml`. -###### Table 11: Structure of the Minimum\_capacity\_requirement.csv file +###### Table 22: Structure of the Minimum\_capacity\_requirement.csv file --- |**Column Name** | **Description**| | :------------ | :-----------| @@ -682,7 +685,7 @@ Some of the columns specified in the input files in Section 2.2 and 2.1 are not This contains the maximum capacity limits to be imposed (e.g. limits on total deployment of solar, wind, or batteries in the system as a whole or in certain collections of zones). It is required if the `MaxCapReq` flag has a non-zero value in `genx_settings.yml`. -###### Table 12: Structure of the Maximum\_capacity\_requirement.csv file +###### Table 23: Structure of the Maximum\_capacity\_requirement.csv file --- |**Column Name** | **Description**| | :------------ | :-----------| @@ -697,7 +700,7 @@ Some of the columns specified in the input files in Section 2.2 and 2.1 are not This file contains the settings parameters required to run the Method of Morris algorithm in GenX. This file is needed if the `MethodofMorris` flag is ON in the YAML file `genx_settings.yml`. -###### Table 13: Structure of the Method\_of\_morris\_range.csv file +###### Table 24: Structure of the Method\_of\_morris\_range.csv file --- |**Column Name** | **Description**| | :------------ | :-----------| diff --git a/src/model/core/fuel.jl b/src/model/core/fuel.jl index 253cc50985..30c4f73ef2 100644 --- a/src/model/core/fuel.jl +++ b/src/model/core/fuel.jl @@ -56,7 +56,7 @@ fuel $i$ consumption by plant $y$ at time $t$ ($vMulFuel_{y,i,t}$); startup fuel For plants using multiple fuels: -During startup, heat input from multiple startup fuels are equal to startup fuel requirements in plant $y$ at time $t$: $StartFuelMMBTUperMW$ times $Capsize$. +During startup, heat input from multiple startup fuels are equal to startup fuel requirements in plant $y$ at time $t$: $StartFuelMMBTUperMW$ $\times$ $Capsize$. ```math \begin{aligned} \sum_{i \in \mathcal{I} } vMulStartFuels_{y, i, t}= CapSize_{y} \times StartFuelMMBTUperMW_{y} \times vSTART_{y,t} @@ -76,7 +76,7 @@ vMulFuels_{y, i, t} >= vPower_{y,t} \times MinCofire_{i} \begin{aligned} vMulFuels_{y, i, t} <= vPower_{y,t} \times MaxCofire_{i} \end{aligned} - +``` """ function fuel!(EP::Model, inputs::Dict, setup::Dict) println("Fuel Module") diff --git a/src/model/core/transmission/investment_transmission.jl b/src/model/core/transmission/investment_transmission.jl index 813c06aab3..804588e22c 100644 --- a/src/model/core/transmission/investment_transmission.jl +++ b/src/model/core/transmission/investment_transmission.jl @@ -1,27 +1,27 @@ @doc raw""" function investment_transmission!(EP::Model, inputs::Dict, setup::Dict) - The function model transmission expansion and adds transmission reinforcement or construction costs to the objective function. Transmission reinforcement costs are equal to the sum across all lines of the product between the transmission reinforcement/construction cost, $pi^{TCAP}_{l}$, times the additional transmission capacity variable, $\bigtriangleup\varphi^{cap}_{l}$. - ```math - \begin{aligned} - & \sum_{l \in \mathcal{L}}\left(\pi^{TCAP}_{l} \times \bigtriangleup\varphi^{cap}_{l}\right) - \end{aligned} - ``` - Note that fixed O\&M and replacement capital costs (depreciation) for existing transmission capacity is treated as a sunk cost and not included explicitly in the GenX objective function. - **Accounting for Transmission Between Zones** - Available transmission capacity between zones is set equal to the existing line's maximum power transfer capacity, $\overline{\varphi^{cap}_{l}}$, plus any transmission capacity added on that line (for lines eligible for expansion in the set $\mathcal{E}$). - ```math - \begin{aligned} - &\varphi^{cap}_{l} = \overline{\varphi^{cap}_{l}} , &\quad \forall l \in (\mathcal{L} \setminus \mathcal{E} ),\forall t \in \mathcal{T}\\ - % trasmission expansion - &\varphi^{cap}_{l} = \overline{\varphi^{cap}_{l}} + \bigtriangleup\varphi^{cap}_{l} , &\quad \forall l \in \mathcal{E},\forall t \in \mathcal{T} - \end{aligned} - ``` - The additional transmission capacity, $\bigtriangleup\varphi^{cap}_{l} $, is constrained by a maximum allowed reinforcement, $\overline{\bigtriangleup\varphi^{cap}_{l}}$, for each line $l \in \mathcal{E}$. - ```math - \begin{aligned} - & \bigtriangleup\varphi^{cap}_{l} \leq \overline{\bigtriangleup\varphi^{cap}_{l}}, &\quad \forall l \in \mathcal{E} - \end{aligned} - ``` +This function model transmission expansion and adds transmission reinforcement or construction costs to the objective function. Transmission reinforcement costs are equal to the sum across all lines of the product between the transmission reinforcement/construction cost, $pi^{TCAP}_{l}$, times the additional transmission capacity variable, $\bigtriangleup\varphi^{cap}_{l}$. +```math +\begin{aligned} + & \sum_{l \in \mathcal{L}}\left(\pi^{TCAP}_{l} \times \bigtriangleup\varphi^{cap}_{l}\right) +\end{aligned} +``` +Note that fixed O\&M and replacement capital costs (depreciation) for existing transmission capacity is treated as a sunk cost and not included explicitly in the GenX objective function. +**Accounting for Transmission Between Zones** +Available transmission capacity between zones is set equal to the existing line's maximum power transfer capacity, $\overline{\varphi^{cap}_{l}}$, plus any transmission capacity added on that line (for lines eligible for expansion in the set $\mathcal{E}$). +```math +\begin{aligned} + &\varphi^{cap}_{l} = \overline{\varphi^{cap}_{l}} , &\quad \forall l \in (\mathcal{L} \setminus \mathcal{E} ),\forall t \in \mathcal{T}\\ + % trasmission expansion + &\varphi^{cap}_{l} = \overline{\varphi^{cap}_{l}} + \bigtriangleup\varphi^{cap}_{l} , &\quad \forall l \in \mathcal{E},\forall t \in \mathcal{T} +\end{aligned} +``` +The additional transmission capacity, $\bigtriangleup\varphi^{cap}_{l} $, is constrained by a maximum allowed reinforcement, $\overline{\bigtriangleup\varphi^{cap}_{l}}$, for each line $l \in \mathcal{E}$. +```math +\begin{aligned} + & \bigtriangleup\varphi^{cap}_{l} \leq \overline{\bigtriangleup\varphi^{cap}_{l}}, &\quad \forall l \in \mathcal{E} +\end{aligned} +``` """ function investment_transmission!(EP::Model, inputs::Dict, setup::Dict) println("Investment Transmission Module") diff --git a/src/model/generate_model.jl b/src/model/generate_model.jl index ff16f66875..18f3f98775 100644 --- a/src/model/generate_model.jl +++ b/src/model/generate_model.jl @@ -42,7 +42,7 @@ The seventh summation represents the total cost of not meeting hourly operating The eighth summation corresponds to the startup costs incurred by technologies to which unit commitment decisions apply (e.g. $y \in \mathcal{UC}$), equal to the cost of start-up, $\pi^{START}_{y,z}$, times the number of startup events, $\chi_{y,z,t}$, for the cluster of units in each zone and time step (weighted by $\omega_t$). -The last term corresponds to the transmission reinforcement or construction costs, for each transmission line in the model. Transmission reinforcement costs are equal to the sum across all lines of the product between the transmission reinforcement/construction cost, $pi^{TCAP}_{l}$, times the additional transmission capacity variable, $\bigtriangleup\varphi^{max}_{l}$. Note that fixed O\&M and replacement capital costs (depreciation) for existing transmission capacity is treated as a sunk cost and not included explicitly in the GenX objective function. +The last term corresponds to the transmission reinforcement or construction costs, for each transmission line in the model. Transmission reinforcement costs are equal to the sum across all lines of the product between the transmission reinforcement/construction cost, $\pi^{TCAP}_{l}$, times the additional transmission capacity variable, $\bigtriangleup\varphi^{max}_{l}$. Note that fixed O\&M and replacement capital costs (depreciation) for existing transmission capacity is treated as a sunk cost and not included explicitly in the GenX objective function. In summary, the objective function can be understood as the minimization of costs associated with five sets of different decisions: (1) where and how to invest on capacity, (2) how to dispatch or operate that capacity, (3) which consumer demand segments to serve or curtail, (4) how to cycle and commit thermal units subject to unit commitment decisions, (5) and where and how to invest in additional transmission network capacity to increase power transfer capacity between zones. Note however that each of these components are considered jointly and the optimization is performed over the whole problem at once as a monolithic co-optimization problem. diff --git a/src/write_outputs/capacity_reserve_margin/effective_capacity.jl b/src/write_outputs/capacity_reserve_margin/effective_capacity.jl index 60a70ecd86..92980eabf3 100644 --- a/src/write_outputs/capacity_reserve_margin/effective_capacity.jl +++ b/src/write_outputs/capacity_reserve_margin/effective_capacity.jl @@ -3,7 +3,7 @@ inputs::Dict, resources::Vector{Int}, capres_zone::Int, - timesteps::Vector{Int})::Matrix{Float64} + timesteps::Vector{Int})::Matrix{Float64}) Effective capacity in a capacity reserve margin zone for certain resources in the given timesteps. """ diff --git a/src/write_outputs/capacity_reserve_margin/write_capacity_value.jl b/src/write_outputs/capacity_reserve_margin/write_capacity_value.jl index 2c902862d3..6301fa177c 100644 --- a/src/write_outputs/capacity_reserve_margin/write_capacity_value.jl +++ b/src/write_outputs/capacity_reserve_margin/write_capacity_value.jl @@ -1,3 +1,20 @@ +@doc raw""" + write_capacity_value(path::AbstractString, + inputs::Dict, + setup::Dict, + EP::Model) + +This is the value of the derated capacities of different types of resources multiplied by the power generated by each of them + +# Arguments +- path::AbstractString: Path to the directory where the file will be written. +- inputs::Dict: Dictionary of input data. +- setup::Dict: Dictionary of setup data. +- EP::Model: EnergyModel object. + +# Results +- A CSV file named "CapacityValue.csv" is written to the directory specified by `path`. +""" function write_capacity_value(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) gen = inputs["RESOURCES"] zones = zone_id.(gen) @@ -139,13 +156,9 @@ end setup::Dict, capres_zone::Int)::Vector{Float64} -Marginal electricity price for each model zone and time step. -This is equal to the dual variable of the power balance constraint. -When solving a linear program (i.e. linearized unit commitment or economic dispatch) -this output is always available; when solving a mixed integer linear program, this can -be calculated only if `WriteShadowPrices` is activated. - - Returns a vector, with units of $/MW +Marginal price for capacity constraint. +This is equal to the dual variable of the capacity constraint. +Returns a vector, with units of $/MW """ function capacity_reserve_margin_price(EP::Model, inputs::Dict, diff --git a/src/write_outputs/choose_output_dir.jl b/src/write_outputs/choose_output_dir.jl index 2da796944d..43a4878e8e 100644 --- a/src/write_outputs/choose_output_dir.jl +++ b/src/write_outputs/choose_output_dir.jl @@ -1,5 +1,5 @@ -""" - path = choose_output_dir(pathinit) +@doc raw""" + choose_output_dir(pathinit) Avoid overwriting (potentially important) existing results by appending to the directory name\n Checks if the suggested output directory already exists. While yes, it appends _1, _2, etc till an unused name is found diff --git a/src/write_outputs/dftranspose.jl b/src/write_outputs/dftranspose.jl index 21c8295899..ec9d1a5f39 100644 --- a/src/write_outputs/dftranspose.jl +++ b/src/write_outputs/dftranspose.jl @@ -10,12 +10,10 @@ ## Note this function is necessary because no stock function to transpose ## DataFrames appears to exist. ################################################################################ -""" +@doc raw""" df = dftranspose(df::DataFrame, withhead::Bool) -Returns a transpose of a Dataframe.\n -FIXME: This is for DataFrames@0.20.2, as used in GenX. -Versions 0.21+ could use stack and unstack to make further changes while retaining the order +Returns a transpose of a Dataframe. """ function dftranspose(df::DataFrame, withhead::Bool) if withhead diff --git a/src/write_outputs/write_outputs.jl b/src/write_outputs/write_outputs.jl index 2c374c0b35..d8933533e3 100644 --- a/src/write_outputs/write_outputs.jl +++ b/src/write_outputs/write_outputs.jl @@ -500,6 +500,11 @@ function write_fulltimeseries(fullpath::AbstractString, return nothing end +""" + write_settings_file(path, setup) + +Internal function for writing settings files +""" function write_settings_file(path, setup) YAML.write_file(joinpath(path, "run_settings.yml"), setup) end From d5d5febfb17d7c5c57e0e925e80cb9ea50f24781 Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Fri, 5 Apr 2024 15:17:04 -0400 Subject: [PATCH 26/59] Added Jack Morris's ORCID ID --- .zenodo.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.zenodo.json b/.zenodo.json index facc5c3cac..97590da304 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -52,7 +52,8 @@ }, { "name": "Morris, Jack", - "affiliation": "MITRE" + "affiliation": "MITRE", + "orcid": "0000-0002-1471-9708" }, { "name": "Patankar, Neha", From 1555217f322beae2ce062ad8a37b8c0e658a50e2 Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Fri, 5 Apr 2024 15:36:17 -0400 Subject: [PATCH 27/59] Updated CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bdeda7065..cbbde25d73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Formatted the code and added a format check to the CI pipeline (#673). - Add check when capital recovery period is zero and investment costs are non-zero in multi-stage GenX (#666) +- Added condition number scaling added to objective function (#667) +- Added versioned doc-pages for v0.3.6 and v0.4.0 ### Fixed - Set MUST_RUN=1 for RealSystemExample/small_hydro plants (#517). @@ -60,6 +62,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix modeling of hydro reservoir with long duration storage (#572). - Fix update of starting transmission capacity in multistage GenX - Fix write_status with UCommit = WriteShadowPrices = 1 (#645) +- Fixed outputting capital recovery cost to 0 if the remaining number of years is 0 (#666) +- Updated the docstring for the initialize_cost_to_go function and adjusted the formula for the discount factor to reflect the code implementation (#672). ### Changed - Use add_to_expression! instead of the += and -= operators for memory performance improvements (#498). @@ -89,6 +93,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `policies`, `resources` and `system`. The examples and tests have been updated to reflect this change. - New folder structure implemented for `example_system`. This folder now consists of nine separate folders each pertaining to a different case study example, ranging from the ISONE three zones, with singlestage, multistage, electrolyzers, all the way to the 9 bus IEEE case for running DC-OPF. +- Pruned HiGHS solver settings to the necessary minimum (#668) +- Changed deploydoc URL to GenX.jl (#662) ### Deprecated - The above `load` keys, which generally refer to electrical demand, are being deprecated. From 106f40dfe261163ed4cb29e7827501b0325890f2 Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Fri, 5 Apr 2024 16:39:20 -0400 Subject: [PATCH 28/59] Fixed doc page rendering errors on model inputs --- docs/src/User_Guide/model_input.md | 83 +++++++++++++++--------------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/docs/src/User_Guide/model_input.md b/docs/src/User_Guide/model_input.md index a789eab93a..c4812683b7 100644 --- a/docs/src/User_Guide/model_input.md +++ b/docs/src/User_Guide/model_input.md @@ -202,9 +202,9 @@ Each file contains cost and performance parameters for various generators and ot |PWFU\_Heat\_Rate\_MMBTU\_per\_MWh\_*i| The slope of fuel usage function of the segment i.| |PWFU\_Load\_Point\_MW\_*i| The end of segment i (MW).| |**Multi-fuel parameters**| -|MULTI_FUELS | {0, 1}, Flag to indicate membership in set of thermal resources that can burn multiple fuels at the same time (e.g., natural gas combined cycle cofiring with hydrogen, coal power plant cofiring with natural gas.| -||MULTI_FUELS = 0: Not part of set (default) | -||MULTI_FUELS = 1: Resources that can use fuel blending. | +|MULTI\_FUELS | {0, 1}, Flag to indicate membership in set of thermal resources that can burn multiple fuels at the same time (e.g., natural gas combined cycle cofiring with hydrogen, coal power plant cofiring with natural gas.| +||MULTI\_FUELS = 0: Not part of set (default) | +||MULTI\_FUELS = 1: Resources that can use fuel blending. | |Num\_Fuels |Number of fuels that a multi-fuel generator (MULTI_FUELS = 1) can use at the same time. The length of ['Fuel1', 'Fuel2', ...] should be equal to 'Num\_Fuels'. Each fuel will requires its corresponding heat rate, min cofire level, and max cofire level. | |Fuel1 |Frist fuel needed for a mulit-fuel generator (MULTI_FUELS = 1). The names should match with the ones in the `Fuels_data.csv`. | |Fuel2 |Second fuel needed for a mulit-fuel generator (MULTI_FUELS = 1). The names should match with the ones in the `Fuels_data.csv`. | @@ -329,9 +329,9 @@ Each file contains cost and performance parameters for various generators and ot --- |**Column Name** | **Description**| | :------------ | :-----------| -|Hydrogen_MWh_Per_Tonne| Electrolyzer efficiency in megawatt-hours (MWh) of electricity per metric tonne of hydrogen produced (MWh/t)| -|Electrolyzer_Min_kt| Minimum annual quantity of hydrogen that must be produced by electrolyzer in kilotonnes (kt)| -|Hydrogen_Price_Per_Tonne| Price (or value) of hydrogen per metric tonne ($/t)| +|Hydrogen\_MWh\_Per\_Tonne| Electrolyzer efficiency in megawatt-hours (MWh) of electricity per metric tonne of hydrogen produced (MWh/t)| +|Electrolyzer\_Min\_kt| Minimum annual quantity of hydrogen that must be produced by electrolyzer in kilotonnes (kt)| +|Hydrogen\_Price\_Per\_Tonne| Price (or value) of hydrogen per metric tonne ($/t)| |Min\_Power |[0,1], The minimum generation level for a unit as a fraction of total capacity. This value cannot be higher than the smallest time-dependent CF value for a resource in `Generators_variability.csv`.| |Ramp\_Up\_Percentage |[0,1], Maximum increase in power output from between two periods (typically hours), reported as a fraction of nameplate capacity.| |Ramp\_Dn\_Percentage |[0,1], Maximum decrease in power output from between two periods (typically hours), reported as a fraction of nameplate capacity.| @@ -350,25 +350,25 @@ Each co-located VRE and storage resource can be easily configured to contain eit |WIND | {0, 1}, Flag to indicate membership in the set of co-located VRE-storage resources with a wind component.| ||WIND = 0: Not part of set (default) | ||WIND = 1: If the co-located VRE-storage resource can produce wind energy. || -|STOR_DC_DISCHARGE | {0, 1, 2}, Flag to indicate membership in set of co-located VRE-storage resources that discharge behind the meter and through the inverter (DC).| -||STOR_DC_DISCHARGE = 0: Not part of set (default) | -||STOR_DC_DISCHARGE = 1: If the co-located VRE-storage resource contains symmetric charge/discharge power capacity with charging capacity equal to discharging capacity (e.g. lithium-ion battery storage). Note that if STOR_DC_DISCHARGE = 1, STOR_DC_CHARGE = 1.| -||STOR_DC_DISCHARGE = 2: If the co-located VRE-storage resource has asymmetric discharge capacities using distinct processes (e.g. hydrogen electrolysis, storage, and conversion to power using fuel cell or combustion turbine).| -|STOR_DC_CHARGE | {0, 1, 2}, Flag to indicate membership in set of co-located VRE-storage resources that charge through the inverter (DC).| -||STOR_DC_CHARGE = 0: Not part of set (default) | -||STOR_DC_CHARGE = 1: If the co-located VRE-storage resource contains symmetric charge/discharge power capacity with charging capacity equal to discharging capacity (e.g. lithium-ion battery storage). Note that if STOR_DC_CHARGE = 1, STOR_DC_DISCHARGE = 1.| -||STOR_DC_CHARGE = 2: If the co-located VRE-storage resource has asymmetric charge capacities using distinct processes (e.g. hydrogen electrolysis, storage, and conversion to power using fuel cell or combustion turbine).| -|STOR_AC_DISCHARGE | {0, 1, 2}, Flag to indicate membership in set of co-located VRE-storage resources that discharges AC.| -||STOR_AC_DISCHARGE = 0: Not part of set (default) | -||STOR_AC_DISCHARGE = 1: If the co-located VRE-storage resource contains symmetric charge/discharge power capacity with charging capacity equal to discharging capacity (e.g. lithium-ion battery storage). Note that if STOR_AC_DISCHARGE = 1, STOR_AC_CHARGE = 1.| -||STOR_AC_DISCHARGE = 2: If the co-located VRE-storage resource has asymmetric discharge capacities using distinct processes (e.g. hydrogen electrolysis, storage, and conversion to power using fuel cell or combustion turbine).| -|STOR_AC_CHARGE | {0, 1, 2}, Flag to indicate membership in set of co-located VRE-storage resources that charge AC.| -||STOR_AC_CHARGE = 0: Not part of set (default) | -||STOR_AC_CHARGE = 1: If the co-located VRE-storage resource contains symmetric charge/discharge power capacity with charging capacity equal to discharging capacity (e.g. lithium-ion battery storage). Note that if STOR_AC_CHARGE = 1, STOR_AC_DISCHARGE = 1.| -||STOR_AC_CHARGE = 2: If the co-located VRE-storage resource has asymmetric charge capacities using distinct processes (e.g. hydrogen electrolysis, storage, and conversion to power using fuel cell or combustion turbine).| -|LDS_VRE_STOR | {0, 1}, Flag to indicate the co-located VRE-storage resources eligible for long duration storage constraints with inter period linkage (e.g., reservoir hydro, hydrogen storage). | -||LDS_VRE_STOR = 0: Not part of set (default) | -||LDS_VRE_STOR = 1: Long duration storage resources| +|STOR\_DC\_DISCHARGE | {0, 1, 2}, Flag to indicate membership in set of co-located VRE-storage resources that discharge behind the meter and through the inverter (DC).| +||STOR\_DC\_DISCHARGE = 0: Not part of set (default) | +||STOR\_DC\_DISCHARGE = 1: If the co-located VRE-storage resource contains symmetric charge/discharge power capacity with charging capacity equal to discharging capacity (e.g. lithium-ion battery storage). Note that if STOR\_DC\_DISCHARGE = 1, STOR\_DC\_CHARGE = 1.| +||STOR\_DC\_DISCHARGE = 2: If the co-located VRE-storage resource has asymmetric discharge capacities using distinct processes (e.g. hydrogen electrolysis, storage, and conversion to power using fuel cell or combustion turbine).| +|STOR\_DC\_CHARGE | {0, 1, 2}, Flag to indicate membership in set of co-located VRE-storage resources that charge through the inverter (DC).| +||STOR\_DC\_CHARGE = 0: Not part of set (default) | +||STOR\_DC\_CHARGE = 1: If the co-located VRE-storage resource contains symmetric charge/discharge power capacity with charging capacity equal to discharging capacity (e.g. lithium-ion battery storage). Note that if STOR\_DC\_CHARGE = 1, STOR\_DC\_DISCHARGE = 1.| +||STOR\_DC\_CHARGE = 2: If the co-located VRE-storage resource has asymmetric charge capacities using distinct processes (e.g. hydrogen electrolysis, storage, and conversion to power using fuel cell or combustion turbine).| +|STOR\_AC\_DISCHARGE | {0, 1, 2}, Flag to indicate membership in set of co-located VRE-storage resources that discharges AC.| +||STOR\_AC\_DISCHARGE = 0: Not part of set (default) | +||STOR\_AC\_DISCHARGE = 1: If the co-located VRE-storage resource contains symmetric charge/discharge power capacity with charging capacity equal to discharging capacity (e.g. lithium-ion battery storage). Note that if STOR\_AC\_DISCHARGE = 1, STOR\_AC\_CHARGE = 1.| +||STOR\_AC\_DISCHARGE = 2: If the co-located VRE-storage resource has asymmetric discharge capacities using distinct processes (e.g. hydrogen electrolysis, storage, and conversion to power using fuel cell or combustion turbine).| +|STOR\_AC\_CHARGE | {0, 1, 2}, Flag to indicate membership in set of co-located VRE-storage resources that charge AC.| +||STOR\_AC\_CHARGE = 0: Not part of set (default) | +||STOR\_AC\_CHARGE = 1: If the co-located VRE-storage resource contains symmetric charge/discharge power capacity with charging capacity equal to discharging capacity (e.g. lithium-ion battery storage). Note that if STOR\_AC\_CHARGE = 1, STOR\_AC\_DISCHARGE = 1.| +||STOR\_AC\_CHARGE = 2: If the co-located VRE-storage resource has asymmetric charge capacities using distinct processes (e.g. hydrogen electrolysis, storage, and conversion to power using fuel cell or combustion turbine).| +|LDS\_VRE\_STOR | {0, 1}, Flag to indicate the co-located VRE-storage resources eligible for long duration storage constraints with inter period linkage (e.g., reservoir hydro, hydrogen storage). | +||LDS\_VRE\_STOR = 0: Not part of set (default) | +||LDS\_VRE\_STOR = 1: Long duration storage resources| |**Existing technology capacity**| |Existing\_Cap\_MW |The existing AC grid connection capacity in MW. | |Existing\_Cap\_MWh |The existing capacity of storage in MWh. | @@ -426,8 +426,8 @@ Each co-located VRE and storage resource can be easily configured to contain eit |**Technical performance parameters**| |Self\_Disch |[0,1], The power loss of storage component of each resource per hour (fraction loss per hour). | |EtaInverter |[0,1], Inverter efficiency representing losses from converting DC to AC power and vice versa for each technology | -|Inverter_Ratio_Solar |-1 (default) - no required ratio between solar PV capacity built to inverter capacity built. If non-negative, represents the ratio of solar PV capacity built to inverter capacity built.| -|Inverter_Ratio_Wind |-1 (default) - no required ratio between wind capacity built to grid connection capacity built. If non-negative, represents the ratio of wind capacity built to grid connection capacity built.| +|Inverter\_Ratio_Solar |-1 (default) - no required ratio between solar PV capacity built to inverter capacity built. If non-negative, represents the ratio of solar PV capacity built to inverter capacity built.| +|Inverter\_Ratio_Wind |-1 (default) - no required ratio between wind capacity built to grid connection capacity built. If non-negative, represents the ratio of wind capacity built to grid connection capacity built.| |Power\_to\_Energy\_AC |The power to energy conversion for the storage component for AC discharging/charging of symmetric storage resources.| |Power\_to\_Energy\_DC |The power to energy conversion for the storage component for DC discharging/charging of symmetric storage resources.| |Eff\_Up\_DC |[0,1], Efficiency of DC charging storage – applies to storage technologies (all STOR types). | @@ -463,7 +463,7 @@ The following table describes the columns in each of these four files. This policy is applied when if `EnergyShareRequirement > 0` in the settings file. \* corresponds to the ith row of the file `Energy_share_requirement.csv`. -##### Table 12: Energy share requirement policy parameters +##### Table 12: Energy share requirement policy parameters in Resource_energy_share_requirement.csv --- |**Column Name** | **Description**| | :------------ | :-----------| @@ -478,44 +478,43 @@ This policy is applied when if `EnergyShareRequirement > 0` in the settings file This policy is applied when if `MinCapReq = 1` in the settings file. \* corresponds to the ith row of the file `Minimum_capacity_requirement.csv`. -##### Table 13: Minimum capacity requirement policy parameters +##### Table 13: Minimum capacity requirement policy parameters in Resource_minimum_capacity_requirement.csv --- |**Column Name** | **Description**| | :------------ | :-----------| |Resource| Resource name corresponding to a resource in one of the resource data files described above.| -|Min_Cap\_*| Flag to indicate which resources are considered for the Minimum Capacity Requirement constraint.| +|Min\_Cap\_*| Flag to indicate which resources are considered for the Minimum Capacity Requirement constraint.| |**co-located VRE-STOR resources only**| -|Min_Cap_Solar\_*| Eligibility of resources with a solar PV component (multiplied by the inverter efficiency for AC terms) to participate in Minimum Technology Carveout constraint.| -|Min_Cap_Wind\_*| Eligibility of resources with a wind component to participate in Minimum Technology Carveout constraint (AC terms).| -|Min_Cap_Stor\_*| Eligibility of resources with a storage component to participate in Minimum Technology Carveout constraint (discharge capacity in AC terms).| +|Min\_Cap\_Solar\_*| Eligibility of resources with a solar PV component (multiplied by the inverter efficiency for AC terms) to participate in Minimum Technology Carveout constraint.| +|Min\_Cap\_Wind\_*| Eligibility of resources with a wind component to participate in Minimum Technology Carveout constraint (AC terms).| +|Min\_Cap\_Stor\_*| Eligibility of resources with a storage component to participate in Minimum Technology Carveout constraint (discharge capacity in AC terms).| This policy is applied when if `MaxCapReq = 1` in the settings file. \* corresponds to the ith row of the file `Maximum_capacity_requirement.csv`. -##### Table 14: Maximum capacity requirement policy parameters +##### Table 14: Maximum capacity requirement policy parameters in Resource_maximum_capacity_requirement.csv --- |**Column Name** | **Description**| | :------------ | :-----------| |Resource| Resource name corresponding to a resource in one of the resource data files described above.| -|Max_Cap\_*| Flag to indicate which resources are considered for the Maximum Capacity Requirement constraint.| +|Max\_Cap\_*| Flag to indicate which resources are considered for the Maximum Capacity Requirement constraint.| |**co-located VRE-STOR resources only**| -|Max_Cap_Solar\_*| Eligibility of resources with a solar PV component (multiplied by the inverter efficiency for AC terms) to participate in Maximum Technology Carveout constraint. -|Max_Cap_Wind\_*| Eligibility of resources with a wind component to participate in Maximum Technology Carveout constraint (AC terms). -|Max_Cap_Stor\_*| Eligibility of resources with a storage component to participate in Maximum Technology Carveout constraint (discharge capacity in AC terms).| +|Max\_Cap\_Solar\_*| Eligibility of resources with a solar PV component (multiplied by the inverter efficiency for AC terms) to participate in Maximum Technology Carveout constraint. +|Max\_Cap\_Wind\_*| Eligibility of resources with a wind component to participate in Maximum Technology Carveout constraint (AC terms). +|Max\_Cap\_Stor\_*| Eligibility of resources with a storage component to participate in Maximum Technology Carveout constraint (discharge capacity in AC terms).| This policy is applied when if `CapacityReserveMargin > 0` in the settings file. \* corresponds to the ith row of the file `Capacity_reserve_margin.csv`. -##### Table 15: Capacity reserve margin policy parameters +##### Table 15: Capacity reserve margin policy parameters in Resource_capacity_reserve_margin.csv --- |**Column Name** | **Description**| | :------------ | :-----------| |Resource| Resource name corresponding to a resource in one of the resource data files described above.| -|Eligible_Cap_Res\_*| Fraction of the resource capacity eligible for contributing to the capacity reserve margin constraint (e.g. derate factor).| +|Derating\_Factor\_*| Fraction of the resource capacity eligible for contributing to the capacity reserve margin constraint (e.g. derate factor).| ##### Additional module-related columns for all resources In addition to the files described above, the `resources` folder can contain additional files that are used to specify attributes for specific resources and modules. Currently, the following files are supported: -1) `Resource_multistage_data.csv`: mandatory if `MultiStage = 1` in the settings file - +`Resource_multistage_data.csv`: mandatory if `MultiStage = 1` in the settings file !!! warning The first column of each additional module file must contain the resource name corresponding to a resource in one of the resource data files described above. Note that the order of resources in these files is not important. From fff2dc36f22f55169e59bd5a23e91f718c4a1bbd Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Fri, 5 Apr 2024 16:47:18 -0400 Subject: [PATCH 29/59] Updated the CPLEX link on README to the latest --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7a4e642e3f..b7f513b34e 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ It is currently setup to use one of the following open-source freely available s - [Cbc](https://github.com/jump-dev/Cbc.jl) for mixed integer linear programming (MILP) problems We also provide the option to use one of these two commercial solvers: - [Gurobi](https://www.gurobi.com), or -- [CPLEX](https://www.ibm.com/analytics/cplex-optimizer). +- [CPLEX](https://www.ibm.com/docs/en/icos/22.1.1?topic=documentation-orientation-guide). Note that using Gurobi and CPLEX requires a valid license on the host machine. There are two ways to run GenX with either type of solver options (open-source free or, licensed commercial) as detailed in the section, `Getting Started`. From cbb9217feb8cfab71d42fcbe30c79e90a85d464c Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Tue, 9 Apr 2024 14:13:01 -0400 Subject: [PATCH 30/59] Revised DF calculation in DDP initialize CTG --- src/multi_stage/dual_dynamic_programming.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/multi_stage/dual_dynamic_programming.jl b/src/multi_stage/dual_dynamic_programming.jl index 9703784473..25d7f0506b 100644 --- a/src/multi_stage/dual_dynamic_programming.jl +++ b/src/multi_stage/dual_dynamic_programming.jl @@ -617,6 +617,10 @@ returns: JuMP model with updated objective function. """ function initialize_cost_to_go(settings_d::Dict, EP::Model, inputs::Dict) cur_stage = settings_d["CurStage"] # Current DDP Investment Planning Stage + stage_len_1 = 0 + for stage_count in 1:(cur_stage-1) + stage_len_1 += settings_d["StageLengths"][stage_count] + end stage_len = settings_d["StageLengths"][cur_stage] wacc = settings_d["WACC"] # Interest Rate and also the discount rate unless specified other wise myopic = settings_d["Myopic"] == 1 # 1 if myopic (only one forward pass), 0 if full DDP @@ -629,7 +633,7 @@ function initialize_cost_to_go(settings_d::Dict, EP::Model, inputs::Dict) ### No discount factor or OPEX multiplier applied in myopic case as costs are left annualized. @objective(EP, Min, EP[:eObj]) else - DF = 1 / (1 + wacc)^(stage_len * (cur_stage - 1)) # Discount factor applied all to costs in each stage ### + DF = 1 / (1 + wacc)^(stage_len_1) # Discount factor applied all to costs in each stage ### # Initialize the cost-to-go variable @variable(EP, vALPHA>=0) @objective(EP, Min, DF * OPEXMULT * EP[:eObj]+vALPHA) From a047697e0389b4815d0dfc1ce22a1cf832ac8a94 Mon Sep 17 00:00:00 2001 From: Luca Bonaldo <39280783+lbonaldo@users.noreply.github.com> Date: Tue, 9 Apr 2024 14:42:09 -0400 Subject: [PATCH 31/59] Discount cUnmetPolicyPenalty in write_multi_stage_cost.jl (#679) --- CHANGELOG.md | 1 + src/multi_stage/write_multi_stage_costs.jl | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbbde25d73..18ea0f2e09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix write_status with UCommit = WriteShadowPrices = 1 (#645) - Fixed outputting capital recovery cost to 0 if the remaining number of years is 0 (#666) - Updated the docstring for the initialize_cost_to_go function and adjusted the formula for the discount factor to reflect the code implementation (#672). +- Fix write_multi_stage_cost.jl: add discount with OPEX multipliers to cUnmetPolicyPenalty (#679) ### Changed - Use add_to_expression! instead of the += and -= operators for memory performance improvements (#498). diff --git a/src/multi_stage/write_multi_stage_costs.jl b/src/multi_stage/write_multi_stage_costs.jl index 0c229a8d35..20bed3a9f7 100644 --- a/src/multi_stage/write_multi_stage_costs.jl +++ b/src/multi_stage/write_multi_stage_costs.jl @@ -36,7 +36,7 @@ function write_multi_stage_costs(outpath::String, settings_d::Dict, inputs_dict: end # For OPEX costs, apply additional discounting - for cost in ["cVar", "cNSE", "cStart", "cUnmetRsv"] + for cost in ["cVar", "cNSE", "cStart", "cUnmetRsv", "cUnmetPolicyPenalty"] if cost in df_costs[!, :Costs] df_costs[df_costs[!, :Costs] .== cost, 2:end] = transpose(OPEXMULTS) .* df_costs[df_costs[!, :Costs] .== cost, From 72b253ddedf4af735b3f1b217bb6f13f4e9618d8 Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Tue, 9 Apr 2024 14:44:56 -0400 Subject: [PATCH 32/59] Updated doc of DDP --- src/multi_stage/dual_dynamic_programming.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/multi_stage/dual_dynamic_programming.jl b/src/multi_stage/dual_dynamic_programming.jl index 25d7f0506b..7ae8e621ce 100644 --- a/src/multi_stage/dual_dynamic_programming.jl +++ b/src/multi_stage/dual_dynamic_programming.jl @@ -593,7 +593,7 @@ The updated objective function $OBJ^{*}$ returned by this method takes the form: where $OBJ$ is the original objective function. $OBJ$ is scaled by two terms. The first is a discount factor (applied only in the non-myopic case), which discounts costs associated with the model stage $p$ to year-0 dollars: ```math \begin{aligned} - DF = \frac{1}{(1+WACC)^{L_{p}*(p-1)}} + DF = \frac{1}{(1+WACC)^{\Sum_{p=0}^{(p-1)}L_{p}}} \end{aligned} ``` where $WACC$ is the weighted average cost of capital, and $L_{p}$ is the length of each stage in years (both set in multi\_stage\_settings.yml) @@ -621,6 +621,7 @@ function initialize_cost_to_go(settings_d::Dict, EP::Model, inputs::Dict) for stage_count in 1:(cur_stage-1) stage_len_1 += settings_d["StageLengths"][stage_count] end + println("Value of stage len_1 in stage ", cur_stage, " is ", stage_len_1) stage_len = settings_d["StageLengths"][cur_stage] wacc = settings_d["WACC"] # Interest Rate and also the discount rate unless specified other wise myopic = settings_d["Myopic"] == 1 # 1 if myopic (only one forward pass), 0 if full DDP From 9d465e79b2f3f6ed68b7b6e408cef36dff93444a Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Tue, 9 Apr 2024 14:47:36 -0400 Subject: [PATCH 33/59] minor doc fix --- src/multi_stage/dual_dynamic_programming.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/multi_stage/dual_dynamic_programming.jl b/src/multi_stage/dual_dynamic_programming.jl index 7ae8e621ce..6baf43865e 100644 --- a/src/multi_stage/dual_dynamic_programming.jl +++ b/src/multi_stage/dual_dynamic_programming.jl @@ -593,7 +593,7 @@ The updated objective function $OBJ^{*}$ returned by this method takes the form: where $OBJ$ is the original objective function. $OBJ$ is scaled by two terms. The first is a discount factor (applied only in the non-myopic case), which discounts costs associated with the model stage $p$ to year-0 dollars: ```math \begin{aligned} - DF = \frac{1}{(1+WACC)^{\Sum_{p=0}^{(p-1)}L_{p}}} + DF = \frac{1}{(1+WACC)^{\sum^{(p-1)}_{p=0}L_{p}}} \end{aligned} ``` where $WACC$ is the weighted average cost of capital, and $L_{p}$ is the length of each stage in years (both set in multi\_stage\_settings.yml) From 9d2949dc7e0c4b2aebeaf342861ee0e051542de8 Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Tue, 9 Apr 2024 14:56:14 -0400 Subject: [PATCH 34/59] Update CHANGELOG.md Fix DF calculation in DDP to make it more generic for variable length stages --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18ea0f2e09..510d9e8ca1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,7 +65,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed outputting capital recovery cost to 0 if the remaining number of years is 0 (#666) - Updated the docstring for the initialize_cost_to_go function and adjusted the formula for the discount factor to reflect the code implementation (#672). - Fix write_multi_stage_cost.jl: add discount with OPEX multipliers to cUnmetPolicyPenalty (#679) - +- Fix DF calculation in DDP to make it more generic for variable length stages ### Changed - Use add_to_expression! instead of the += and -= operators for memory performance improvements (#498). - Generally, 'demand' is now used where (electrical) 'load' was used previously (#397). From 0f7ae5469ce149d9c6c2f540b271e5d0920c88e3 Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Tue, 9 Apr 2024 15:00:37 -0400 Subject: [PATCH 35/59] Update CHANGELOG.md Added PR # in CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 510d9e8ca1..b390d4790b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,7 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add check when capital recovery period is zero and investment costs are non-zero in multi-stage GenX (#666) - Added condition number scaling added to objective function (#667) -- Added versioned doc-pages for v0.3.6 and v0.4.0 +- Added versioned doc-pages for v0.3.6 and v0.4.0 (#680) ### Fixed - Set MUST_RUN=1 for RealSystemExample/small_hydro plants (#517). From afbda63263e979bd16deb98827eaeecdc63c7096 Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Tue, 9 Apr 2024 15:01:23 -0400 Subject: [PATCH 36/59] Update CHANGELOG.md Fix --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b390d4790b..b667d35950 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,7 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add check when capital recovery period is zero and investment costs are non-zero in multi-stage GenX (#666) - Added condition number scaling added to objective function (#667) -- Added versioned doc-pages for v0.3.6 and v0.4.0 (#680) +- Added versioned doc-pages for v0.3.6 and v0.4.0 ### Fixed - Set MUST_RUN=1 for RealSystemExample/small_hydro plants (#517). @@ -65,7 +65,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed outputting capital recovery cost to 0 if the remaining number of years is 0 (#666) - Updated the docstring for the initialize_cost_to_go function and adjusted the formula for the discount factor to reflect the code implementation (#672). - Fix write_multi_stage_cost.jl: add discount with OPEX multipliers to cUnmetPolicyPenalty (#679) -- Fix DF calculation in DDP to make it more generic for variable length stages +- Fix DF calculation in DDP to make it more generic for variable length stages (#680) ### Changed - Use add_to_expression! instead of the += and -= operators for memory performance improvements (#498). - Generally, 'demand' is now used where (electrical) 'load' was used previously (#397). From 0cef5cd54595dfd2815186469b283fae2bb46a6a Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Tue, 9 Apr 2024 15:28:21 -0400 Subject: [PATCH 37/59] Update write_multi_stage_costs.jl Update write_multi_stage_costs.jl with refined DF --- src/multi_stage/write_multi_stage_costs.jl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/multi_stage/write_multi_stage_costs.jl b/src/multi_stage/write_multi_stage_costs.jl index 20bed3a9f7..9df028bd36 100644 --- a/src/multi_stage/write_multi_stage_costs.jl +++ b/src/multi_stage/write_multi_stage_costs.jl @@ -30,7 +30,13 @@ function write_multi_stage_costs(outpath::String, settings_d::Dict, inputs_dict: if myopic DF = 1 # DF=1 because we do not apply discount factor in myopic case else - DF = 1 / (1 + wacc)^(stage_lens[p] * (p - 1)) # Discount factor applied to ALL costs in each stage + stage_length_1 = 0 + if p > 1 + for stage_counter in 1:(p - 1) + stage_length_1 += stage_lens[stage_counter] + end + end + DF = 1 / (1 + wacc)^(stage_length_1) # Discount factor applied to ALL costs in each stage end df_costs[!, Symbol("TotalCosts_p$p")] = DF .* costs_d[p][!, Symbol("Total")] end From 199953007eb9c89a70d7f1ff91b0dbb436fe643e Mon Sep 17 00:00:00 2001 From: "Chakrabarti, Sambuddha (Sam)" Date: Tue, 9 Apr 2024 16:02:23 -0400 Subject: [PATCH 38/59] Minor doc fix; Changed counter of summation from p to k --- src/multi_stage/dual_dynamic_programming.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/multi_stage/dual_dynamic_programming.jl b/src/multi_stage/dual_dynamic_programming.jl index 6baf43865e..ad1ba0f74d 100644 --- a/src/multi_stage/dual_dynamic_programming.jl +++ b/src/multi_stage/dual_dynamic_programming.jl @@ -593,7 +593,7 @@ The updated objective function $OBJ^{*}$ returned by this method takes the form: where $OBJ$ is the original objective function. $OBJ$ is scaled by two terms. The first is a discount factor (applied only in the non-myopic case), which discounts costs associated with the model stage $p$ to year-0 dollars: ```math \begin{aligned} - DF = \frac{1}{(1+WACC)^{\sum^{(p-1)}_{p=0}L_{p}}} + DF = \frac{1}{(1+WACC)^{\sum^{(p-1)}_{k=0}L_{k}}} \end{aligned} ``` where $WACC$ is the weighted average cost of capital, and $L_{p}$ is the length of each stage in years (both set in multi\_stage\_settings.yml) From e0bc751f8a4b59221d9efd770cc10b022182811d Mon Sep 17 00:00:00 2001 From: ql0320 <46303054+qluo0320github@users.noreply.github.com> Date: Thu, 11 Apr 2024 18:48:58 -0400 Subject: [PATCH 39/59] Add FLEX power constraint (#677) --- .../resources/flexible_demand/flexible_demand.jl | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/model/resources/flexible_demand/flexible_demand.jl b/src/model/resources/flexible_demand/flexible_demand.jl index 18bdbbe681..37c459f58c 100644 --- a/src/model/resources/flexible_demand/flexible_demand.jl +++ b/src/model/resources/flexible_demand/flexible_demand.jl @@ -18,6 +18,12 @@ At any given time step, the amount of demand that can be shifted or deferred can \Pi_{y,t} \leq \rho^{max}_{y,z,t}\Delta_{y,z} \hspace{4 cm} \forall y \in \mathcal{DF}, z \in \mathcal{Z}, t \in \mathcal{T} \end{aligned} ``` +At any given time step, the amount of demand that can be met cannot exceed the capacity of the FLEX resources. +```math +\begin{aligned} +\eta_{y,z}^{dflex}\Theta_{y,z,t} \leq \Delta_{y,z} \hspace{4 cm} \forall y \in \mathcal{DF}, z \in \mathcal{Z}, t \in \mathcal{T} +\end{aligned} +``` **Maximum time delay and advancements** Delayed demand must then be served within a fixed number of time steps. This is done by enforcing the sum of demand satisfied ($\Theta_{y,z,t}$) in the following $\tau^{delay}_{y,z}$ time steps (e.g., t + 1 to t + $\tau^{delay}_{y,z}$) to be greater than or equal to the level of energy deferred during time step $t$. ```math @@ -104,12 +110,12 @@ function flexible_demand!(EP::Model, inputs::Dict, setup::Dict) EP[:vCHARGE_FLEX][y, t] # Maximum charging rate - # NOTE: the maximum amount that can be shifted is given by hourly availability of the resource times the maximum capacity of the resource [y in FLEX_Z, t = 1:T], EP[:vCHARGE_FLEX][y, t] <= inputs["pP_Max"][y, t] * EP[:eTotalCap][y] - # NOTE: no maximum discharge rate unless constrained by other factors like transmission, etc. + # Maximum discharging rate + [y in FLEX_Z, t = 1:T], + flexible_demand_energy_eff(gen[y]) * EP[:vP][y, t] <= EP[:eTotalCap][y] end) - for y in FLEX_Z # Require deferred demands to be satisfied within the specified time delay From 1e7fbf8f4e2a8a704a7f367cfa057f4bce49497a Mon Sep 17 00:00:00 2001 From: ql0320 <46303054+qluo0320github@users.noreply.github.com> Date: Thu, 11 Apr 2024 19:03:56 -0400 Subject: [PATCH 40/59] Fix write_power_balance for VRE_STOR (#682) --- CHANGELOG.md | 1 + src/write_outputs/write_power_balance.jl | 27 ++++++++++++++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18ea0f2e09..935f1f9151 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed outputting capital recovery cost to 0 if the remaining number of years is 0 (#666) - Updated the docstring for the initialize_cost_to_go function and adjusted the formula for the discount factor to reflect the code implementation (#672). - Fix write_multi_stage_cost.jl: add discount with OPEX multipliers to cUnmetPolicyPenalty (#679) +- Fix write_power_balance.jl: add additional two columns ("VRE_Storage_Discharge" and "VRE_Storage_Charge") for VRE_STOR ### Changed - Use add_to_expression! instead of the += and -= operators for memory performance improvements (#498). diff --git a/src/write_outputs/write_power_balance.jl b/src/write_outputs/write_power_balance.jl index 4661fb3faa..924c3b5f88 100644 --- a/src/write_outputs/write_power_balance.jl +++ b/src/write_outputs/write_power_balance.jl @@ -19,6 +19,10 @@ function write_power_balance(path::AbstractString, inputs::Dict, setup::Dict, EP if !isempty(ELECTROLYZER) push!(Com_list, "Electrolyzer_Consumption") end + if !isempty(VRE_STOR) + push!(Com_list, "VRE_Storage_Discharge") + push!(Com_list, "VRE_Storage_Charge") + end L = length(Com_list) dfPowerBalance = DataFrame(BalanceComponent = repeat(Com_list, outer = Z), Zone = repeat(1:Z, inner = L), @@ -37,15 +41,6 @@ function write_power_balance(path::AbstractString, inputs::Dict, setup::Dict, EP :]).data), dims = 1) end - if !isempty(intersect(resources_in_zone_by_rid(gen, z), VRE_STOR)) - VS_ALL_ZONE = intersect(resources_in_zone_by_rid(gen, z), inputs["VS_STOR"]) - powerbalance[(z - 1) * L + 2, :] = sum(value.(EP[:vP][VS_ALL_ZONE, :]), - dims = 1) - powerbalance[(z - 1) * L + 3, :] = (-1) * - sum(value.(EP[:vCHARGE_VRE_STOR][VS_ALL_ZONE, - :]).data, - dims = 1) - end if !isempty(intersect(resources_in_zone_by_rid(gen, z), FLEX)) FLEX_ZONE = intersect(resources_in_zone_by_rid(gen, z), FLEX) powerbalance[(z - 1) * L + 4, :] = sum((value.(EP[:vCHARGE_FLEX][FLEX_ZONE, @@ -71,6 +66,20 @@ function write_power_balance(path::AbstractString, inputs::Dict, setup::Dict, EP :].data), dims = 1) end + # VRE storage discharge and charge + if !isempty(intersect(resources_in_zone_by_rid(gen, z), VRE_STOR)) + VS_ALL_ZONE = intersect(resources_in_zone_by_rid(gen, z), inputs["VS_STOR"]) + + # if ELECTROLYZER is empty, increase indices by 1 + is_electrolyzer_empty = isempty(ELECTROLYZER) + discharge_idx = is_electrolyzer_empty ? 11 : 12 + charge_idx = is_electrolyzer_empty ? 12 : 13 + + powerbalance[(z - 1) * L + discharge_idx, :] = sum( + value.(EP[:vP][VS_ALL_ZONE, :]), dims = 1) + powerbalance[(z - 1) * L + charge_idx, :] = (-1) * sum( + value.(EP[:vCHARGE_VRE_STOR][VS_ALL_ZONE, :]).data, dims = 1) + end end if setup["ParameterScale"] == 1 powerbalance *= ModelScalingFactor From f254c2c4f9475a0692039ec228cca1747395dd89 Mon Sep 17 00:00:00 2001 From: Luca Bonaldo <39280783+lbonaldo@users.noreply.github.com> Date: Fri, 12 Apr 2024 18:33:01 -0400 Subject: [PATCH 41/59] Add option to constraint total capacity in mga (#681) Co-authored-by: ml6802 <90064266+ml6802@users.noreply.github.com> --- CHANGELOG.md | 2 + docs/src/User_Guide/generate_alternatives.md | 14 +-- .../modeling_to_generate_alternatives.jl | 104 ++++++++++++++---- src/case_runners/case_runner.jl | 2 +- src/configure_settings/configure_settings.jl | 1 + src/model/generate_model.jl | 4 + 6 files changed, 94 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 935f1f9151..199da72a7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 non-zero in multi-stage GenX (#666) - Added condition number scaling added to objective function (#667) - Added versioned doc-pages for v0.3.6 and v0.4.0 +- Add constraint in mga to compute total capacity in each zone from a given technology type (#681) +- New settings parameter MGAAnnualGeneration to switch between different MGA formulations (#681) ### Fixed - Set MUST_RUN=1 for RealSystemExample/small_hydro plants (#517). diff --git a/docs/src/User_Guide/generate_alternatives.md b/docs/src/User_Guide/generate_alternatives.md index 580629af75..f0fe836362 100644 --- a/docs/src/User_Guide/generate_alternatives.md +++ b/docs/src/User_Guide/generate_alternatives.md @@ -4,12 +4,10 @@ GenX includes a modeling to generate alternatives (MGA) package that can be used 1. Add a `Resource_Type` column in all the resource `.csv` files denoting the type of each technology. 2. Add a `MGA` column in all the resource `.csv` files denoting the availability of the technology. -3. Set the `ModelingToGenerateAlternatives` flag in the `GenX_Settings.yml` file to 1. -4. Set the `ModelingtoGenerateAlternativeSlack` flag in the `GenX_Settings.yml` file to the desirable level of slack. -5. Create a `Rand_mga_objective_coefficients.csv` file to provide random objective function coefficients for each MGA iteration. +3. Set the `ModelingToGenerateAlternatives` flag in the `genx_settings.yml` file to 1. +4. Set the `ModelingtoGenerateAlternativeSlack` flag in the `genx_settings.yml` file to the desirable level of slack. +5. Set the `ModelingToGenerateAlternativesIterations` flag to half the total number of desired solutions, as each iteration provides 2 solutions. +6. Set the `MGAAnnualGeneration` flag in the `genx_settings.yml` file to the desired MGA formulation. +7. Solve the model using `Run.jl` file. -For each iteration, number of rows in the `Rand_mga_objective_coefficients`.csv file represents the number of distinct technology types while number of columns represent the number of model zones. - -Solve the model using `Run.jl` file. - -Results from the MGA algorithm would be saved in MGA_max and MGA_min folders in the `Example_Systems/` folder. \ No newline at end of file +Results from the MGA algorithm would be saved in MGA_max and MGA_min folders in the case folder. \ No newline at end of file diff --git a/src/additional_tools/modeling_to_generate_alternatives.jl b/src/additional_tools/modeling_to_generate_alternatives.jl index ba0fec33e3..0fe990c7be 100644 --- a/src/additional_tools/modeling_to_generate_alternatives.jl +++ b/src/additional_tools/modeling_to_generate_alternatives.jl @@ -3,20 +3,35 @@ We have implemented an updated Modeling to Generate Alternatives (MGA) Algorithm proposed by [Berntsen and Trutnevyte (2017)](https://www.sciencedirect.com/science/article/pii/S0360544217304097) to generate a set of feasible, near cost-optimal technology portfolios. This algorithm was developed by [Brill Jr, E. D., 1979](https://pubsonline.informs.org/doi/abs/10.1287/mnsc.25.5.413) and introduced to energy system planning by [DeCarolia, J. F., 2011](https://www.sciencedirect.com/science/article/pii/S0140988310000721). -To create the MGA formulation, we replace the cost-minimizing objective function of GenX with a new objective function that creates multiple generation portfolios by zone. We further add a new budget constraint based on the optimal objective function value $f^*$ of the least-cost model and the user-specified value of slack $\delta$. After adding the slack constraint, the resulting MGA formulation is given as: +To create the MGA formulation, we replace the cost-minimizing objective function of GenX with a new objective function that creates multiple generation portfolios by zone. We further add a new budget constraint based on the optimal objective function value $f^*$ of the least-cost model and the user-specified value of slack $\delta$. After adding the slack constraint, the resulting MGA formulation is given as (`MGAAnnualGeneration = 0` in the genx_settings.yml file, or not set): ```math \begin{aligned} \text{max/min} \quad &\sum_{z \in \mathcal{Z}}\sum_{r \in \mathcal{R}} \beta_{z,r}^{k}P_{z,r}\\ \text{s.t.} \quad - &P_{zr} = \sum_{y \in \mathcal{G}}\sum_{t \in \mathcal{T}} \omega_{t} \Theta_{y,t,z,r} \\ + &P_{z,r} = \sum_{y \in \mathcal{G}}C_{y,z,r} \\ & f \leq f^* + \delta \\ &Ax = b \end{aligned} ``` -where, $\beta_{zr}$ is a random objective fucntion coefficient betwen $[0,100]$ for MGA iteration $k$. $\Theta_{y,t,z,r}$ is a generation of technology $y$ in zone $z$ in time period $t$ that belongs to a resource type $r$. We aggregate $\Theta_{y,t,z,r}$ into a new variable $P_{z,r}$ that represents total generation from technology type $r$ in a zone $z$. In the second constraint above, $\delta$ denote the increase in budget from the least-cost solution and $f$ represents the expression for the total system cost. The constraint $Ax = b$ represents all other constraints in the power system model. We then solve the formulation with minimization and maximization objective function to explore near optimal solution space. +where, $\beta_{z,r}$ is a random objective function coefficient betwen $[0,1]$ for MGA iteration $k$. We aggregate capacity into a new variable $P_{z,r}$ that represents total capacity from technology type $r$ in a zone $z$. + +If the users set `MGAAnnualGeneration = 1` in the genx_settings.yml file, the MGA formulation is given as: +```math +\begin{aligned} +\text{max/min} \quad + &\sum_{z \in \mathcal{Z}}\sum_{r \in \mathcal{R}} \beta_{z,r}^{k}P_{z,r}\\ + \text{s.t.} \quad + &P_{z,r} = \sum_{y \in \mathcal{G}}\sum_{t \in \mathcal{T}} \omega_{t} \Theta_{y,t,z,r} \\ + & f \leq f^* + \delta \\ + &Ax = b +\end{aligned} +``` +where, $\beta_{z,r}$ is a random objective function coefficient betwen $[0,1]$ for MGA iteration $k$. $\Theta_{y,t,z,r}$ is a generation of technology $y$ in zone $z$ in time period $t$ that belongs to a resource type $r$. We aggregate $\Theta_{y,t,z,r}$ into a new variable $P_{z,r}$ that represents total generation from technology type $r$ in a zone $z$. + +In the second constraint in both the above formulations, $\delta$ denote the increase in budget from the least-cost solution and $f$ represents the expression for the total system cost. The constraint $Ax = b$ represents all other constraints in the power system model. We then solve the formulation with minimization and maximization objective function to explore near optimal solution space. """ function mga(EP::Model, path::AbstractString, setup::Dict, inputs::Dict) if setup["ModelingToGenerateAlternatives"] == 1 @@ -39,29 +54,10 @@ function mga(EP::Model, path::AbstractString, setup::Dict, inputs::Dict) # Read slack parameter representing desired increase in budget from the least cost solution slack = setup["ModelingtoGenerateAlternativeSlack"] - ### Variables ### - - @variable(EP, vSumvP[TechTypes = 1:length(TechTypes), z = 1:Z]>=0) # Variable denoting total generation from eligible technology of a given type - - ### End Variables ### - ### Constraints ### # Constraint to set budget for MGA iterations @constraint(EP, budget, EP[:eObj]<=Least_System_Cost * (1 + slack)) - - # Constraint to compute total generation in each zone from a given Technology Type - function resource_in_zone_with_TechType(tt::Int64, z::Int64) - condition::BitVector = (resource_type_mga.(gen) .== TechTypes[tt]) .& - (zone_id.(gen) .== z) - return resource_id.(gen[condition]) - end - @constraint(EP, - cGeneration[tt = 1:length(TechTypes), z = 1:Z], - vSumvP[tt, - z]==sum(EP[:vP][y, t] * inputs["omega"][t] - for y in resource_in_zone_with_TechType(tt, z), t in 1:T)) - ### End Constraints ### ### Create Results Directory for MGA iterations @@ -87,7 +83,8 @@ function mga(EP::Model, path::AbstractString, setup::Dict, inputs::Dict) ### Maximization objective @objective(EP, Max, - sum(pRand[tt, z] * vSumvP[tt, z] for tt in 1:length(TechTypes), z in 1:Z)) + sum(pRand[tt, z] * EP[:vMGA][tt, z] + for tt in 1:length(TechTypes), z in 1:Z)) # Solve Model Iteration status = optimize!(EP) @@ -101,7 +98,8 @@ function mga(EP::Model, path::AbstractString, setup::Dict, inputs::Dict) ### Minimization objective @objective(EP, Min, - sum(pRand[tt, z] * vSumvP[tt, z] for tt in 1:length(TechTypes), z in 1:Z)) + sum(pRand[tt, z] * EP[:vMGA][tt, z] + for tt in 1:length(TechTypes), z in 1:Z)) # Solve Model Iteration status = optimize!(EP) @@ -117,3 +115,61 @@ function mga(EP::Model, path::AbstractString, setup::Dict, inputs::Dict) ### End MGA Iterations ### end end + +@doc raw""" + mga!(EP::Model, inputs::Dict, setup::Dict) + +This function reads the input data, collect the resources with MGA flag on and creates a set of unique technology types. +The function then adds a constraint to the model to compute total capacity in each zone from a given Technology Type. + +If the user set `MGAAnnualGeneration = 0` in the genx_settings.yml file, the constraint has the following form: +```math +P_{z,r} = \sum_{y \in \mathcal{G}}C_{y,z,r} +``` +where, the aggregated capacity $P_{z,r}$ represents total capacity from technology type $r$ in a zone $z$. + +If the user set `MGAAnnualGeneration = 1` in the genx_settings.yml file, the constraint has the following form: +```math +P_{z,r} = \sum_{y \in \mathcal{G}}\sum_{t \in \mathcal{T}} \omega_{t} \Theta_{y,t,z,r} +``` +where $\Theta_{y,t,z,r}$ is a generation of technology $y$ in zone $z$ in time period $t$ that belongs to a resource type $r$. $\Theta_{y,t,z,r}$ is aggregated into a new variable $P_{z,r}$ that represents total generation from technology type $r$ in a zone $z$. + +# Arguments +- `EP::Model`: GenX model object +- `inputs::Dict`: Dictionary containing input data + +# Returns +- This function updates the model object `EP` with the MGA variables and constraints in-place. +""" +function mga!(EP::Model, inputs::Dict, setup::Dict) + println("MGA Module") + + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones + gen = inputs["RESOURCES"] # Resources data + # Read set of MGA variables + annual_gen = setup["MGAAnnualGeneration"] ### Choose setting in genx_settings.yaml: MGAAnnualGeneration: 1 = annual generation, otherwise, sum of capacity + # Create a set of unique technology types + resources_with_mga_on = gen[ids_with_mga(gen)] + TechTypes = unique(resource_type_mga.(resources_with_mga_on)) + + function resource_in_zone_same_TechType(tt::Int64, z::Int64) + condition::BitVector = (resource_type_mga.(gen) .== TechTypes[tt]) .& + (zone_id.(gen) .== z) + return resource_id.(gen[condition]) + end + # Constraint to compute total generation in each zone from a given Technology Type + ### Variables ### + @variable(EP, vMGA[TechTypes = 1:length(TechTypes), z = 1:Z]>=0) + + ### Constraint ### + if annual_gen == 1 # annual generation + @constraint(EP, cGeneration[tt = 1:length(TechTypes), z = 1:Z], + vMGA[tt,z]==sum(EP[:vP][y, t] * inputs["omega"][t] + for y in resource_in_zone_same_TechType(tt, z), t in 1:T)) + else + @constraint(EP, cCapEquiv[tt = 1:length(TechTypes), z = 1:Z], + vMGA[tt,z]==sum(EP[:eTotalCap][y] + for y in resource_in_zone_same_TechType(tt, z))) + end +end diff --git a/src/case_runners/case_runner.jl b/src/case_runners/case_runner.jl index f5725e7bfc..13b30ac7b5 100644 --- a/src/case_runners/case_runner.jl +++ b/src/case_runners/case_runner.jl @@ -94,7 +94,7 @@ function run_genx_case_simple!(case::AbstractString, mysetup::Dict, optimizer::A println(elapsed_time) if mysetup["ModelingToGenerateAlternatives"] == 1 println("Starting Model to Generate Alternatives (MGA) Iterations") - mga(EP, case, mysetup, myinputs, outputs_path) + mga(EP, case, mysetup, myinputs) end if mysetup["MethodofMorris"] == 1 diff --git a/src/configure_settings/configure_settings.jl b/src/configure_settings/configure_settings.jl index d37107f256..3130bab7cf 100644 --- a/src/configure_settings/configure_settings.jl +++ b/src/configure_settings/configure_settings.jl @@ -18,6 +18,7 @@ function default_settings() "TimeDomainReductionFolder" => "TDR_results", "ModelingToGenerateAlternatives" => 0, "ModelingtoGenerateAlternativeSlack" => 0.1, + "MGAAnnualGeneration" => 0, "MultiStage" => 0, "MethodofMorris" => 0, "IncludeLossesInESR" => 0, diff --git a/src/model/generate_model.jl b/src/model/generate_model.jl index 18f3f98775..bac3e661a8 100644 --- a/src/model/generate_model.jl +++ b/src/model/generate_model.jl @@ -225,6 +225,10 @@ function generate_model(setup::Dict, inputs::Dict, OPTIMIZER::MOI.OptimizerWithA maximum_capacity_requirement!(EP, inputs, setup) end + if setup["ModelingToGenerateAlternatives"] == 1 + mga!(EP, inputs, setup) + end + ## Define the objective function @objective(EP, Min, setup["ObjScale"]*EP[:eObj]) From aa0e0ef56e2a87e8d6bd6ab8ec89a01e0b21962b Mon Sep 17 00:00:00 2001 From: Luca Bonaldo <39280783+lbonaldo@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:07:26 -0400 Subject: [PATCH 42/59] Remove fuel cols in vre and stor files in examples (#684) --- example_systems/1_three_zones/resources/Vre.csv | 10 +++++----- .../2_three_zones_w_electrolyzer/resources/Vre.csv | 10 +++++----- .../3_three_zones_w_co2_capture/resources/Vre.csv | 10 +++++----- .../4_three_zones_w_policies_slack/resources/Vre.csv | 10 +++++----- .../5_three_zones_w_piecewise_fuel/resources/Vre.csv | 10 +++++----- .../inputs/inputs_p1/resources/Vre.csv | 10 +++++----- .../inputs/inputs_p2/resources/Vre.csv | 10 +++++----- .../inputs/inputs_p3/resources/Vre.csv | 10 +++++----- .../resources/Vre.csv | 10 +++++----- .../8_three_zones_w_retrofit/resources/Storage.csv | 8 ++++---- .../8_three_zones_w_retrofit/resources/Vre.csv | 10 +++++----- 11 files changed, 54 insertions(+), 54 deletions(-) diff --git a/example_systems/1_three_zones/resources/Vre.csv b/example_systems/1_three_zones/resources/Vre.csv index c5b8c5ea95..0183631d73 100644 --- a/example_systems/1_three_zones/resources/Vre.csv +++ b/example_systems/1_three_zones/resources/Vre.csv @@ -1,5 +1,5 @@ -Resource,Zone,Num_VRE_Bins,New_Build,Can_Retire,Existing_Cap_MW,Max_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,Heat_Rate_MMBTU_per_MWh,Fuel,Reg_Max,Rsv_Max,Reg_Cost,Rsv_Cost,region,cluster -MA_solar_pv,1,1,1,0,0,-1,0,85300,18760,0,9.13,None,0,0,0,0,MA,1 -CT_onshore_wind,2,1,1,0,0,-1,0,97200,43205,0.1,9.12,None,0,0,0,0,CT,1 -CT_solar_pv,2,1,1,0,0,-1,0,85300,18760,0,9.16,None,0,0,0,0,CT,1 -ME_onshore_wind,3,1,1,0,0,-1,0,97200,43205,0.1,9.12,None,0,0,0,0,ME,1 \ No newline at end of file +Resource,Zone,Num_VRE_Bins,New_Build,Can_Retire,Existing_Cap_MW,Max_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,Reg_Max,Rsv_Max,Reg_Cost,Rsv_Cost,region,cluster +MA_solar_pv,1,1,1,0,0,-1,0,85300,18760,0,0,0,0,0,MA,1 +CT_onshore_wind,2,1,1,0,0,-1,0,97200,43205,0.1,0,0,0,0,CT,1 +CT_solar_pv,2,1,1,0,0,-1,0,85300,18760,0,0,0,0,0,CT,1 +ME_onshore_wind,3,1,1,0,0,-1,0,97200,43205,0.1,0,0,0,0,ME,1 \ No newline at end of file diff --git a/example_systems/2_three_zones_w_electrolyzer/resources/Vre.csv b/example_systems/2_three_zones_w_electrolyzer/resources/Vre.csv index f53a8ad0c1..1371042441 100644 --- a/example_systems/2_three_zones_w_electrolyzer/resources/Vre.csv +++ b/example_systems/2_three_zones_w_electrolyzer/resources/Vre.csv @@ -1,5 +1,5 @@ -Resource,Zone,Num_VRE_Bins,New_Build,Existing_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,Heat_Rate_MMBTU_per_MWh,Qualified_Hydrogen_Supply,region,cluster -MA_solar_pv,1,1,1,0,0,85300,18760,0,9.13,1,MA,1 -CT_onshore_wind,2,1,1,0,0,97200,43205,0.1,9.12,1,CT,1 -CT_solar_pv,2,1,1,0,0,85300,18760,0,9.16,1,CT,1 -ME_onshore_wind,3,1,1,0,0,97200,43205,0.1,9.12,1,ME,1 \ No newline at end of file +Resource,Zone,Num_VRE_Bins,New_Build,Existing_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,Qualified_Hydrogen_Supply,region,cluster +MA_solar_pv,1,1,1,0,0,85300,18760,0,1,MA,1 +CT_onshore_wind,2,1,1,0,0,97200,43205,0.1,1,CT,1 +CT_solar_pv,2,1,1,0,0,85300,18760,0,1,CT,1 +ME_onshore_wind,3,1,1,0,0,97200,43205,0.1,1,ME,1 \ No newline at end of file diff --git a/example_systems/3_three_zones_w_co2_capture/resources/Vre.csv b/example_systems/3_three_zones_w_co2_capture/resources/Vre.csv index b053e3eb8a..0183631d73 100644 --- a/example_systems/3_three_zones_w_co2_capture/resources/Vre.csv +++ b/example_systems/3_three_zones_w_co2_capture/resources/Vre.csv @@ -1,5 +1,5 @@ -Resource,Zone,Num_VRE_Bins,New_Build,Can_Retire,Existing_Cap_MW,Max_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,Heat_Rate_MMBTU_per_MWh,Reg_Max,Rsv_Max,Reg_Cost,Rsv_Cost,region,cluster -MA_solar_pv,1,1,1,0,0,-1,0,85300,18760,0,9.13,0,0,0,0,MA,1 -CT_onshore_wind,2,1,1,0,0,-1,0,97200,43205,0.1,9.12,0,0,0,0,CT,1 -CT_solar_pv,2,1,1,0,0,-1,0,85300,18760,0,9.16,0,0,0,0,CT,1 -ME_onshore_wind,3,1,1,0,0,-1,0,97200,43205,0.1,9.12,0,0,0,0,ME,1 \ No newline at end of file +Resource,Zone,Num_VRE_Bins,New_Build,Can_Retire,Existing_Cap_MW,Max_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,Reg_Max,Rsv_Max,Reg_Cost,Rsv_Cost,region,cluster +MA_solar_pv,1,1,1,0,0,-1,0,85300,18760,0,0,0,0,0,MA,1 +CT_onshore_wind,2,1,1,0,0,-1,0,97200,43205,0.1,0,0,0,0,CT,1 +CT_solar_pv,2,1,1,0,0,-1,0,85300,18760,0,0,0,0,0,CT,1 +ME_onshore_wind,3,1,1,0,0,-1,0,97200,43205,0.1,0,0,0,0,ME,1 \ No newline at end of file diff --git a/example_systems/4_three_zones_w_policies_slack/resources/Vre.csv b/example_systems/4_three_zones_w_policies_slack/resources/Vre.csv index b053e3eb8a..0183631d73 100644 --- a/example_systems/4_three_zones_w_policies_slack/resources/Vre.csv +++ b/example_systems/4_three_zones_w_policies_slack/resources/Vre.csv @@ -1,5 +1,5 @@ -Resource,Zone,Num_VRE_Bins,New_Build,Can_Retire,Existing_Cap_MW,Max_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,Heat_Rate_MMBTU_per_MWh,Reg_Max,Rsv_Max,Reg_Cost,Rsv_Cost,region,cluster -MA_solar_pv,1,1,1,0,0,-1,0,85300,18760,0,9.13,0,0,0,0,MA,1 -CT_onshore_wind,2,1,1,0,0,-1,0,97200,43205,0.1,9.12,0,0,0,0,CT,1 -CT_solar_pv,2,1,1,0,0,-1,0,85300,18760,0,9.16,0,0,0,0,CT,1 -ME_onshore_wind,3,1,1,0,0,-1,0,97200,43205,0.1,9.12,0,0,0,0,ME,1 \ No newline at end of file +Resource,Zone,Num_VRE_Bins,New_Build,Can_Retire,Existing_Cap_MW,Max_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,Reg_Max,Rsv_Max,Reg_Cost,Rsv_Cost,region,cluster +MA_solar_pv,1,1,1,0,0,-1,0,85300,18760,0,0,0,0,0,MA,1 +CT_onshore_wind,2,1,1,0,0,-1,0,97200,43205,0.1,0,0,0,0,CT,1 +CT_solar_pv,2,1,1,0,0,-1,0,85300,18760,0,0,0,0,0,CT,1 +ME_onshore_wind,3,1,1,0,0,-1,0,97200,43205,0.1,0,0,0,0,ME,1 \ No newline at end of file diff --git a/example_systems/5_three_zones_w_piecewise_fuel/resources/Vre.csv b/example_systems/5_three_zones_w_piecewise_fuel/resources/Vre.csv index b053e3eb8a..0183631d73 100644 --- a/example_systems/5_three_zones_w_piecewise_fuel/resources/Vre.csv +++ b/example_systems/5_three_zones_w_piecewise_fuel/resources/Vre.csv @@ -1,5 +1,5 @@ -Resource,Zone,Num_VRE_Bins,New_Build,Can_Retire,Existing_Cap_MW,Max_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,Heat_Rate_MMBTU_per_MWh,Reg_Max,Rsv_Max,Reg_Cost,Rsv_Cost,region,cluster -MA_solar_pv,1,1,1,0,0,-1,0,85300,18760,0,9.13,0,0,0,0,MA,1 -CT_onshore_wind,2,1,1,0,0,-1,0,97200,43205,0.1,9.12,0,0,0,0,CT,1 -CT_solar_pv,2,1,1,0,0,-1,0,85300,18760,0,9.16,0,0,0,0,CT,1 -ME_onshore_wind,3,1,1,0,0,-1,0,97200,43205,0.1,9.12,0,0,0,0,ME,1 \ No newline at end of file +Resource,Zone,Num_VRE_Bins,New_Build,Can_Retire,Existing_Cap_MW,Max_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,Reg_Max,Rsv_Max,Reg_Cost,Rsv_Cost,region,cluster +MA_solar_pv,1,1,1,0,0,-1,0,85300,18760,0,0,0,0,0,MA,1 +CT_onshore_wind,2,1,1,0,0,-1,0,97200,43205,0.1,0,0,0,0,CT,1 +CT_solar_pv,2,1,1,0,0,-1,0,85300,18760,0,0,0,0,0,CT,1 +ME_onshore_wind,3,1,1,0,0,-1,0,97200,43205,0.1,0,0,0,0,ME,1 \ No newline at end of file diff --git a/example_systems/6_three_zones_w_multistage/inputs/inputs_p1/resources/Vre.csv b/example_systems/6_three_zones_w_multistage/inputs/inputs_p1/resources/Vre.csv index 7aba14a14a..12d317ff1e 100644 --- a/example_systems/6_three_zones_w_multistage/inputs/inputs_p1/resources/Vre.csv +++ b/example_systems/6_three_zones_w_multistage/inputs/inputs_p1/resources/Vre.csv @@ -1,5 +1,5 @@ -Resource,Zone,Num_VRE_Bins,New_Build,Can_Retire,Existing_Cap_MW,Max_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,Heat_Rate_MMBTU_per_MWh,Reg_Max,Rsv_Max,Reg_Cost,Rsv_Cost,region,cluster -MA_solar_pv,1,1,1,1,0,-1,0,85300,18760,0,9.13,0,0,0,0,MA,1 -CT_onshore_wind,2,1,1,1,0,-1,0,97200,43205,0.1,9.12,0,0,0,0,CT,1 -CT_solar_pv,2,1,1,1,0,-1,0,85300,18760,0,9.16,0,0,0,0,CT,1 -ME_onshore_wind,3,1,1,1,0,-1,0,97200,43205,0.1,9.12,0,0,0,0,ME,1 \ No newline at end of file +Resource,Zone,Num_VRE_Bins,New_Build,Can_Retire,Existing_Cap_MW,Max_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,Reg_Max,Rsv_Max,Reg_Cost,Rsv_Cost,region,cluster +MA_solar_pv,1,1,1,1,0,-1,0,85300,18760,0,0,0,0,0,MA,1 +CT_onshore_wind,2,1,1,1,0,-1,0,97200,43205,0.1,0,0,0,0,CT,1 +CT_solar_pv,2,1,1,1,0,-1,0,85300,18760,0,0,0,0,0,CT,1 +ME_onshore_wind,3,1,1,1,0,-1,0,97200,43205,0.1,0,0,0,0,ME,1 \ No newline at end of file diff --git a/example_systems/6_three_zones_w_multistage/inputs/inputs_p2/resources/Vre.csv b/example_systems/6_three_zones_w_multistage/inputs/inputs_p2/resources/Vre.csv index 7aba14a14a..12d317ff1e 100644 --- a/example_systems/6_three_zones_w_multistage/inputs/inputs_p2/resources/Vre.csv +++ b/example_systems/6_three_zones_w_multistage/inputs/inputs_p2/resources/Vre.csv @@ -1,5 +1,5 @@ -Resource,Zone,Num_VRE_Bins,New_Build,Can_Retire,Existing_Cap_MW,Max_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,Heat_Rate_MMBTU_per_MWh,Reg_Max,Rsv_Max,Reg_Cost,Rsv_Cost,region,cluster -MA_solar_pv,1,1,1,1,0,-1,0,85300,18760,0,9.13,0,0,0,0,MA,1 -CT_onshore_wind,2,1,1,1,0,-1,0,97200,43205,0.1,9.12,0,0,0,0,CT,1 -CT_solar_pv,2,1,1,1,0,-1,0,85300,18760,0,9.16,0,0,0,0,CT,1 -ME_onshore_wind,3,1,1,1,0,-1,0,97200,43205,0.1,9.12,0,0,0,0,ME,1 \ No newline at end of file +Resource,Zone,Num_VRE_Bins,New_Build,Can_Retire,Existing_Cap_MW,Max_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,Reg_Max,Rsv_Max,Reg_Cost,Rsv_Cost,region,cluster +MA_solar_pv,1,1,1,1,0,-1,0,85300,18760,0,0,0,0,0,MA,1 +CT_onshore_wind,2,1,1,1,0,-1,0,97200,43205,0.1,0,0,0,0,CT,1 +CT_solar_pv,2,1,1,1,0,-1,0,85300,18760,0,0,0,0,0,CT,1 +ME_onshore_wind,3,1,1,1,0,-1,0,97200,43205,0.1,0,0,0,0,ME,1 \ No newline at end of file diff --git a/example_systems/6_three_zones_w_multistage/inputs/inputs_p3/resources/Vre.csv b/example_systems/6_three_zones_w_multistage/inputs/inputs_p3/resources/Vre.csv index 7aba14a14a..12d317ff1e 100644 --- a/example_systems/6_three_zones_w_multistage/inputs/inputs_p3/resources/Vre.csv +++ b/example_systems/6_three_zones_w_multistage/inputs/inputs_p3/resources/Vre.csv @@ -1,5 +1,5 @@ -Resource,Zone,Num_VRE_Bins,New_Build,Can_Retire,Existing_Cap_MW,Max_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,Heat_Rate_MMBTU_per_MWh,Reg_Max,Rsv_Max,Reg_Cost,Rsv_Cost,region,cluster -MA_solar_pv,1,1,1,1,0,-1,0,85300,18760,0,9.13,0,0,0,0,MA,1 -CT_onshore_wind,2,1,1,1,0,-1,0,97200,43205,0.1,9.12,0,0,0,0,CT,1 -CT_solar_pv,2,1,1,1,0,-1,0,85300,18760,0,9.16,0,0,0,0,CT,1 -ME_onshore_wind,3,1,1,1,0,-1,0,97200,43205,0.1,9.12,0,0,0,0,ME,1 \ No newline at end of file +Resource,Zone,Num_VRE_Bins,New_Build,Can_Retire,Existing_Cap_MW,Max_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,Reg_Max,Rsv_Max,Reg_Cost,Rsv_Cost,region,cluster +MA_solar_pv,1,1,1,1,0,-1,0,85300,18760,0,0,0,0,0,MA,1 +CT_onshore_wind,2,1,1,1,0,-1,0,97200,43205,0.1,0,0,0,0,CT,1 +CT_solar_pv,2,1,1,1,0,-1,0,85300,18760,0,0,0,0,0,CT,1 +ME_onshore_wind,3,1,1,1,0,-1,0,97200,43205,0.1,0,0,0,0,ME,1 \ No newline at end of file diff --git a/example_systems/7_three_zones_w_colocated_VRE_storage/resources/Vre.csv b/example_systems/7_three_zones_w_colocated_VRE_storage/resources/Vre.csv index b053e3eb8a..0183631d73 100644 --- a/example_systems/7_three_zones_w_colocated_VRE_storage/resources/Vre.csv +++ b/example_systems/7_three_zones_w_colocated_VRE_storage/resources/Vre.csv @@ -1,5 +1,5 @@ -Resource,Zone,Num_VRE_Bins,New_Build,Can_Retire,Existing_Cap_MW,Max_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,Heat_Rate_MMBTU_per_MWh,Reg_Max,Rsv_Max,Reg_Cost,Rsv_Cost,region,cluster -MA_solar_pv,1,1,1,0,0,-1,0,85300,18760,0,9.13,0,0,0,0,MA,1 -CT_onshore_wind,2,1,1,0,0,-1,0,97200,43205,0.1,9.12,0,0,0,0,CT,1 -CT_solar_pv,2,1,1,0,0,-1,0,85300,18760,0,9.16,0,0,0,0,CT,1 -ME_onshore_wind,3,1,1,0,0,-1,0,97200,43205,0.1,9.12,0,0,0,0,ME,1 \ No newline at end of file +Resource,Zone,Num_VRE_Bins,New_Build,Can_Retire,Existing_Cap_MW,Max_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,Reg_Max,Rsv_Max,Reg_Cost,Rsv_Cost,region,cluster +MA_solar_pv,1,1,1,0,0,-1,0,85300,18760,0,0,0,0,0,MA,1 +CT_onshore_wind,2,1,1,0,0,-1,0,97200,43205,0.1,0,0,0,0,CT,1 +CT_solar_pv,2,1,1,0,0,-1,0,85300,18760,0,0,0,0,0,CT,1 +ME_onshore_wind,3,1,1,0,0,-1,0,97200,43205,0.1,0,0,0,0,ME,1 \ No newline at end of file diff --git a/example_systems/8_three_zones_w_retrofit/resources/Storage.csv b/example_systems/8_three_zones_w_retrofit/resources/Storage.csv index 31d9d93a89..0e776f93a9 100644 --- a/example_systems/8_three_zones_w_retrofit/resources/Storage.csv +++ b/example_systems/8_three_zones_w_retrofit/resources/Storage.csv @@ -1,4 +1,4 @@ -Resource,Zone,Model,LDS,New_Build,Can_Retire,Existing_Cap_MW,Existing_Cap_MWh,Existing_Charge_Cap_MW,Max_Cap_MW,Max_Cap_MWh,Max_Charge_Cap_MW,Min_Cap_MW,Min_Cap_MWh,Min_Charge_Cap_MW,Inv_Cost_per_MWyr,Inv_Cost_per_MWhyr,Inv_Cost_Charge_per_MWyr,Fixed_OM_Cost_per_MWyr,Fixed_OM_Cost_per_MWhyr,Fixed_OM_Cost_Charge_per_MWyr,Var_OM_Cost_per_MWh,Var_OM_Cost_per_MWh_In,Heat_Rate_MMBTU_per_MWh,Fuel,Cap_Size,Start_Cost_per_MW,Start_Fuel_MMBTU_per_MW,Ramp_Up_Percentage,Ramp_Dn_Percentage,Hydro_Energy_to_Power_Ratio,Min_Power,Self_Disch,Eff_Up,Eff_Down,Min_Duration,Max_Duration,Reg_Max,Rsv_Max,Reg_Cost,Rsv_Cost,MGA,Resource_Type,region,cluster -MA_battery,1,1,0,1,0,0,0,0,-1,-1,-1,0,0,0,19584,22494,0,4895,5622,0,0.15,0.15,0.0,None,0,0,0,1.0,1.0,0,0.0,0,0.92,0.92,1,10,0.0,0.0,0,0,0,battery_mid,MA,0 -CT_battery,2,1,0,1,0,0,0,0,-1,-1,-1,0,0,0,19584,22494,0,4895,5622,0,0.15,0.15,0.0,None,0,0,0,1.0,1.0,0,0.0,0,0.92,0.92,1,10,0.0,0.0,0,0,0,battery_mid,CT,0 -ME_battery,3,1,0,1,0,0,0,0,-1,-1,-1,0,0,0,19584,22494,0,4895,5622,0,0.15,0.15,0.0,None,0,0,0,1.0,1.0,0,0.0,0,0.92,0.92,1,10,0.0,0.0,0,0,0,battery_mid,ME,0 +Resource,Zone,Model,New_Build,Can_Retire,Existing_Cap_MW,Existing_Cap_MWh,Max_Cap_MW,Max_Cap_MWh,Min_Cap_MW,Min_Cap_MWh,Inv_Cost_per_MWyr,Inv_Cost_per_MWhyr,Fixed_OM_Cost_per_MWyr,Fixed_OM_Cost_per_MWhyr,Var_OM_Cost_per_MWh,Var_OM_Cost_per_MWh_In,Cap_Size,Start_Cost_per_MW,Ramp_Up_Percentage,Ramp_Dn_Percentage,Min_Power,Self_Disch,Eff_Up,Eff_Down,Min_Duration,Max_Duration,Reg_Max,Rsv_Max,Reg_Cost,Rsv_Cost,MGA,Resource_Type,region,cluster +MA_battery,1,1,1,0,0,0,-1,-1,0,0,19584,22494,4895,5622,0.15,0.15,0,0,1,1,0,0,0.92,0.92,1,10,0,0,0,0,0,battery_mid,MA,0 +CT_battery,2,1,1,0,0,0,-1,-1,0,0,19584,22494,4895,5622,0.15,0.15,0,0,1,1,0,0,0.92,0.92,1,10,0,0,0,0,0,battery_mid,CT,0 +ME_battery,3,1,1,0,0,0,-1,-1,0,0,19584,22494,4895,5622,0.15,0.15,0,0,1,1,0,0,0.92,0.92,1,10,0,0,0,0,0,battery_mid,ME,0 \ No newline at end of file diff --git a/example_systems/8_three_zones_w_retrofit/resources/Vre.csv b/example_systems/8_three_zones_w_retrofit/resources/Vre.csv index 7b80e308d7..deb34b92f9 100644 --- a/example_systems/8_three_zones_w_retrofit/resources/Vre.csv +++ b/example_systems/8_three_zones_w_retrofit/resources/Vre.csv @@ -1,5 +1,5 @@ -Resource,Zone,LDS,Num_VRE_Bins,New_Build,Can_Retire,Existing_Cap_MW,Max_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,Heat_Rate_MMBTU_per_MWh,Fuel,Cap_Size,Start_Cost_per_MW,Start_Fuel_MMBTU_per_MW,Ramp_Up_Percentage,Ramp_Dn_Percentage,Hydro_Energy_to_Power_Ratio,Min_Power,Reg_Max,Rsv_Max,Reg_Cost,Rsv_Cost,MGA,Resource_Type,region,cluster -MA_solar_pv,1,0,1,1,0,0,-1,0,85300,18760,0.0,9.13,None,0,0,0,1.0,1.0,0,0.0,0.0,0.0,0,0,1,solar_photovoltaic,MA,1 -CT_onshore_wind,2,0,1,1,0,0,-1,0,97200,43205,0.1,9.12,None,0,0,0,1.0,1.0,0,0.0,0.0,0.0,0,0,1,onshore_wind_turbine,CT,1 -CT_solar_pv,2,0,1,1,0,0,-1,0,85300,18760,0.0,9.16,None,0,0,0,1.0,1.0,0,0.0,0.0,0.0,0,0,1,solar_photovoltaic,CT,1 -ME_onshore_wind,3,0,1,1,0,0,-1,0,97200,43205,0.1,9.12,None,0,0,0,1.0,1.0,0,0.0,0.0,0.0,0,0,1,onshore_wind_turbine,ME,1 +Resource,Zone,Num_VRE_Bins,New_Build,Can_Retire,Existing_Cap_MW,Max_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,Reg_Max,Rsv_Max,Reg_Cost,Rsv_Cost,MGA,Resource_Type,region,cluster +MA_solar_pv,1,1,1,0,0,-1,0,85300,18760,0,0,0,0,0,1,solar_photovoltaic,MA,1 +CT_onshore_wind,2,1,1,0,0,-1,0,97200,43205,0.1,0,0,0,0,1,onshore_wind_turbine,CT,1 +CT_solar_pv,2,1,1,0,0,-1,0,85300,18760,0,0,0,0,0,1,solar_photovoltaic,CT,1 +ME_onshore_wind,3,1,1,0,0,-1,0,97200,43205,0.1,0,0,0,0,1,onshore_wind_turbine,ME,1 \ No newline at end of file From 1102d329826fe9ee58a7c2408e2a922cbbd465f4 Mon Sep 17 00:00:00 2001 From: Luca Bonaldo <39280783+lbonaldo@users.noreply.github.com> Date: Fri, 19 Apr 2024 11:47:56 -0400 Subject: [PATCH 43/59] Add can_retire validation for multi-stage optimization (#683) --- CHANGELOG.md | 2 + src/case_runners/case_runner.jl | 4 ++ .../configure_multi_stage_inputs.jl | 34 +++++++++ test/test_multistage.jl | 70 +++++++++++++++++++ 4 files changed, 110 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 199da72a7a..f9dc73862e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added versioned doc-pages for v0.3.6 and v0.4.0 - Add constraint in mga to compute total capacity in each zone from a given technology type (#681) - New settings parameter MGAAnnualGeneration to switch between different MGA formulations (#681) +- Add validation for `Can_Retire` column in multi-stage GenX since the current implementation + does not allow a resource to switch from can_retire = 0 to can_retire = 1 between stages. (#683) ### Fixed - Set MUST_RUN=1 for RealSystemExample/small_hydro plants (#517). diff --git a/src/case_runners/case_runner.jl b/src/case_runners/case_runner.jl index 13b30ac7b5..c96af53dbc 100644 --- a/src/case_runners/case_runner.jl +++ b/src/case_runners/case_runner.jl @@ -159,6 +159,10 @@ function run_genx_case_multistage!(case::AbstractString, mysetup::Dict, optimize model_dict[t] = generate_model(mysetup, inputs_dict[t], OPTIMIZER) end + # check that resources do not switch from can_retire = 0 to can_retire = 1 between stages + validate_can_retire_multistage( + inputs_dict, mysetup["MultiStageSettingsDict"]["NumStages"]) + ### Solve model println("Solving Model") diff --git a/src/multi_stage/configure_multi_stage_inputs.jl b/src/multi_stage/configure_multi_stage_inputs.jl index bbf4bb3431..d2f024d9a8 100644 --- a/src/multi_stage/configure_multi_stage_inputs.jl +++ b/src/multi_stage/configure_multi_stage_inputs.jl @@ -217,3 +217,37 @@ function configure_multi_stage_inputs(inputs_d::Dict, return inputs_d end + +@doc raw""" + validate_can_retire_multistage(inputs_dict::Dict, num_stages::Int) + +This function validates that all the resources do not switch from havig `can_retire = 0` to `can_retire = 1` during the multi-stage optimization. + +# Arguments +- `inputs_dict::Dict`: A dictionary containing the inputs for each stage. +- `num_stages::Int`: The number of stages in the multi-stage optimization. + +# Returns +- Throws an error if a resource switches from `can_retire = 0` to `can_retire = 1` between stages. +""" +function validate_can_retire_multistage(inputs_dict::Dict, num_stages::Int) + for stage in 2:num_stages # note: loop starts from 2 because we are comparing stage t with stage t-1 + can_retire_current = can_retire.(inputs_dict[stage]["RESOURCES"]) + can_retire_previous = can_retire.(inputs_dict[stage - 1]["RESOURCES"]) + + # Check if any resource switched from can_retire = 0 to can_retire = 1 between stage t-1 and t + if any(can_retire_current .- can_retire_previous .> 0) + # Find the resources that switched from can_retire = 0 to can_retire = 1 and throw an error + retire_switch_ids = findall(can_retire_current .- can_retire_previous .> 0) + resources_switched = inputs_dict[stage]["RESOURCES"][retire_switch_ids] + for resource in resources_switched + @warn "Resource `$(resource_name(resource))` with id = $(resource_id(resource)) switched " * + "from can_retire = 0 to can_retire = 1 between stages $(stage - 1) and $stage" + end + msg = "Current implementation of multi-stage optimization does not allow resources " * + "to switch from can_retire = 0 to can_retire = 1 between stages." + error(msg) + end + end + return nothing +end diff --git a/test/test_multistage.jl b/test/test_multistage.jl index 4215bca5fc..9798f3789b 100644 --- a/test/test_multistage.jl +++ b/test/test_multistage.jl @@ -185,4 +185,74 @@ end test_update_cumulative_min_ret!() +function test_can_retire_validation() + @testset "No resources switch from can_retire = 0 to can_retire = 1" begin + inputs = Dict{Int, Dict}() + inputs[1] = Dict("RESOURCES" => [ + GenX.Thermal(Dict(:resource => "thermal", :id => 1, + :can_retire => 1)), + GenX.Vre(Dict(:resource => "vre", :id => 2, + :can_retire => 1)), + GenX.Hydro(Dict(:resource => "hydro", :id => 3, + :can_retire => 1)), + GenX.FlexDemand(Dict(:resource => "flex_demand", :id => 4, + :can_retire => 1))]) + inputs[2] = Dict("RESOURCES" => [ + GenX.Thermal(Dict(:resource => "thermal", :id => 1, + :can_retire => 0)), + GenX.Vre(Dict(:resource => "vre", :id => 2, + :can_retire => 1)), + GenX.Hydro(Dict(:resource => "hydro", :id => 3, + :can_retire => 1)), + GenX.FlexDemand(Dict(:resource => "flex_demand", :id => 4, + :can_retire => 1))]) + inputs[3] = Dict("RESOURCES" => [ + GenX.Thermal(Dict(:resource => "thermal", :id => 1, + :can_retire => 0)), + GenX.Vre(Dict(:resource => "vre", :id => 2, + :can_retire => 0)), + GenX.Hydro(Dict(:resource => "hydro", :id => 3, + :can_retire => 1)), + GenX.FlexDemand(Dict(:resource => "flex_demand", :id => 4, + :can_retire => 1))]) + @test isnothing(GenX.validate_can_retire_multistage(inputs, 3)) + end + + @testset "One resource switches from can_retire = 0 to can_retire = 1" begin + inputs = Dict{Int, Dict}() + inputs[1] = Dict("RESOURCES" => [ + GenX.Thermal(Dict(:resource => "thermal", :id => 1, + :can_retire => 0)), + GenX.Vre(Dict(:resource => "vre", :id => 2, + :can_retire => 0)), + GenX.Hydro(Dict(:resource => "hydro", :id => 3, + :can_retire => 0)), + GenX.FlexDemand(Dict(:resource => "flex_demand", :id => 4, + :can_retire => 1))]) + inputs[2] = Dict("RESOURCES" => [ + GenX.Thermal(Dict(:resource => "thermal", :id => 1, + :can_retire => 0)), + GenX.Vre(Dict(:resource => "vre", :id => 2, + :can_retire => 0)), + GenX.Hydro(Dict(:resource => "hydro", :id => 3, + :can_retire => 1)), + GenX.FlexDemand(Dict(:resource => "flex_demand", :id => 4, + :can_retire => 1))]) + inputs[3] = Dict("RESOURCES" => [ + GenX.Thermal(Dict(:resource => "thermal", :id => 1, + :can_retire => 0)), + GenX.Vre(Dict(:resource => "vre", :id => 2, + :can_retire => 0)), + GenX.Hydro(Dict(:resource => "hydro", :id => 3, + :can_retire => 1)), + GenX.FlexDemand(Dict(:resource => "flex_demand", :id => 4, + :can_retire => 1))]) + @test_throws ErrorException GenX.validate_can_retire_multistage(inputs, 3) + end +end + +with_logger(ConsoleLogger(stderr, Logging.Error)) do + test_can_retire_validation() +end + end # module TestMultiStage From 5ba48305f0e8233491aace70eb534a31fad5894c Mon Sep 17 00:00:00 2001 From: Maya Mutic <62064527+mmutic@users.noreply.github.com> Date: Fri, 19 Apr 2024 12:09:34 -0400 Subject: [PATCH 44/59] Update docs with new Tutorials (#685) --- CHANGELOG.md | 1 + docs/make.jl | 39 +- .../Tutorial_1_configuring_settings.md | 16 +- .../Tutorial_2_network_visualization.md | 7 +- ...utorial_3_K-means_time_domain_reduction.md | 47 +- .../Tutorials/Tutorial_4_model_generation.md | 30 +- docs/src/Tutorials/Tutorial_7_setup.md | 2872 +++++++++++++++++ docs/src/Tutorials/Tutorial_8_outputs.md | 875 +++++ docs/src/Tutorials/Tutorials_intro.md | 2 + docs/src/Tutorials/files/Julia.png | Bin 0 -> 80822 bytes docs/src/Tutorials/files/LatexHierarchy.png | Bin 93747 -> 43427 bytes docs/src/Tutorials/files/OneZoneCase.png | Bin 324610 -> 0 bytes docs/src/Tutorials/files/addGenX.png | Bin 0 -> 6962 bytes docs/src/Tutorials/files/addIJulia.png | Bin 0 -> 38239 bytes docs/src/Tutorials/files/default_settings.png | Bin 269573 -> 72669 bytes .../Tutorials/files/genx_settings_none.png | Bin 55051 -> 11676 bytes docs/src/Tutorials/files/genxsettings.png | Bin 447480 -> 153682 bytes docs/src/Tutorials/files/highs_defaults.png | Bin 944058 -> 241471 bytes docs/src/Tutorials/files/jump_logo.png | Bin 30453 -> 10166 bytes docs/src/Tutorials/files/jupyter_screen.png | Bin 0 -> 75417 bytes docs/src/Tutorials/files/new_england.png | Bin 318652 -> 97054 bytes docs/src/Tutorials/files/opennotebook.png | Bin 0 -> 7690 bytes docs/src/Tutorials/files/output_58_0.svg | 1 - docs/src/Tutorials/files/output_65_0.svg | 1 - docs/src/Tutorials/files/runcase.png | Bin 343629 -> 149121 bytes docs/src/Tutorials/files/statenames.csv | 2 - docs/src/Tutorials/files/states.csv | 50 - docs/src/Tutorials/files/t3_TDR_demand.svg | 1 + .../files/{output_14_0.svg => t3_demand.svg} | 2 +- docs/src/Tutorials/files/t3_ext_periods.svg | 1 + docs/src/Tutorials/files/t3_kmeans.png | Bin 0 -> 55154 bytes docs/src/Tutorials/files/t3_nokmeans.png | Bin 0 -> 58730 bytes docs/src/Tutorials/files/t3_obj_vals.svg | 82 + docs/src/Tutorials/files/t3_recon.svg | 1 + docs/src/Tutorials/files/t7_1p_none.svg | 54 + docs/src/Tutorials/files/t7_2p_load_mass.svg | 87 + docs/src/Tutorials/files/t7_2p_mass_none.svg | 83 + docs/src/Tutorials/files/t7_2p_mass_zero.svg | 83 + .../Tutorials/files/t7_3p_csm_esr_mass.svg | 113 + .../Tutorials/files/t7_3p_mass_load_gen.svg | 122 + docs/src/Tutorials/files/t7_3p_slack.svg | 114 + .../files/t7_4p_esr_mass_load_gen.svg | 154 + .../files/t7_4p_mcr_csm_esr_mass.svg | 142 + .../Tutorials/files/t7_mcr_esr_csm_load.svg | 52 + docs/src/Tutorials/files/t8_cap.svg | 1 + docs/src/Tutorials/files/t8_cost.svg | 291 ++ docs/src/Tutorials/files/t8_emm1.svg | 1 + docs/src/Tutorials/files/t8_emm2.svg | 1 + docs/src/Tutorials/files/t8_emm_comp.svg | 66 + docs/src/Tutorials/files/t8_heatmap.svg | 371 +++ .../files/t8_resource_allocation.svg | 82 + docs/src/Tutorials/files/usingIJulia.png | Bin 0 -> 44119 bytes 52 files changed, 5733 insertions(+), 114 deletions(-) create mode 100644 docs/src/Tutorials/Tutorial_7_setup.md create mode 100644 docs/src/Tutorials/Tutorial_8_outputs.md create mode 100644 docs/src/Tutorials/files/Julia.png delete mode 100644 docs/src/Tutorials/files/OneZoneCase.png create mode 100644 docs/src/Tutorials/files/addGenX.png create mode 100644 docs/src/Tutorials/files/addIJulia.png create mode 100644 docs/src/Tutorials/files/jupyter_screen.png create mode 100644 docs/src/Tutorials/files/opennotebook.png delete mode 100644 docs/src/Tutorials/files/output_58_0.svg delete mode 100644 docs/src/Tutorials/files/output_65_0.svg delete mode 100644 docs/src/Tutorials/files/statenames.csv delete mode 100644 docs/src/Tutorials/files/states.csv create mode 100644 docs/src/Tutorials/files/t3_TDR_demand.svg rename docs/src/Tutorials/files/{output_14_0.svg => t3_demand.svg} (99%) create mode 100644 docs/src/Tutorials/files/t3_ext_periods.svg create mode 100644 docs/src/Tutorials/files/t3_kmeans.png create mode 100644 docs/src/Tutorials/files/t3_nokmeans.png create mode 100644 docs/src/Tutorials/files/t3_obj_vals.svg create mode 100644 docs/src/Tutorials/files/t3_recon.svg create mode 100644 docs/src/Tutorials/files/t7_1p_none.svg create mode 100644 docs/src/Tutorials/files/t7_2p_load_mass.svg create mode 100644 docs/src/Tutorials/files/t7_2p_mass_none.svg create mode 100644 docs/src/Tutorials/files/t7_2p_mass_zero.svg create mode 100644 docs/src/Tutorials/files/t7_3p_csm_esr_mass.svg create mode 100644 docs/src/Tutorials/files/t7_3p_mass_load_gen.svg create mode 100644 docs/src/Tutorials/files/t7_3p_slack.svg create mode 100644 docs/src/Tutorials/files/t7_4p_esr_mass_load_gen.svg create mode 100644 docs/src/Tutorials/files/t7_4p_mcr_csm_esr_mass.svg create mode 100644 docs/src/Tutorials/files/t7_mcr_esr_csm_load.svg create mode 100644 docs/src/Tutorials/files/t8_cap.svg create mode 100644 docs/src/Tutorials/files/t8_cost.svg create mode 100644 docs/src/Tutorials/files/t8_emm1.svg create mode 100644 docs/src/Tutorials/files/t8_emm2.svg create mode 100644 docs/src/Tutorials/files/t8_emm_comp.svg create mode 100644 docs/src/Tutorials/files/t8_heatmap.svg create mode 100644 docs/src/Tutorials/files/t8_resource_allocation.svg create mode 100644 docs/src/Tutorials/files/usingIJulia.png diff --git a/CHANGELOG.md b/CHANGELOG.md index f9dc73862e..60e91ac0f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New settings parameter MGAAnnualGeneration to switch between different MGA formulations (#681) - Add validation for `Can_Retire` column in multi-stage GenX since the current implementation does not allow a resource to switch from can_retire = 0 to can_retire = 1 between stages. (#683) +- Add tutorials for running GenX (#637 and #685) ### Fixed - Set MUST_RUN=1 for RealSystemExample/small_hydro plants (#517). diff --git a/docs/make.jl b/docs/make.jl index 70479f3adb..5fd26c6e0f 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -4,15 +4,16 @@ import DataStructures: OrderedDict DocMeta.setdocmeta!(GenX, :DocTestSetup, :(using GenX); recursive = true) -pages = OrderedDict("Welcome Page" => [ +pages = OrderedDict( + "Welcome Page" => [ "GenX: Introduction" => "index.md", "Installation Guide" => "installation.md", "Limitation of GenX" => "limitations_genx.md", - "Third Party Extensions" => "third_party_genx.md", + "Third Party Extensions" => "third_party_genx.md" ], "Getting Started" => [ "Running GenX" => "Getting_Started/examples_casestudies.md", - "Commertial solvers" => "Getting_Started/commercial_solvers.md", + "Commertial solvers" => "Getting_Started/commercial_solvers.md" ], "Tutorials" => [ "Tutorials Overview" => "Tutorials/Tutorials_intro.md", @@ -21,7 +22,9 @@ pages = OrderedDict("Welcome Page" => [ "Tutorial 3: K-Means and Time Domain Reduction" => "Tutorials/Tutorial_3_K-means_time_domain_reduction.md", "Tutorial 4: Model Generation" => "Tutorials/Tutorial_4_model_generation.md", "Tutorial 5: Solving the Model" => "Tutorials/Tutorial_5_solve_model.md", - "Tutorial 6: Post Processing" => "Tutorials/Tutorial_6_solver_settings.md", + "Tutorial 6: Solver Settings" => "Tutorials/Tutorial_6_solver_settings.md", + "Tutorial 7: Policy Constraints" => "Tutorials/Tutorial_7_setup.md", + "Tutorial 8: Outputs" => "Tutorials/Tutorial_8_outputs.md" ], "User Guide" => [ "Overall workflow" => "User_Guide/workflow.md", @@ -35,13 +38,13 @@ pages = OrderedDict("Welcome Page" => [ "Slack Variables for Policies" => "User_Guide/slack_variables_overview.md", "Method of Morris Inputs" => "User_Guide/methodofmorris_input.md", "Running the Model" => "User_Guide/running_model.md", - "Model Outputs" => "User_Guide/model_output.md", + "Model Outputs" => "User_Guide/model_output.md" ], "Model Concept and Overview" => [ "Model Introduction" => "Model_Concept_Overview/model_introduction.md", "Notation" => "Model_Concept_Overview/model_notation.md", "Objective Function" => "Model_Concept_Overview/objective_function.md", - "Power Balance" => "Model_Concept_Overview/power_balance.md", + "Power Balance" => "Model_Concept_Overview/power_balance.md" ], "Model Reference" => [ "Core" => "Model_Reference/core.md", @@ -50,7 +53,7 @@ pages = OrderedDict("Welcome Page" => [ "Flexible Demand" => "Model_Reference/Resources/flexible_demand.md", "Hydro" => [ "Hydro Reservoir" => "Model_Reference/Resources/hydro_res.md", - "Long Duration Hydro" => "Model_Reference/Resources/hydro_inter_period_linkage.md", + "Long Duration Hydro" => "Model_Reference/Resources/hydro_inter_period_linkage.md" ], "Must Run" => "Model_Reference/Resources/must_run.md", "Retrofit" => "Model_Reference/Resources/retrofit.md", @@ -61,17 +64,17 @@ pages = OrderedDict("Welcome Page" => [ "Long Duration Storage" => "Model_Reference/Resources/long_duration_storage.md", "Storage All" => "Model_Reference/Resources/storage_all.md", "Storage Asymmetric" => "Model_Reference/Resources/storage_asymmetric.md", - "Storage Symmetric" => "Model_Reference/Resources/storage_symmetric.md", + "Storage Symmetric" => "Model_Reference/Resources/storage_symmetric.md" ], "Co-located VRE and Storage" => "Model_Reference/Resources/vre_stor.md", "Thermal" => [ "Thermal" => "Model_Reference/Resources/thermal.md", "Thermal Commit" => "Model_Reference/Resources/thermal_commit.md", - "Thermal No Commit" => "Model_Reference/Resources/thermal_no_commit.md", + "Thermal No Commit" => "Model_Reference/Resources/thermal_no_commit.md" ], "Hydrogen Electrolyzers" => "Model_Reference/Resources/electrolyzers.md", "Scheduled maintenance for various resources" => "Model_Reference/Resources/maintenance.md", - "Resource types" => "Model_Reference/Resources/resource.md", + "Resource types" => "Model_Reference/Resources/resource.md" ], "Maintenance" => "Model_Reference/maintenance_overview.md", "Policies" => "Model_Reference/policies.md", @@ -86,15 +89,16 @@ pages = OrderedDict("Welcome Page" => [ "Multi-Stage Modeling Introduction" => "Model_Reference/Multi_Stage/multi_stage_overview.md", "Configure multi-stage inputs" => "Model_Reference/Multi_Stage/configure_multi_stage_inputs.md", "Model multi stage: Dual Dynamic Programming Algorithm" => "Model_Reference/Multi_Stage/dual_dynamic_programming.md", - "Endogenous Retirement" => "Model_Reference/Multi_Stage/endogenous_retirement.md", + "Endogenous Retirement" => "Model_Reference/Multi_Stage/endogenous_retirement.md" ], "Method of Morris" => "Model_Reference/methodofmorris.md", - "Utility Functions" => "Model_Reference/utility_functions.md", + "Utility Functions" => "Model_Reference/utility_functions.md" ], "Public API Reference" => [ "Public API" => "Public_API/public_api.md"], "Third Party Extensions" => "additional_third_party_extensions.md", - "Developer Docs" => "developer_guide.md") + "Developer Docs" => "developer_guide.md" +) # Build documentation. # ==================== @@ -108,8 +112,10 @@ makedocs(; canonical = "https://genxproject.github.io/GenX.jl/stable", assets = ["assets/genx_style.css"], sidebar_sitename = false, - collapselevel = 1), - pages = [p for p in pages]) + collapselevel = 1 + ), + pages = [p for p in pages] +) # Deploy built documentation. # =========================== @@ -122,4 +128,5 @@ deploydocs(; devurl = "dev", push_preview = true, versions = ["stable" => "v^", "v#.#.#", "dev" => "dev"], - forcepush = false) + forcepush = false +) diff --git a/docs/src/Tutorials/Tutorial_1_configuring_settings.md b/docs/src/Tutorials/Tutorial_1_configuring_settings.md index 8ba1968665..a25b85a41d 100644 --- a/docs/src/Tutorials/Tutorial_1_configuring_settings.md +++ b/docs/src/Tutorials/Tutorial_1_configuring_settings.md @@ -17,15 +17,11 @@ To see how changing the settings affects the outputs, see Tutorials 3 and 7. Below is the settings file for `example_systems/1_three_zones`: -```@raw html - -``` +![png](./files/genxsettings.png) -All `genx_settings.yml` files in `Example_Systems` specify most parameters. When configuring your own settings, however, it is not necessary to input all parameters as defaults are specified for each one in `configure_settings.jl`. +All `genx_settings.yml` files in `example_systems` specify most parameters. When configuring your own settings, however, it is not necessary to input all parameters as defaults are specified for each one in `configure_settings.jl`. -```@raw html - -``` +![png](./files/default_settings.png) To open `genx_settings.yml` in Jupyter, use the function `YAML.load(open(...))` and navigate to file in the desired directory: @@ -69,11 +65,9 @@ YAML.write_file("example_systems/1_three_zones/settings/genx_settings.yml", new_ The empty file will look like this: -```@raw html - -``` +![png](./files/genx_settings_none.png) -Now, we run GenX and output the file `capacity.csv` from the `Results` folder. To do this, we use the function `include`, which takes a .jl file and runs it in jupyter notebook: +Now, we run GenX and output the file `capacity.csv` from the `results` folder. To do this, we use the function `include`, which takes a .jl file and runs it in jupyter notebook: ```julia diff --git a/docs/src/Tutorials/Tutorial_2_network_visualization.md b/docs/src/Tutorials/Tutorial_2_network_visualization.md index 67624d285f..9c598962d7 100644 --- a/docs/src/Tutorials/Tutorial_2_network_visualization.md +++ b/docs/src/Tutorials/Tutorial_2_network_visualization.md @@ -24,10 +24,9 @@ network = CSV.read("example_systems/1_three_zones/system/Network.csv",DataFrame, MA, CT, and ME are the abbreviations for states Massachusetts, Connecticut, and Maine. However, since the US region of New England contains other states as well, MA in this case is also used to refer to those states. -Columns `Start_Zone` and `End_Zone` specify the network of the three regions. In this case, there are only two network lines, specified in the `Network_Lines` columns. The `Start_Zone` column indicates that the first node, MA, is the source of both lines as both rows have value 1. Rows `z1` and `z2` have values of 2 and 3 in `End_Zone`, which means both nodes CT and ME recieve energy from node MA. This is also indicated in the column `transmission_path_name'. +Columns `Start_Zone` and `End_Zone` specify the network of the three regions. In this case, there are only two network lines, specified in the `Network_Lines` columns. The `Start_Zone` column indicates that the first node, MA, is the source of both lines as both rows have value 1. Rows `z1` and `z2` have values of 2 and 3 in `End_Zone`, which means both nodes CT and ME recieve energy from node MA. This is also indicated in the column `transmission path name'. Below is a visualization of the network: -```@raw html - -``` +![png](./files/new_england.png) + diff --git a/docs/src/Tutorials/Tutorial_3_K-means_time_domain_reduction.md b/docs/src/Tutorials/Tutorial_3_K-means_time_domain_reduction.md index 4e9a49a6e6..5648c1eadc 100644 --- a/docs/src/Tutorials/Tutorial_3_K-means_time_domain_reduction.md +++ b/docs/src/Tutorials/Tutorial_3_K-means_time_domain_reduction.md @@ -3,19 +3,19 @@ [Interactive Notebook of the tutorial](https://github.com/GenXProject/GenX-Tutorials/blob/main/Tutorials/Tutorial_3_K-means_Time_Domain_Reduction.ipynb) -A good tool to reduce computation time of GenX is to use [Time-domain reduction](@ref). Time Domain Reduction is a method that selects a smaller set of time steps from the data in a way that reduces computation time while still capturing the main information of the model. In this tutorial, we go over how TDR works in GenX and how it uses K-means clustering to choose the optimal time steps. For more information on TDR in capacity expansion models, see [Mallapragada et al](https://www.sciencedirect.com/science/article/pii/S0360544218315238). +A good tool to reduce computation time of GenX is to use [Time-domain reduction](@ref). Time-domain Reduction is a method that selects a smaller set of time steps from the data in a way that reduces computation time while still capturing the main information of the model. In this tutorial, we go over how TDR works in GenX and how it uses K-means clustering to choose the optimal time steps. For more information on TDR in capacity expansion models, see [Mallapragada et al](https://www.sciencedirect.com/science/article/pii/S0360544218315238). ### Table of Contents * [Time Domain Reduction](#TDR) * [K-Means Clustering](#Kmeans) * [Results of Time Domain Reduction](#TDRResults) - * [Reconstruction](#Reconstruction) +* [Reconstruction](#Reconstruction) * [Extreme Periods](#ExtPeriods) * [Objective Values and Representative Periods](#ObjVals) ### Time Domain Reduction -To see how Time Domain Reduction works, let's look at the `Doad_data` in `example_systems/1_three_zones`: +To see how Time Domain Reduction works, let's look at the `Demand_data` in `example_systems/1_three_zones`: ```julia @@ -29,9 +29,6 @@ using Plots using Clustering using ScikitLearn @sk_import datasets: (make_blobs) -``` - WARNING: redefinition of constant make_blobs. This may fail, cause incorrect answers, or produce other errors. - PyObject ```julia case = joinpath("example_systems/1_three_zones"); @@ -88,7 +85,7 @@ loads |> width=600,height=400,linewidth=.01) ``` -![svg](./files/output_14_0.svg) +![svg](./files/t3_demand.svg) As in [Tutorial 1: Configuring Settings](@ref), we can open the `genx_settings.yml` file for `1_three_zones` to see how `TimeDomainReduction` is set. If it's set to 1, this means TDR is being used. @@ -118,7 +115,7 @@ genx_settings_TZ = YAML.load(open((joinpath(case,"settings/genx_settings.yml"))) "WriteShadowPrices" => 1 ``` -To visualize how TDR decreases computation time, let's start by running `SmallNewEngland/OneZone` without TDR. In the third section of this tutorial, we'll run the example again using TDR. +To visualize how TDR decreases computation time, let's start by running `example_systems/1_three_zones` without TDR. In the third section of this tutorial, we'll run the example again using TDR. To run GenX without TDR, we start by editing the settings to set `TimeDomainReduction` to 0: @@ -158,14 +155,16 @@ And run it using `include`. (Note: this process will take a few minutes): ```julia @time include("example_systems/1_three_zones/Run.jl") ``` +Time elapsed for writing is + 142.404724 seconds This took a little while to run, and would take even longer for larger systems. Let's see how we can get the run time down using Time Domain Reduction. The next sections go over how K-means clustering is used to perform TDR, and how to interpret the resulting files in GenX. ### K-means clustering -Let's go over how TDR works. To perform TDR, GenX uses __K-means clustering__. K-means is an optimization method that clusters data into several groups based on their proximity to "centers" determined by the algorithm. +Let's go over how TDR works. To perform TDR, GenX uses __K-means clustering__. _K_-means is an optimization method that clusters data into several groups based on their proximity to "centers" determined by the algorithm. -K-means finds a set number of groups such that the variance between the distance of each point in the group to the mean of the group is minimized. +_K_-means finds a set number of groups such that the variance between the distance of each point in the group to the mean of the group is minimized. ```math \begin{align*} @@ -173,7 +172,7 @@ K-means finds a set number of groups such that the variance between the distance \end{align*} ``` -Where $\mathbf{S} = \{S_1, ... , S_k\}$ are the clusters, with $x$ denoting the elements of the clusters, and $\mu_i$ the mean of each cluster, i.e. the mean of the distances from each point to the center of the cluster. By taking the argmin over $\mathbf{S}$, the points $x$ are clustered into groups where their distance to the center is the smallest. For more information on how k-means works, see the [Wikipedia](https://en.wikipedia.org/wiki/K-means_clustering). +Where $\mathbf{S} = \{S_1, ... , S_k\}$ are the clusters, with $x$ denoting the elements of the clusters, and $\mu_i$ the mean of each cluster, i.e. the mean of the distances from each point to the center of the cluster. By taking the argmin over $\mathbf{S}$, the points $x$ are clustered into groups where their distance to the center is the smallest. For more information on how _k_-means works, see the [Wikipedia](https://en.wikipedia.org/wiki/K-means_clustering). GenX uses the package `Clustering.jl`, with documentation [here](https://juliastats.org/Clustering.jl/dev/kmeans.html#K-means). As an example, using the package `ScikitLearn.jl`, let's generate data that can cluster easily. @@ -184,7 +183,7 @@ X, y = make_blobs(n_samples=50,centers=centers); # From scikit-learn b = DataFrame(X,:auto) ``` -Note that clustering works for data without obvious groupings, but using blobs as an example makes k-means easier to visualize. +Note that clustering works for data without obvious groupings, but using blobs as an example makes _k_-means easier to visualize. ```julia @@ -192,6 +191,8 @@ plotly() Plots.scatter(b[!,"x1"],b[!,"x2"],legend=false,title="Before K-means Clustering") ``` +![png](./files/t3_nokmeans.png) + Now we use the function `kmeans`, which is also used in `src/time_domain_reduction` in GenX. @@ -217,11 +218,13 @@ plotly() Plots.scatter(b[!,"x1"],b[!,"x2"],legend=false,marker_z=R.assignments,c=:lightrainbow,title="After K-means Clustering") ``` +![png](./files/t3_kmeans.png) + In GenX, the representative periods are the centers of the clusters, each representing one week of the year. In the above example that would mean there are 52 data points gathered into 11 clusters (to see this for yourself, change `make_blobs` to have 52 data points and 11 clusters.) ### Results of Time Domain Reduction -To visualize the results of TDR, we'll set TDR = 1 back in the `genx_settings.yml` file in `Example_Systems_Tutorials/SmallNewEngland/OneZone/`: +To visualize the results of TDR, we'll set TDR = 1 back in the `genx_settings.yml` file in `example_systems/1_three_zones`: ```julia @@ -245,7 +248,7 @@ And run GenX again with TDR: @time include("example_systems/1_three_zones/Run.jl") ``` -Csv files with the results of TDR are generated automatically in a folder called `TDR_results` found within the same folder containing the input csv files, in this case `Example_Systems_Tutorials/SmallNewEngland/OneZone`. The csv files in this folder show the files used in `Run.jl` that have been pared down from the initial input files. +Csv files with the results of TDR are generated automatically in a folder called `TDR_results` found within the same folder containing the input csv files, in this case `example_systems/1_three_zones`. The csv files in this folder show the files used in `Run.jl` that have been pared down from the initial input files. As an example, consider the input file `Fuels_data.csv`: @@ -268,7 +271,7 @@ As you can see, the original has all 8,760 hours, while the TDR version only has loads_TDR = CSV.read(joinpath(case,"TDR_Results/Demand_data.csv"),DataFrame,missingstring="NA") ``` -The 1,848 hours are divided into 11 sections of 168 hours, with each section representing one week of the original data. The number of hours per representative period is set in `time_domain_reduction_settings.yml`. Also specified in the file are the minimum and maximum number of clusters we would like to have (in this case 8 and 11). The k-means algorithm will then select the number of clusters that should be sufficient to capture the GenX model in fewer time steps (in this case 11). +The 1,848 hours are divided into 11 sections of 168 hours, with each section representing one week of the original data. The number of hours per representative period is set in `time_domain_reduction_settings.yml`. Also specified in the file are the minimum and maximum number of clusters we would like to have (in this case 8 and 11). The _k_-means algorithm will then select the number of clusters that should be sufficient to capture the GenX model in fewer time steps (in this case 11). ```julia @@ -286,7 +289,9 @@ Below, we create arrays out of the representative weeks and plot them on the sam ```julia Period_map = CSV.read(joinpath(case,"TDR_Results/Period_map.csv"),DataFrame,missingstring="NA") ``` - +``` @raw html +
52×3 DataFrame
27 rows omitted
RowPeriod_IndexRep_PeriodRep_Period_Index
Int64Int64Int64
1141
2241
3341
4441
5582
6682
7782
8882
9982
1010123
1111123
1212123
1313123
4141205
4242205
4343236
4444174
45454810
46464810
47474810
48484810
49494911
50504911
515182
525282
+``` ```julia # Find array of unique representative periods @@ -317,7 +322,7 @@ loads_with_TDR |> color={"Week:n", scale={scheme="paired"},sort="decsending"}, title="MW Load per hour with TDR Representative Weeks", width=845,height=400) ``` -![svg](./files/output_58_0.svg) +![svg](./files/t3_TDR_demand.svg) TDR is performed for four total data sets: demand (found in Demand.csv), wind and solar (found in Generators_variability.csv), and fuel prices (found in Fuels.csv). Above is just the demand load for one of the three total nodes in the example system, which is why the data may not appear to "represent" all 52 weeks (notice there are fewer representative periods in the fall). Instead, the periods more accurately represent all the data time series combined, including some other parts of the data not seen in this particular plot. @@ -384,7 +389,7 @@ myscheme = ["#a6cee3","#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c width=845,height=300) ``` -![svg](./files/output_65_0.svg) +![svg](./files/t3_ext_periods.svg) The first plot (with Extreme Periods off) may not have the week with the highest peak highlighted. If the week with the highest demand is highlighted, try re-running the cell with Extreme Periods Off plotting the results. @@ -396,7 +401,7 @@ YAML.write_file(joinpath(case,"settings/time_domain_reduction_settings.yml"), ti rm(joinpath(case,"TDR_results"), recursive=true) ``` -#### Reconstruction +### Reconstruction Below is a plot of a reconstruction of the data using only the weeks isolated as representative periods. This is what GenX reads when it runs the solver with TDR on. @@ -443,6 +448,8 @@ G2 = Plots.plot(recon[!,:hour], recon[!,:MW], linewidth=1.7, Plots.plot(G1,G2,layout=(2,1)) ``` +![svg](./files/t3_recon.svg) + Each color represents one of the representative weeks. The range of 8-11 representative periods was chosen by the developers because it was deemed to be the smallest set that still matches the optimal value of the data well. The next section of this Tutorial goes over how the optimal values of the data change as the number of representative periods changes. @@ -617,6 +624,8 @@ scatter!(twinx(),obj_val_plot[:,1],times,color=:red,markeralpha=.5,label=:"Time" ygrid!(:on, :dashdot, 0.1) ``` +![svg](./files/t3_obj_val.svg) + Here, we can see that while having very few representative periods produces an objective value that differs greatly from the orignal, once we reach around 12 representative periods the difference begins to taper out. Therefore, the original choice of 11 maximum periods in `1_three_zones` decreases the run time of GenX significantly while while maintaining an objective value close to the original. diff --git a/docs/src/Tutorials/Tutorial_4_model_generation.md b/docs/src/Tutorials/Tutorial_4_model_generation.md index 116b76fe2d..39031fb630 100644 --- a/docs/src/Tutorials/Tutorial_4_model_generation.md +++ b/docs/src/Tutorials/Tutorial_4_model_generation.md @@ -13,9 +13,7 @@ We'll start by explaining JuMP, the optimization package that GenX uses to gener * [Run generate_model](#Run) -```@raw html - -``` +![png](./files/jump_logo.png) JuMP is a modeling language for Julia. It allows users to create models for optimization problems, define variables and constraints, and apply a variety of solvers for the model. @@ -122,16 +120,30 @@ When `Run.jl` is called, the model for GenX is constructed in a similar way, but The basic structure of the way `Run.jl` generates and solves the model is as follows: -```@raw html - -``` +![png](./files/LatexHierarchy.png) The function `run_genx_case(case)` takes the "case" as its input. The case is all of the input files and settings found in the same folder as `Run.jl`. For example, in `example_systems/1_three_zones`, the case is: -```@raw html - +```julia +cd(readdir,"example_systems/1_three_zones") ``` + + + + 9-element Vector{String}: + ".DS_Store" + "README.md" + "Run.jl" + "TDR_results" + "policies" + "resources" + "results" + "settings" + "system" + + + `Run_genx_case` defines the __setup__, which are the settings in `genx_settings.yml`. From there, either `run_genx_case_simple(case, mysetup)` or`run_genx_case_multistage(case, mysetup)` is called. Both of these define the __inputs__ and __optimizer__. The optimizer is the solver as specified in `genx_settings.yml`, and the inputs are a variety of parameters specified by the settings and csv files found in the folder. Both of these functions then call `generate_model(mysetup, myinputs, OPTIMIZER)`, which is the main subject of this tutorial. As in the above example, `generate_model` utilizes the JuMP functions `Model()`, `@expression`, `@variable`, and `@constraints` to form a model. This section goes through `generate_model` and explains how the expressions are formed to create the model. @@ -260,7 +272,7 @@ typeof(OPTIMIZER) MathOptInterface.OptimizerWithAttributes ``` -The "inputs" argument is generated by the function `load_inputs` from the case in `run_genx_case_simple` (or multistage). If TDR is set to 1 in the settings file, then `load_inputs` will draw some of the files from the `TDR_Results` folder. `TDR_Results` is produced when the case is run. +The "inputs" argument is generated by the function `load_inputs` from the case in `run_genx_case_simple` (or multistage). If TDR is set to 1 in the settings file, then `load_inputs` will draw some of the files from the `TDR_results` folder. `TDR_results` is produced when the case is run. ```julia diff --git a/docs/src/Tutorials/Tutorial_7_setup.md b/docs/src/Tutorials/Tutorial_7_setup.md new file mode 100644 index 0000000000..0d93604c83 --- /dev/null +++ b/docs/src/Tutorials/Tutorial_7_setup.md @@ -0,0 +1,2872 @@ +# Tutorial 7: Policy Constraints + +[Interactive Notebook of the tutorial](https://github.com/GenXProject/GenX-Tutorials/blob/main/Tutorials/Tutorial_7_Setup.ipynb) + +As show in previous tutorials, the settings file can be changed to adapt to a number of different systems. In Tutorial 3, we discussed how the setting Time Domain Reduction can reduce the computation time of the solver. Other settings, however, directly impact the values of the solution itself. This tutorial goes over the policy attributes in the settings and shows how implementing certain policies affects the optimal solution in GenX. To demonstrate these effects, we will be using `example_systems/1_three_zones`. + +## Table of Contents +* [No Policy Constraints](#NoPolicies) +* [CO2 Cap](#CO2Cap) + * [Mass Cap](#MassCap) + * [Tolerance](#Tolerance) + * [CO2 Slack](#CO2Slack) + * [Load-based Cap](#LoadCap) + * [Generator-based Cap](#GenerateCap) +* [Energy Share Requirement](#ESR) +* [Capacity Reserve Margin](#CRM) +* [Minimum Capacity Requirement](#MCR) +* [All Together](#All) + + +## No Policy Constraints + + +```julia +using JuMP +using HiGHS +using GenX +using CSV +using DataFrames +using Plots +using StatsPlots +``` + + +```julia +case = joinpath("example_systems/1_three_zones") + +genx_settings = GenX.get_settings_path(case, "genx_settings.yml"); +writeoutput_settings = GenX.get_settings_path(case, "output_settings.yml") +setup = GenX.configure_settings(genx_settings,writeoutput_settings) +``` + + Configuring Settings + + + + + + Dict{Any, Any} with 33 entries: + "HydrogenHourlyMatching" => 0 + "NetworkExpansion" => 1 + "TimeDomainReductionFolder" => "TDR_results" + "WriteOutputs" => "full" + "SystemFolder" => "system" + "EnableJuMPStringNames" => 1 + "Trans_Loss_Segments" => 1 + "ModelingtoGenerateAlternativeSlack" => 0.1 + "PoliciesFolder" => "policies" + "MultiStage" => 0 + "ComputeConflicts" => 1 + "OverwriteResults" => 0 + "ModelingToGenerateAlternatives" => 0 + "MaxCapReq" => 0 + "MinCapReq" => 1 + "CO2Cap" => 2 + "WriteShadowPrices" => 1 + "OperationalReserves" => 0 + "ParameterScale" => 1 + "EnergyShareRequirement" => 0 + "PrintModel" => 0 + "TimeDomainReduction" => 1 + "DC_OPF" => 0 + "CapacityReserveMargin" => 0 + "MethodofMorris" => 0 + ⋮ => ⋮ + + + +The settings we'll focus on here are , `CO2Cap`, `EnergyShareRequirement`, `CapacityReserveMargin`, and `MinCapReq`. Each of these ensures that the environmental impact of the model is taken into account, and are therefore referred to as __policy settings__ . For more information on what each one does, see the documentation on [Emission mitigation policies]. + + +```julia +println("MaxCapReq: ", setup["MaxCapReq"]) +println("MinCapReq: ", setup["MinCapReq"]) +println("CO2Cap: ", setup["CO2Cap"]) +println("EnergyShareRequirement: ", setup["EnergyShareRequirement"]) +println("CapacityReserveMargin: ", setup["CapacityReserveMargin"]) +``` + + MaxCapReq: 0 + MinCapReq: 1 + CO2Cap: 2 + EnergyShareRequirement: 0 + CapacityReserveMargin: 0 + + +`1_three_zones` uses `MinCapReq` and `CO2Cap`. For the purpose of this tutorial, we're going to set these back to zero to start. + + +```julia +setup["MinCapReq"] = 0 +setup["CO2Cap"] = 0; +``` + +Now, we'll generate and solve the model using these results: + + +```julia +## Delte Previous TDR Results +if "TDR_results" in cd(readdir,case) + rm(joinpath(case,"TDR_results"), recursive=true) +end + +### Create TDR_Results +TDRpath = joinpath(case, setup["TimeDomainReductionFolder"]) +system_path = joinpath(case, setup["SystemFolder"]) +settings_path = GenX.get_settings_path(case) + +if setup["TimeDomainReduction"] == 1 + GenX.prevent_doubled_timedomainreduction(system_path) + if !GenX.time_domain_reduced_files_exist(TDRpath) + println("Clustering Time Series Data (Grouped)...") + GenX.cluster_inputs(case, settings_path, setup) + else + println("Time Series Data Already Clustered.") + end +end + +``` + + Clustering Time Series Data (Grouped)... + Reading Input CSV Files + Network.csv Successfully Read! + Demand (load) data Successfully Read! + Fuels_data.csv Successfully Read! + + + Thermal.csv Successfully Read. + Vre.csv Successfully Read. + Storage.csv Successfully Read. + Resource_energy_share_requirement.csv Successfully Read. + Resource_capacity_reserve_margin.csv Successfully Read. + Resource_minimum_capacity_requirement.csv Successfully Read. + + + Summary of resources loaded into the model: + ------------------------------------------------------- + Resource type Number of resources + ======================================================= + Thermal 3 + VRE 4 + Storage 3 + ======================================================= + Total number of resources: 10 + ------------------------------------------------------- + Generators_variability.csv Successfully Read! + Validating time basis + CSV Files Successfully Read In From example_systems/1_three_zones + Error: Geography Key 1 is invalid. Select `System' or `Zone'. + + + + + + Dict{String, Any} with 9 entries: + "RMSE" => Dict("ME_NG"=>0.210014, "ME_onshore_wind_z3"=>0.310986, "D… + "OutputDF" => 1848×19 DataFrame + "ColToZoneMap" => Dict("Demand_MW_z3"=>3, "CT_battery_z2"=>2, "MA_natural_ga… + "ClusterObject" => KmeansResult{Matrix{Float64}, Float64, Int64}([-0.734116 2… + "TDRsetup" => Dict{Any, Any}("IterativelyAddPeriods"=>1, "ExtremePeriods… + "Assignments" => [1, 1, 1, 1, 2, 2, 2, 2, 2, 3 … 6, 9, 3, 10, 10, 10, 11,… + "InputDF" => 1680×52 DataFrame + "Weights" => [673.846, 1179.23, 842.308, 673.846, 1010.77, 1347.69, 134… + "Centers" => Any[4, 8, 12, 15, 20, 23, 27, 30, 44, 48, 49] + + + + +```julia +OPTIMIZER = GenX.configure_solver(settings_path,HiGHS.Optimizer); +inputs = GenX.load_inputs(setup, case) +``` + + Reading Input CSV Files + Network.csv Successfully Read! + Demand (load) data Successfully Read! + Fuels_data.csv Successfully Read! + + Summary of resources loaded into the model: + ------------------------------------------------------- + Resource type Number of resources + ======================================================= + Thermal 3 + VRE 4 + Storage 3 + ======================================================= + Total number of resources: 10 + ------------------------------------------------------- + Generators_variability.csv Successfully Read! + Validating time basis + CSV Files Successfully Read In From example_systems/1_three_zones + + + Thermal.csv Successfully Read. + Vre.csv Successfully Read. + Storage.csv Successfully Read. + Resource_energy_share_requirement.csv Successfully Read. + Resource_capacity_reserve_margin.csv Successfully Read. + Resource_minimum_capacity_requirement.csv Successfully Read. + + + + + Dict{Any, Any} with 67 entries: + "Z" => 3 + "LOSS_LINES" => [1, 2] + "STOR_HYDRO_SHORT_DURATION" => Int64[] + "RET_CAP_CHARGE" => Set{Int64}() + "pC_D_Curtail" => [50.0, 45.0, 27.5, 10.0] + "pTrans_Max_Possible" => [5.9, 4.0] + "pNet_Map" => [1.0 -1.0 0.0; 1.0 0.0 -1.0] + "omega" => 4.01099, 4.01099, 4.01099, 4.01099, 4.01099, … + "pMax_Line_Reinforcement" => [2.95, 2.0] + "RET_CAP_ENERGY" => Int64[] + "RESOURCES" => AbstractResource + "COMMIT" => [1, 2, 3] + "pMax_D_Curtail" => [1.0, 0.04, 0.024, 0.003] + "STOR_ALL" => [8, 9, 10] + "THERM_ALL" => [1, 2, 3] + "REP_PERIOD" => 11 + "PWFU_Num_Segments" => 0 + "STOR_LONG_DURATION" => Int64[] + "THERM_COMMIT_PWFU" => Int64[] + "STOR_SYMMETRIC" => [8, 9, 10] + "VRE" => [4, 5, 6, 7] + "RETRO" => Int64[] + "THERM_COMMIT" => [1, 2, 3] + "TRANS_LOSS_SEGS" => 1 + "H" => 168 + ⋮ => ⋮ + + + + +```julia +EP = GenX.generate_model(setup,inputs,OPTIMIZER) +``` + + Discharge Module + Non-served Energy Module + Investment Discharge Module + Unit Commitment Module + Fuel Module + CO2 Module + Investment Transmission Module + Transmission Module + Dispatchable Resources Module + Storage Resources Module + Storage Investment Module + Storage Core Resources Module + Storage Resources with Symmetric Charge/Discharge Capacity Module + Thermal (Unit Commitment) Resources Module + + + + + + A JuMP Model + Minimization problem with: + Variables: 120136 + Objective function type: AffExpr + `AffExpr`-in-`MathOptInterface.EqualTo{Float64}`: 35112 constraints + `AffExpr`-in-`MathOptInterface.GreaterThan{Float64}`: 20331 constraints + `AffExpr`-in-`MathOptInterface.LessThan{Float64}`: 97949 constraints + `VariableRef`-in-`MathOptInterface.EqualTo{Float64}`: 1 constraint + `VariableRef`-in-`MathOptInterface.GreaterThan{Float64}`: 116439 constraints + Model mode: AUTOMATIC + CachingOptimizer state: EMPTY_OPTIMIZER + Solver name: HiGHS + Names registered in the model: FuelCalculationCommit_single, cFuelCalculation_single, cMaxCap, cMaxCapEnergy, cMaxCapEnergyDuration, cMaxFlow_in, cMaxFlow_out, cMaxLineReinforcement, cMaxNSE, cMaxRetCommit, cMaxRetEnergy, cMaxRetNoCommit, cMinCap, cMinCapEnergy, cMinCapEnergyDuration, cNSEPerSeg, cPowerBalance, cSoCBalInterior, cSoCBalStart, cStartFuel_single, cTAuxLimit, cTAuxSum, cTLoss, eAvail_Trans_Cap, eCFix, eCFixEnergy, eCFuelOut, eCFuelStart, eCNSE, eCStart, eCVar_in, eCVar_out, eELOSS, eELOSSByZone, eEmissionsByPlant, eEmissionsByZone, eExistingCap, eExistingCapEnergy, eFuelConsumption, eFuelConsumptionYear, eFuelConsumption_single, eGenerationByThermAll, eGenerationByVRE, eGenerationByZone, eLosses_By_Zone, eNet_Export_Flows, eObj, ePlantCFuelOut, ePlantCFuelStart, ePlantFuel_generation, ePlantFuel_start, ePowerBalance, ePowerBalanceDisp, ePowerBalanceLossesByZone, ePowerBalanceNetExportFlows, ePowerBalanceNse, ePowerBalanceStor, ePowerBalanceThermCommit, eStartFuel, eTotalCFix, eTotalCFixEnergy, eTotalCFuelOut, eTotalCFuelStart, eTotalCNSE, eTotalCNSET, eTotalCNSETS, eTotalCNetworkExp, eTotalCStart, eTotalCStartT, eTotalCVarIn, eTotalCVarInT, eTotalCVarOut, eTotalCVarOutT, eTotalCap, eTotalCapEnergy, eTransMax, eZonalCFuelOut, eZonalCFuelStart, vCAP, vCAPENERGY, vCHARGE, vCOMMIT, vFLOW, vFuel, vNEW_TRANS_CAP, vNSE, vP, vRETCAP, vRETCAPENERGY, vS, vSHUT, vSTART, vStartFuel, vTAUX_NEG, vTAUX_POS, vTLOSS, vZERO + + + + +```julia +GenX.solve_model(EP,setup) +``` + + Running HiGHS 1.6.0: Copyright (c) 2023 HiGHS under MIT licence terms + Presolving model + 118155 rows, 81204 cols, 422835 nonzeros + 110998 rows, 74047 cols, 423349 nonzeros + Presolve : Reductions: rows 110998(-42394); columns 74047(-46089); elements 423349(-47782) + Solving the presolved LP + IPX model has 110998 rows, 74047 columns and 423349 nonzeros + Input + Number of variables: 74047 + Number of free variables: 3696 + Number of constraints: 110998 + Number of equality constraints: 16867 + Number of matrix entries: 423349 + Matrix range: [4e-07, 1e+01] + RHS range: [7e-01, 2e+01] + Objective range: [1e-04, 4e+02] + Bounds range: [2e-03, 2e+01] + Preprocessing + Dualized model: no + Number of dense columns: 15 + Range of scaling factors: [5.00e-01, 1.00e+00] + IPX version 1.0 + Interior Point Solve + Iter P.res D.res P.obj D.obj mu Time + 0 8.62e+00 3.81e+02 3.30336414e+06 -5.31617580e+06 3.30e+03 0s + 1 4.09e+00 1.06e+02 2.34353411e+05 -5.13796175e+06 1.43e+03 0s + 2 3.78e+00 7.03e+01 1.87013341e+05 -1.15199236e+07 1.34e+03 0s + 3 1.33e+00 4.12e+01 -3.76464137e+05 -1.37088411e+07 7.85e+02 1s + Constructing starting basis... + 4 4.13e-01 1.08e+01 2.66640168e+05 -8.48314805e+06 2.43e+02 3s + 5 1.12e-01 5.62e+00 3.71879810e+05 -5.58576107e+06 1.28e+02 4s + 6 7.53e-03 1.62e+00 2.30531116e+05 -1.92720962e+06 3.67e+01 5s + 7 9.27e-04 1.77e-01 1.30486918e+05 -3.83901614e+05 5.66e+00 6s + 8 1.14e-04 4.38e-02 5.27259057e+04 -1.00386376e+05 1.48e+00 7s + 9 1.52e-05 6.88e-03 2.76584248e+04 -2.19746140e+04 3.52e-01 8s + 10 5.20e-06 2.59e-03 1.39025442e+04 -7.44814138e+03 1.35e-01 8s + 11 2.36e-06 1.09e-03 1.02345396e+04 -1.80403130e+03 7.13e-02 10s + 12 1.03e-06 3.59e-04 7.72508848e+03 1.35005473e+03 3.59e-02 12s + 13 6.67e-07 1.53e-04 6.83171406e+03 2.57204744e+03 2.35e-02 15s + 14 5.06e-07 7.64e-05 6.41494456e+03 3.13597410e+03 1.79e-02 18s + 15 3.52e-07 4.64e-05 5.95636098e+03 3.44861286e+03 1.36e-02 21s + 16 1.69e-07 2.49e-05 5.33436713e+03 3.75261594e+03 8.58e-03 24s + 17 1.38e-07 2.05e-05 5.23488752e+03 3.81660239e+03 7.68e-03 26s + 18 1.21e-07 1.95e-05 5.23765885e+03 3.83998603e+03 7.57e-03 28s + 19 7.35e-08 1.60e-05 5.05272742e+03 3.91685812e+03 6.15e-03 30s + 20 6.04e-08 1.49e-05 5.02221768e+03 3.93278446e+03 5.90e-03 33s + 21 2.85e-08 1.13e-05 4.88298181e+03 4.01774654e+03 4.68e-03 35s + 22 2.29e-08 6.29e-06 4.83470832e+03 4.00260600e+03 4.48e-03 37s + 23 1.31e-08 3.63e-06 4.72358228e+03 4.16712858e+03 3.00e-03 38s + 24 9.10e-09 2.96e-06 4.70648899e+03 4.19085995e+03 2.77e-03 40s + 25 4.45e-09 1.89e-06 4.63836996e+03 4.26416898e+03 2.01e-03 41s + 26 3.49e-09 1.84e-06 4.63722238e+03 4.26628187e+03 1.99e-03 42s + 27 2.15e-09 1.23e-06 4.59679916e+03 4.32342389e+03 1.47e-03 43s + 28 1.87e-09 1.07e-06 4.59259771e+03 4.33476451e+03 1.39e-03 46s + 29 1.58e-09 1.05e-06 4.59058841e+03 4.33567604e+03 1.37e-03 48s + 30 1.18e-09 8.27e-07 4.58014487e+03 4.35414040e+03 1.21e-03 49s + 31 7.95e-10 4.22e-07 4.56632970e+03 4.39355672e+03 9.27e-04 51s + 32 4.34e-10 3.79e-07 4.55792749e+03 4.39835388e+03 8.56e-04 51s + 33 2.37e-10 2.16e-07 4.54724964e+03 4.42097386e+03 6.77e-04 52s + 34 2.10e-10 2.06e-07 4.54686537e+03 4.42339252e+03 6.62e-04 53s + 35 7.73e-11 1.26e-07 4.53250796e+03 4.44139125e+03 4.89e-04 54s + 36 1.55e-11 1.06e-07 4.52064756e+03 4.44667475e+03 3.97e-04 55s + 37 1.50e-11 1.03e-07 4.52062467e+03 4.44749533e+03 3.92e-04 55s + 38 9.79e-12 1.00e-07 4.52199331e+03 4.44806728e+03 3.96e-04 56s + 39 8.92e-12 6.93e-08 4.52041735e+03 4.46101031e+03 3.19e-04 56s + 40 7.30e-12 5.17e-08 4.52001352e+03 4.46367984e+03 3.02e-04 57s + 41 4.70e-12 3.05e-08 4.51328477e+03 4.47536516e+03 2.03e-04 57s + 42 3.83e-12 2.70e-08 4.51240381e+03 4.47626904e+03 1.94e-04 58s + 43 2.85e-12 2.63e-08 4.51136761e+03 4.47647929e+03 1.87e-04 59s + 44 2.10e-12 1.31e-08 4.50946086e+03 4.48203628e+03 1.47e-04 59s + 45 1.31e-12 4.15e-09 4.50665353e+03 4.48821996e+03 9.88e-05 60s + 46 9.09e-13 3.31e-09 4.50579733e+03 4.48871202e+03 9.15e-05 61s + 47 9.09e-13 3.26e-09 4.50576718e+03 4.48852664e+03 9.24e-05 61s + 48 7.53e-13 2.25e-09 4.50502226e+03 4.49018328e+03 7.95e-05 62s + 49 7.53e-13 2.21e-09 4.50492035e+03 4.48999997e+03 7.99e-05 62s + 50 5.83e-13 1.52e-09 4.50350728e+03 4.49199895e+03 6.17e-05 63s + 51 1.99e-13 1.32e-09 4.50092854e+03 4.49240806e+03 4.57e-05 63s + 52 1.78e-13 1.23e-09 4.50080001e+03 4.49253227e+03 4.43e-05 64s + 53 9.24e-14 7.11e-10 4.49993325e+03 4.49367022e+03 3.36e-05 64s + 54 7.11e-14 5.59e-10 4.49966942e+03 4.49408670e+03 2.99e-05 65s + 55 7.11e-14 5.54e-10 4.49963721e+03 4.49407202e+03 2.98e-05 65s + 56 7.11e-14 5.25e-10 4.49964358e+03 4.49413082e+03 2.95e-05 66s + 57 4.97e-14 3.86e-10 4.49901737e+03 4.49483758e+03 2.24e-05 66s + 58 4.97e-14 3.77e-10 4.49900976e+03 4.49485417e+03 2.23e-05 66s + 59 4.97e-14 3.61e-10 4.49894867e+03 4.49494477e+03 2.15e-05 67s + 60 2.84e-14 2.16e-10 4.49842279e+03 4.49567956e+03 1.47e-05 67s + 61 2.84e-14 1.25e-10 4.49813093e+03 4.49622420e+03 1.02e-05 67s + 62 2.13e-14 9.05e-11 4.49785493e+03 4.49641576e+03 7.71e-06 68s + 63 2.13e-14 2.46e-11 4.49758346e+03 4.49698685e+03 3.20e-06 68s + 64 2.13e-14 1.77e-11 4.49754878e+03 4.49704995e+03 2.67e-06 68s + 65 2.13e-14 3.18e-12 4.49747123e+03 4.49722789e+03 1.30e-06 69s + 66 2.13e-14 1.36e-12 4.49740334e+03 4.49723907e+03 8.80e-07 69s + 67 2.13e-14 1.36e-12 4.49734663e+03 4.49727135e+03 4.03e-07 70s + 68 2.13e-14 9.66e-13 4.49734376e+03 4.49727731e+03 3.56e-07 70s + 69 2.13e-14 5.68e-13 4.49732633e+03 4.49728530e+03 2.20e-07 70s + 70 2.13e-14 4.55e-13 4.49731802e+03 4.49730841e+03 5.15e-08 71s + 71 2.13e-14 4.83e-13 4.49731683e+03 4.49730985e+03 3.74e-08 71s + 72 2.13e-14 1.92e-12 4.49731281e+03 4.49731141e+03 7.53e-09 72s + 73 2.13e-14 5.35e-12 4.49731227e+03 4.49731210e+03 9.13e-10 72s + 74* 2.13e-14 2.73e-12 4.49731219e+03 4.49731218e+03 7.21e-11 72s + 75* 2.84e-14 7.65e-12 4.49731219e+03 4.49731218e+03 4.94e-12 73s + 76* 3.55e-14 4.81e-12 4.49731219e+03 4.49731219e+03 4.09e-13 73s + 77* 3.55e-14 7.19e-12 4.49731219e+03 4.49731219e+03 2.31e-15 73s + Running crossover as requested + Primal residual before push phase: 1.40e-09 + Dual residual before push phase: 5.05e-10 + Number of dual pushes required: 41031 + Number of primal pushes required: 722 + Summary + Runtime: 73.32s + Status interior point solve: optimal + Status crossover: optimal + objective value: 4.49731219e+03 + interior solution primal residual (abs/rel): 5.48e-11 / 3.35e-12 + interior solution dual residual (abs/rel): 7.19e-12 / 1.79e-14 + interior solution objective gap (abs/rel): -5.22e-10 / -1.16e-13 + basic solution primal infeasibility: 2.78e-17 + basic solution dual infeasibility: 5.41e-16 + Ipx: IPM optimal + Ipx: Crossover optimal + Solving the original LP from the solution after postsolve + Model status : Optimal + IPM iterations: 77 + Crossover iterations: 4712 + Objective value : 4.4973121850e+03 + HiGHS run time : 73.55 + LP solved for primal + + + + + + (A JuMP Model + Minimization problem with: + Variables: 120136 + Objective function type: AffExpr + `AffExpr`-in-`MathOptInterface.EqualTo{Float64}`: 35112 constraints + `AffExpr`-in-`MathOptInterface.GreaterThan{Float64}`: 20331 constraints + `AffExpr`-in-`MathOptInterface.LessThan{Float64}`: 97949 constraints + `VariableRef`-in-`MathOptInterface.EqualTo{Float64}`: 1 constraint + `VariableRef`-in-`MathOptInterface.GreaterThan{Float64}`: 116439 constraints + Model mode: AUTOMATIC + CachingOptimizer state: ATTACHED_OPTIMIZER + Solver name: HiGHS + Names registered in the model: FuelCalculationCommit_single, cFuelCalculation_single, cMaxCap, cMaxCapEnergy, cMaxCapEnergyDuration, cMaxFlow_in, cMaxFlow_out, cMaxLineReinforcement, cMaxNSE, cMaxRetCommit, cMaxRetEnergy, cMaxRetNoCommit, cMinCap, cMinCapEnergy, cMinCapEnergyDuration, cNSEPerSeg, cPowerBalance, cSoCBalInterior, cSoCBalStart, cStartFuel_single, cTAuxLimit, cTAuxSum, cTLoss, eAvail_Trans_Cap, eCFix, eCFixEnergy, eCFuelOut, eCFuelStart, eCNSE, eCStart, eCVar_in, eCVar_out, eELOSS, eELOSSByZone, eEmissionsByPlant, eEmissionsByZone, eExistingCap, eExistingCapEnergy, eFuelConsumption, eFuelConsumptionYear, eFuelConsumption_single, eGenerationByThermAll, eGenerationByVRE, eGenerationByZone, eLosses_By_Zone, eNet_Export_Flows, eObj, ePlantCFuelOut, ePlantCFuelStart, ePlantFuel_generation, ePlantFuel_start, ePowerBalance, ePowerBalanceDisp, ePowerBalanceLossesByZone, ePowerBalanceNetExportFlows, ePowerBalanceNse, ePowerBalanceStor, ePowerBalanceThermCommit, eStartFuel, eTotalCFix, eTotalCFixEnergy, eTotalCFuelOut, eTotalCFuelStart, eTotalCNSE, eTotalCNSET, eTotalCNSETS, eTotalCNetworkExp, eTotalCStart, eTotalCStartT, eTotalCVarIn, eTotalCVarInT, eTotalCVarOut, eTotalCVarOutT, eTotalCap, eTotalCapEnergy, eTransMax, eZonalCFuelOut, eZonalCFuelStart, vCAP, vCAPENERGY, vCHARGE, vCOMMIT, vFLOW, vFuel, vNEW_TRANS_CAP, vNSE, vP, vRETCAP, vRETCAPENERGY, vS, vSHUT, vSTART, vStartFuel, vTAUX_NEG, vTAUX_POS, vTLOSS, vZERO, 73.97517013549805) + + + +Using `value.()`, we can see what the total capacity is of the optimized model: + + +```julia +totCap_base = value.(EP[:eTotalCap]) +``` + + + + + 10-element Vector{Float64}: + 10.41532872265646 + 10.085613331810192 + 0.0 + 0.0 + 0.0 + 0.0 + 2.026239619715743 + 0.0 + 0.0 + 0.16552558225070782 + + + +Each element corresponds to the MW value of the node in the grid. In `1_three_zones`, there are ten nodes, each of which are either natural gas, wind, solar, or battery plants. We can see which is which using `RESOURCE_NAMES` in the inputs dictionary: + + +```julia +RT = inputs["RESOURCE_NAMES"]; +DataFrame([RT totCap_base],["Resource","Total Capacity"]) +``` + + + +```@raw html +
10×2 DataFrame
RowResourceTotal Capacity
AnyAny
1MA_natural_gas_combined_cycle10.4153
2CT_natural_gas_combined_cycle10.0856
3ME_natural_gas_combined_cycle0.0
4MA_solar_pv0.0
5CT_onshore_wind0.0
6CT_solar_pv0.0
7ME_onshore_wind2.02624
8MA_battery0.0
9CT_battery0.0
10ME_battery0.165526
+``` + + +To visualize the impact of the emmissions policies, let's group the nodes together by type and plot the data. + + +```julia +# Group by plant type +totCapB = [totCap_base[1] + totCap_base[2] + totCap_base[3], totCap_base[4] + totCap_base[6], + totCap_base[5] + totCap_base[7], totCap_base[8] + totCap_base[9] + totCap_base[10]] +totCapB = reshape(totCapB,(:,1)) # Convert to matrix +``` + + + + + 4×1 Matrix{Float64}: + 20.500942054466652 + 0.0 + 2.026239619715743 + 0.16552558225070782 + + + + +```julia +colors=[:silver :yellow :deepskyblue :violetred3] + +G1 = groupedbar(transpose(totCapB), bar_position = :stack, bar_width=0.1,size=(400,450), + labels=["Natural Gas" "Solar" "Wind" "Battery"], + title="No Emissions Policies \n Obj Val: $(round(objective_value(EP),digits=6))",xticks=[ ],ylabel="GW",color=colors) +``` + +![svg](./files/t7_1p_none.svg) + +As you can see, with no limit on emissions, GenX goes straight to using natural gas. Let's try changing the settings to enforce emissions constraints. + +## CO2 Cap + +The setting `CO2Cap` specifies if the model should have a constraint on CO$_2$ emmissions, and, if so, what that constraint should look like. There are three types, mass, load-based, and generator-based. + +### Mass Cap + +The first type of constraint, done by setting `CO2Cap` to "1", is a mass based constraint, which simply puts a limit on the total tons of CO$_2$ able to be produced per megawatt of electricty. + + +```julia +setup["CO2Cap"] = 1; +``` + + +```julia +inputs = GenX.load_inputs(setup, case) +EP2 = GenX.generate_model(setup,inputs,OPTIMIZER) +GenX.solve_model(EP2,setup) +``` + + Reading Input CSV Files + Network.csv Successfully Read! + Demand (load) data Successfully Read! + Fuels_data.csv Successfully Read! + + Summary of resources loaded into the model: + ------------------------------------------------------- + Resource type Number of resources + ======================================================= + Thermal 3 + VRE 4 + Storage 3 + ======================================================= + Total number of resources: 10 + ------------------------------------------------------- + Generators_variability.csv Successfully Read! + Validating time basis + CO2_cap.csv Successfully Read! + CSV Files Successfully Read In From example_systems/1_three_zones + Discharge Module + + + Thermal.csv Successfully Read. + Vre.csv Successfully Read. + Storage.csv Successfully Read. + Resource_energy_share_requirement.csv Successfully Read. + Resource_capacity_reserve_margin.csv Successfully Read. + Resource_minimum_capacity_requirement.csv Successfully Read. + + Non-served Energy Module + Investment Discharge Module + Unit Commitment Module + Fuel Module + CO2 Module + Investment Transmission Module + Transmission Module + Dispatchable Resources Module + Storage Resources Module + Storage Investment Module + Storage Core Resources Module + Storage Resources with Symmetric Charge/Discharge Capacity Module + Thermal (Unit Commitment) Resources Module + CO2 Policies Module + Running HiGHS 1.6.0: Copyright (c) 2023 HiGHS under MIT licence terms + Presolving model + 118158 rows, 81204 cols, 433923 nonzeros + 110739 rows, 73785 cols, 435465 nonzeros + Presolve : Reductions: rows 110739(-42656); columns 73785(-46354); elements 435465(-46757) + Solving the presolved LP + IPX model has 110739 rows, 73785 columns and 435465 nonzeros + Input + Number of variables: 73785 + Number of free variables: 3696 + Number of constraints: 110739 + Number of equality constraints: 16605 + Number of matrix entries: 435465 + Matrix range: [4e-07, 1e+01] + RHS range: [7e-01, 2e+01] + Objective range: [1e-04, 4e+02] + Bounds range: [2e-03, 2e+01] + Preprocessing + Dualized model: no + Number of dense columns: 15 + Range of scaling factors: [5.00e-01, 4.00e+00] + IPX version 1.0 + Interior Point Solve + Iter P.res D.res P.obj D.obj mu Time + 0 1.16e+01 3.76e+02 3.33411424e+06 -5.16532632e+06 4.37e+03 0s + 1 6.12e+00 1.19e+02 2.67580772e+05 -5.49991870e+06 2.15e+03 0s + 2 5.78e+00 8.43e+01 2.63971420e+05 -1.26660532e+07 2.19e+03 1s + 3 2.60e+00 5.19e+01 -6.33191198e+04 -1.58670064e+07 1.38e+03 1s + Constructing starting basis... + 4 7.34e-01 1.69e+01 8.22606550e+05 -1.15977191e+07 4.66e+02 3s + 5 3.33e-01 5.93e+00 6.52520930e+05 -5.60493380e+06 1.92e+02 5s + 6 1.40e-01 1.86e+00 4.30003244e+05 -2.61740021e+06 7.56e+01 6s + 7 7.48e-02 6.92e-01 3.17908008e+05 -1.47858711e+06 3.76e+01 7s + 8 1.34e-02 2.13e-01 1.58499195e+05 -7.36966970e+05 1.06e+01 8s + 9 8.58e-03 1.84e-01 1.38726156e+05 -6.77517443e+05 8.88e+00 9s + 10 1.85e-03 1.25e-01 1.06997055e+05 -5.46895734e+05 6.03e+00 10s + 11 9.24e-04 6.46e-02 8.86553337e+04 -3.44818602e+05 3.62e+00 11s + 12 4.63e-04 4.30e-02 7.38073046e+04 -2.58650422e+05 2.62e+00 12s + 13 2.59e-04 1.76e-02 5.82580398e+04 -1.15494132e+05 1.25e+00 13s + 14 1.82e-04 1.25e-02 5.26785356e+04 -8.56355514e+04 9.61e-01 14s + 15 8.88e-05 9.97e-03 4.58204676e+04 -6.94861851e+04 7.82e-01 15s + 16 4.53e-05 6.30e-03 3.78921045e+04 -3.64421549e+04 4.86e-01 16s + 17 2.06e-05 3.52e-03 3.37528536e+04 -1.59069943e+04 3.10e-01 17s + 18 1.03e-05 1.99e-03 3.04164644e+04 -2.07682393e+03 1.95e-01 18s + 19 8.47e-06 1.65e-03 3.00013657e+04 4.67701651e+02 1.75e-01 19s + 20 7.43e-06 1.51e-03 2.96873596e+04 1.69164986e+03 1.65e-01 20s + 21 4.34e-06 1.21e-03 2.86080014e+04 4.49654207e+03 1.41e-01 21s + 22 3.15e-06 9.70e-04 2.84959673e+04 6.35826288e+03 1.28e-01 21s + 23 8.47e-07 3.92e-04 2.73823203e+04 1.23933845e+04 8.37e-02 22s + 24 2.40e-07 1.04e-04 2.61231516e+04 1.76443726e+04 4.63e-02 23s + 25 2.22e-07 9.36e-05 2.60148183e+04 1.78437138e+04 4.45e-02 24s + 26 1.62e-07 7.29e-05 2.55552259e+04 1.84206144e+04 3.88e-02 24s + 27 1.22e-07 5.52e-05 2.50949238e+04 1.91000865e+04 3.25e-02 25s + 28 1.09e-07 4.85e-05 2.50021728e+04 1.92517935e+04 3.12e-02 26s + 29 8.94e-08 4.45e-05 2.47907107e+04 1.93334302e+04 2.96e-02 27s + 30 8.24e-08 4.34e-05 2.47681701e+04 1.93720252e+04 2.92e-02 28s + 31 7.16e-08 2.50e-05 2.46362518e+04 1.99291856e+04 2.54e-02 29s + 32 4.66e-08 2.01e-05 2.42437587e+04 2.01152325e+04 2.23e-02 29s + 33 4.19e-08 1.97e-05 2.42647574e+04 2.01301840e+04 2.23e-02 30s + 34 2.79e-08 1.90e-05 2.40142742e+04 2.01663344e+04 2.08e-02 31s + 35 2.32e-08 6.69e-06 2.39399116e+04 2.06855599e+04 1.75e-02 31s + 36 1.07e-08 5.30e-06 2.34566702e+04 2.08955297e+04 1.38e-02 32s + 37 9.51e-09 4.35e-06 2.33786345e+04 2.11021969e+04 1.22e-02 33s + 38 8.47e-09 3.86e-06 2.33591312e+04 2.11486293e+04 1.19e-02 34s + 39 4.72e-09 2.75e-06 2.31415700e+04 2.13759124e+04 9.49e-03 35s + 40 4.12e-09 2.60e-06 2.31287674e+04 2.13971288e+04 9.30e-03 35s + 41 4.08e-09 2.30e-06 2.31279677e+04 2.14560762e+04 8.98e-03 36s + 42 2.31e-09 1.19e-06 2.29798114e+04 2.17571173e+04 6.57e-03 37s + 43 2.12e-09 1.08e-06 2.29572577e+04 2.17902511e+04 6.27e-03 37s + 44 1.50e-09 6.92e-07 2.28682893e+04 2.19491402e+04 4.93e-03 38s + 45 1.44e-09 5.32e-07 2.28678426e+04 2.19786865e+04 4.77e-03 39s + 46 7.50e-10 1.52e-07 2.27876378e+04 2.21512825e+04 3.41e-03 39s + 47 6.72e-10 1.34e-07 2.27673512e+04 2.21739594e+04 3.18e-03 40s + 48 4.76e-10 2.75e-08 2.27150227e+04 2.23098580e+04 2.17e-03 41s + 49 3.86e-10 2.55e-08 2.26819898e+04 2.23156051e+04 1.97e-03 41s + 50 3.42e-10 2.05e-08 2.26687731e+04 2.23252871e+04 1.84e-03 42s + 51 3.03e-10 1.42e-08 2.26566767e+04 2.23383080e+04 1.71e-03 42s + 52 1.93e-10 1.06e-08 2.26117685e+04 2.23551537e+04 1.38e-03 43s + 53 1.66e-10 3.63e-09 2.25954131e+04 2.24149312e+04 9.68e-04 44s + 54 8.18e-11 9.07e-10 2.25454653e+04 2.24370312e+04 5.82e-04 44s + 55 5.43e-11 4.51e-10 2.25280247e+04 2.24432375e+04 4.55e-04 46s + 56 5.00e-11 4.05e-10 2.25264013e+04 2.24433424e+04 4.46e-04 46s + 57 4.69e-11 3.79e-10 2.25244405e+04 2.24432461e+04 4.36e-04 47s + 58 4.68e-11 3.45e-10 2.25244112e+04 2.24428231e+04 4.38e-04 47s + 59 4.43e-11 3.07e-10 2.25225216e+04 2.24434057e+04 4.24e-04 48s + 60 4.09e-11 2.55e-11 2.25200762e+04 2.24519301e+04 3.66e-04 48s + 61 3.36e-11 2.18e-11 2.25158099e+04 2.24513558e+04 3.46e-04 49s + 62 2.89e-11 1.00e-11 2.25119474e+04 2.24545183e+04 3.08e-04 49s + 63 1.40e-11 1.82e-12 2.24943596e+04 2.24673979e+04 1.45e-04 50s + 64 7.30e-12 9.09e-13 2.24854412e+04 2.24712286e+04 7.62e-05 50s + 65 6.03e-12 9.09e-13 2.24836491e+04 2.24717155e+04 6.40e-05 51s + 66 3.78e-12 2.27e-13 2.24803516e+04 2.24733198e+04 3.77e-05 51s + 67 1.71e-12 2.27e-13 2.24769782e+04 2.24737620e+04 1.73e-05 52s + 68 3.41e-13 2.27e-13 2.24749307e+04 2.24737943e+04 6.10e-06 52s + 69 2.64e-13 9.09e-13 2.24743978e+04 2.24739891e+04 2.19e-06 53s + 70 1.82e-13 5.65e-13 2.24742311e+04 2.24740852e+04 7.83e-07 53s + 71 2.84e-13 9.09e-13 2.24741715e+04 2.24741245e+04 2.52e-07 54s + 72 8.02e-14 2.21e-12 2.24741476e+04 2.24741372e+04 5.59e-08 54s + 73 1.97e-13 3.76e-12 2.24741425e+04 2.24741405e+04 1.08e-08 55s + 74 2.16e-13 4.30e-12 2.24741412e+04 2.24741409e+04 1.56e-09 55s + 75* 3.92e-13 4.87e-12 2.24741410e+04 2.24741410e+04 1.34e-10 56s + 76* 5.84e-12 3.78e-12 2.24741410e+04 2.24741410e+04 1.00e-11 56s + 77* 3.24e-12 3.33e-12 2.24741410e+04 2.24741410e+04 1.95e-12 56s + Running crossover as requested + Primal residual before push phase: 1.33e-07 + Dual residual before push phase: 8.94e-07 + Number of dual pushes required: 34869 + Number of primal pushes required: 3818 + Summary + Runtime: 56.21s + Status interior point solve: optimal + Status crossover: optimal + objective value: 2.24741410e+04 + interior solution primal residual (abs/rel): 2.42e-10 / 9.32e-12 + interior solution dual residual (abs/rel): 3.61e-09 / 8.97e-12 + interior solution objective gap (abs/rel): 3.00e-07 / 1.33e-11 + basic solution primal infeasibility: 3.53e-13 + basic solution dual infeasibility: 2.56e-14 + Ipx: IPM optimal + Ipx: Crossover optimal + Solving the original LP from the solution after postsolve + Model status : Optimal + IPM iterations: 77 + Crossover iterations: 4765 + Objective value : 2.2474141017e+04 + HiGHS run time : 56.47 + LP solved for primal + + + + + + (A JuMP Model + Minimization problem with: + Variables: 120139 + Objective function type: AffExpr + `AffExpr`-in-`MathOptInterface.EqualTo{Float64}`: 35112 constraints + `AffExpr`-in-`MathOptInterface.GreaterThan{Float64}`: 20331 constraints + `AffExpr`-in-`MathOptInterface.LessThan{Float64}`: 97952 constraints + `VariableRef`-in-`MathOptInterface.EqualTo{Float64}`: 4 constraints + `VariableRef`-in-`MathOptInterface.GreaterThan{Float64}`: 116439 constraints + Model mode: AUTOMATIC + CachingOptimizer state: ATTACHED_OPTIMIZER + Solver name: HiGHS + Names registered in the model: FuelCalculationCommit_single, cCO2Emissions_systemwide, cFuelCalculation_single, cMaxCap, cMaxCapEnergy, cMaxCapEnergyDuration, cMaxFlow_in, cMaxFlow_out, cMaxLineReinforcement, cMaxNSE, cMaxRetCommit, cMaxRetEnergy, cMaxRetNoCommit, cMinCap, cMinCapEnergy, cMinCapEnergyDuration, cNSEPerSeg, cPowerBalance, cSoCBalInterior, cSoCBalStart, cStartFuel_single, cTAuxLimit, cTAuxSum, cTLoss, eAvail_Trans_Cap, eCFix, eCFixEnergy, eCFuelOut, eCFuelStart, eCNSE, eCStart, eCVar_in, eCVar_out, eELOSS, eELOSSByZone, eEmissionsByPlant, eEmissionsByZone, eExistingCap, eExistingCapEnergy, eFuelConsumption, eFuelConsumptionYear, eFuelConsumption_single, eGenerationByThermAll, eGenerationByVRE, eGenerationByZone, eLosses_By_Zone, eNet_Export_Flows, eObj, ePlantCFuelOut, ePlantCFuelStart, ePlantFuel_generation, ePlantFuel_start, ePowerBalance, ePowerBalanceDisp, ePowerBalanceLossesByZone, ePowerBalanceNetExportFlows, ePowerBalanceNse, ePowerBalanceStor, ePowerBalanceThermCommit, eStartFuel, eTotalCFix, eTotalCFixEnergy, eTotalCFuelOut, eTotalCFuelStart, eTotalCNSE, eTotalCNSET, eTotalCNSETS, eTotalCNetworkExp, eTotalCStart, eTotalCStartT, eTotalCVarIn, eTotalCVarInT, eTotalCVarOut, eTotalCVarOutT, eTotalCap, eTotalCapEnergy, eTransMax, eZonalCFuelOut, eZonalCFuelStart, vCAP, vCAPENERGY, vCHARGE, vCO2Cap_slack, vCOMMIT, vFLOW, vFuel, vNEW_TRANS_CAP, vNSE, vP, vRETCAP, vRETCAPENERGY, vS, vSHUT, vSTART, vStartFuel, vTAUX_NEG, vTAUX_POS, vTLOSS, vZERO, 56.83114790916443) + + + + +```julia +totCap2 = value.(EP2[:eTotalCap]) +totCapB2 = [totCap2[1] + totCap2[2] + totCap2[3], totCap2[4] + totCap2[6], + totCap2[5] + totCap2[7], totCap2[8] + totCap2[9] + totCap2[10]] + +DataFrame([RT totCap2],["Resource Type","Total Capacity"]) +``` + + + +```@raw html +
10×2 DataFrame
RowResource TypeTotal Capacity
AnyAny
1MA_natural_gas_combined_cycle0.423658
2CT_natural_gas_combined_cycle0.629033
3ME_natural_gas_combined_cycle0.35463
4MA_solar_pv42.9756
5CT_onshore_wind0.0
6CT_solar_pv67.9858
7ME_onshore_wind7.80683
8MA_battery13.262
9CT_battery29.851
10ME_battery2.62375
+``` + + + +```julia +G2 = groupedbar(transpose(totCapB2), bar_position = :stack, bar_width=0.1,size=(100,450), + labels=["Natural Gas" "Solar" "Wind" "Battery"],legend = false,title="CO2 Mass Cap \n Obj Val: $(round(objective_value(EP2),digits=6))", +xticks=[ ],ylabel="GW",color=colors) +plot(G2,G1,size=(900,450),titlefontsize=8) + +``` +![svg](./files/t7_2p_mass_none.svg) + + + +The model favors solar power now, but natural gas and wind are also used. One thing to note is that the objective value of this system is much higher than it was without emissions constraints. The amount of CO$_2$ allowed is determined by the input file CO2_cap.csv: + + +```julia +CO2Cap = CSV.read(joinpath(case,"policies/CO2_cap.csv"),DataFrame,missingstring="NA") +``` + + + +```@raw html +
3×11 DataFrame
RowColumn1Network_zonesCO_2_Cap_Zone_1CO_2_Cap_Zone_2CO_2_Cap_Zone_3CO_2_Max_tons_MWh_1CO_2_Max_tons_MWh_2CO_2_Max_tons_MWh_3CO_2_Max_Mtons_1CO_2_Max_Mtons_2CO_2_Max_Mtons_3
String3String3Int64Int64Int64Float64Float64Float64Float64Float64Float64
1MAz11000.050.00.00.0180.00.0
2CTz20100.00.050.00.00.0250.0
3MEz30010.00.00.050.00.00.025
+``` + + +#### Tolerance + +Let's try setting the CO$_2$ emissions tolerance to 0 for all nodes: + + +```julia +CO2Cap2 = copy(CO2Cap); # Save old tolerances +``` + + +```julia +CO2Cap2[!,"CO_2_Max_tons_MWh_1"] = [0.0;0.0;0.0]; +CO2Cap2[!,"CO_2_Max_tons_MWh_2"] = [0.0;0.0;0.0]; +CO2Cap2[!,"CO_2_Max_tons_MWh_3"] = [0.0;0.0;0.0]; +CO2Cap2[!,"CO_2_Max_Mtons_1"] = [0.0;0.0;0.0]; +CO2Cap2[!,"CO_2_Max_Mtons_2"] = [0.0;0.0;0.0]; +CO2Cap2[!,"CO_2_Max_Mtons_3"] = [0.0;0.0;0.0]; +``` + + +```julia +CSV.write(joinpath(case,"policies/CO2_cap.csv"),CO2Cap2) +``` + + + + + "example_systems/1_three_zones/policies/CO2_cap.csv" + + + + +```julia +inputs = GenX.load_inputs(setup, case) +EP3 = GenX.generate_model(setup,inputs,OPTIMIZER) +GenX.solve_model(EP3,setup) +``` + + Reading Input CSV Files + Network.csv Successfully Read! + Demand (load) data Successfully Read! + Fuels_data.csv Successfully Read! + + Summary of resources loaded into the model: + ------------------------------------------------------- + Resource type Number of resources + ======================================================= + Thermal 3 + VRE 4 + Storage 3 + ======================================================= + Total number of resources: 10 + ------------------------------------------------------- + Generators_variability.csv Successfully Read! + Validating time basis + CO2_cap.csv Successfully Read! + CSV Files Successfully Read In From example_systems/1_three_zones + Discharge Module + + + Thermal.csv Successfully Read. + Vre.csv Successfully Read. + Storage.csv Successfully Read. + Resource_energy_share_requirement.csv Successfully Read. + Resource_capacity_reserve_margin.csv Successfully Read. + Resource_minimum_capacity_requirement.csv Successfully Read. + + Non-served Energy Module + Investment Discharge Module + Unit Commitment Module + Fuel Module + CO2 Module + Investment Transmission Module + Transmission Module + Dispatchable Resources Module + Storage Resources Module + Storage Investment Module + Storage Core Resources Module + Storage Resources with Symmetric Charge/Discharge Capacity Module + Thermal (Unit Commitment) Resources Module + CO2 Policies Module + Running HiGHS 1.6.0: Copyright (c) 2023 HiGHS under MIT licence terms + Presolving model + 62715 rows, 59025 cols, 206619 nonzeros + 55750 rows, 52060 cols, 206345 nonzeros + Presolve : Reductions: rows 55750(-97645); columns 52060(-68079); elements 206345(-275877) + Solving the presolved LP + IPX model has 55750 rows, 52060 columns and 206345 nonzeros + Input + Number of variables: 52060 + Number of free variables: 3696 + Number of constraints: 55750 + Number of equality constraints: 11515 + Number of matrix entries: 206345 + Matrix range: [4e-07, 1e+01] + RHS range: [7e-01, 2e+01] + Objective range: [1e-04, 4e+02] + Bounds range: [2e-03, 2e+01] + Preprocessing + Dualized model: no + Number of dense columns: 12 + Range of scaling factors: [5.00e-01, 1.00e+00] + IPX version 1.0 + Interior Point Solve + Iter P.res D.res P.obj D.obj mu Time + 0 8.66e+00 3.74e+02 3.32626622e+06 -5.13181178e+06 3.28e+03 0s + 1 4.14e+00 1.16e+02 8.23203603e+05 -4.36728593e+06 1.47e+03 0s + 2 3.82e+00 7.84e+01 7.99936610e+05 -9.74711636e+06 1.47e+03 0s + 3 2.29e+00 4.73e+01 6.28103564e+05 -1.28672691e+07 1.11e+03 0s + Constructing starting basis... + 4 3.38e-01 1.80e+01 1.09797314e+06 -8.08041855e+06 2.88e+02 1s + 5 1.83e-01 6.66e+00 7.53792907e+05 -4.07570830e+06 1.33e+02 2s + 6 8.56e-02 2.91e+00 4.53047274e+05 -2.12656299e+06 6.27e+01 2s + 7 4.32e-02 1.06e+00 2.95761273e+05 -9.82158558e+05 2.74e+01 3s + 8 2.45e-02 4.65e-01 2.07805891e+05 -5.33311956e+05 1.42e+01 3s + 9 1.41e-02 2.62e-01 1.50001050e+05 -3.47096559e+05 8.53e+00 3s + 10 1.09e-02 2.01e-01 1.34054733e+05 -3.01418233e+05 7.08e+00 3s + 11 3.02e-03 1.70e-01 9.84009565e+04 -2.78142992e+05 5.84e+00 4s + 12 6.80e-04 1.29e-01 8.22624601e+04 -2.40603504e+05 4.88e+00 4s + 13 5.19e-04 8.27e-02 7.53145221e+04 -1.68983726e+05 3.45e+00 4s + 14 2.11e-04 5.30e-02 6.28457097e+04 -1.25280563e+05 2.49e+00 4s + 15 8.42e-05 2.34e-02 5.29276919e+04 -7.04624346e+04 1.44e+00 5s + 16 4.88e-05 1.67e-02 4.48734610e+04 -4.78632217e+04 1.05e+00 5s + 17 3.17e-05 1.36e-02 4.18106416e+04 -3.79473404e+04 8.85e-01 5s + 18 1.54e-05 7.88e-03 3.47578913e+04 -1.29683476e+04 5.12e-01 6s + 19 5.88e-06 3.32e-03 3.06596888e+04 3.42643300e+03 2.74e-01 6s + 20 3.16e-06 1.13e-03 2.91816414e+04 1.11818829e+04 1.69e-01 6s + 21 2.01e-06 7.37e-04 2.80927086e+04 1.35130431e+04 1.35e-01 7s + 22 1.72e-06 6.41e-04 2.77751797e+04 1.42624417e+04 1.24e-01 7s + 23 1.64e-06 5.49e-04 2.77386064e+04 1.47199954e+04 1.19e-01 7s + 24 7.12e-07 4.66e-04 2.64331519e+04 1.56194992e+04 9.88e-02 8s + 25 2.77e-07 2.78e-04 2.60769883e+04 1.74334827e+04 7.80e-02 8s + 26 2.04e-07 1.45e-04 2.59822889e+04 1.87097084e+04 6.48e-02 8s + 27 7.83e-08 8.86e-05 2.50321932e+04 2.02590181e+04 4.24e-02 9s + 28 6.65e-08 7.45e-05 2.50172059e+04 2.04898905e+04 4.01e-02 9s + 29 5.27e-08 5.73e-05 2.49264089e+04 2.09172601e+04 3.55e-02 9s + 30 3.63e-08 4.90e-05 2.48125703e+04 2.11310315e+04 3.25e-02 10s + 31 2.24e-08 2.37e-05 2.46270269e+04 2.20176436e+04 2.29e-02 10s + 32 1.61e-08 1.80e-05 2.44783423e+04 2.23689799e+04 1.85e-02 10s + 33 1.48e-08 1.67e-05 2.44673758e+04 2.24042775e+04 1.81e-02 11s + 34 1.34e-08 1.10e-05 2.44514558e+04 2.26133491e+04 1.61e-02 11s + 35 5.16e-09 8.66e-06 2.42796001e+04 2.27599671e+04 1.33e-02 11s + 36 3.11e-09 3.47e-06 2.42120824e+04 2.31931323e+04 8.91e-03 11s + 37 5.68e-14 2.68e-06 2.40481768e+04 2.32862443e+04 6.66e-03 12s + 38 5.68e-14 1.15e-06 2.39818085e+04 2.36142391e+04 3.21e-03 12s + 39 5.68e-14 1.13e-06 2.39814115e+04 2.36145091e+04 3.21e-03 12s + 40 5.68e-14 9.70e-07 2.39782559e+04 2.36292988e+04 3.05e-03 13s + 41 6.39e-14 9.03e-07 2.39740539e+04 2.36364177e+04 2.95e-03 13s + 42 8.53e-14 8.87e-07 2.39736934e+04 2.36389906e+04 2.92e-03 13s + 43 8.53e-14 4.51e-07 2.39668070e+04 2.37015134e+04 2.32e-03 14s + 44 5.68e-14 3.63e-07 2.39530038e+04 2.37286287e+04 1.96e-03 14s + 45 6.39e-14 2.62e-07 2.39399533e+04 2.37645087e+04 1.53e-03 15s + 46 7.11e-14 2.35e-07 2.39403195e+04 2.37681261e+04 1.50e-03 15s + 47 5.68e-14 1.07e-07 2.39354909e+04 2.38220443e+04 9.89e-04 15s + 48 5.83e-14 8.32e-08 2.39319570e+04 2.38298216e+04 8.91e-04 16s + 49 6.34e-14 8.23e-08 2.39320866e+04 2.38307615e+04 8.84e-04 16s + 50 5.68e-14 2.73e-08 2.39222759e+04 2.38676200e+04 4.76e-04 16s + 51 5.68e-14 1.81e-08 2.39090601e+04 2.38790981e+04 2.61e-04 17s + 52 5.68e-14 2.13e-09 2.39087205e+04 2.38918872e+04 1.47e-04 17s + 53 5.68e-14 6.69e-10 2.39073686e+04 2.38988570e+04 7.42e-05 17s + 54 5.68e-14 1.71e-10 2.39057985e+04 2.39011287e+04 4.07e-05 17s + 55 5.68e-14 4.37e-11 2.39047907e+04 2.39026178e+04 1.89e-05 18s + 56 5.68e-14 2.50e-11 2.39045148e+04 2.39028929e+04 1.41e-05 18s + 57 5.68e-14 1.27e-11 2.39040896e+04 2.39032417e+04 7.39e-06 18s + 58 5.68e-14 2.27e-12 2.39039185e+04 2.39036010e+04 2.77e-06 18s + 59 5.68e-14 4.55e-13 2.39038268e+04 2.39036756e+04 1.32e-06 18s + 60 5.68e-14 9.09e-13 2.39037810e+04 2.39037207e+04 5.26e-07 18s + 61 5.68e-14 2.27e-13 2.39037547e+04 2.39037335e+04 1.85e-07 19s + 62 5.68e-14 2.27e-13 2.39037481e+04 2.39037351e+04 1.13e-07 19s + 63 5.68e-14 4.55e-13 2.39037407e+04 2.39037385e+04 1.93e-08 19s + 64 5.68e-14 1.14e-13 2.39037395e+04 2.39037392e+04 2.44e-09 19s + 65* 5.68e-14 2.27e-13 2.39037393e+04 2.39037393e+04 1.93e-10 19s + 66* 8.53e-14 2.27e-13 2.39037393e+04 2.39037393e+04 1.22e-11 19s + 67* 5.68e-14 9.09e-13 2.39037393e+04 2.39037393e+04 1.62e-12 19s + Running crossover as requested + Primal residual before push phase: 7.05e-08 + Dual residual before push phase: 9.91e-07 + Number of dual pushes required: 2532 + Number of primal pushes required: 3866 + Summary + Runtime: 19.36s + Status interior point solve: optimal + Status crossover: optimal + objective value: 2.39037393e+04 + interior solution primal residual (abs/rel): 2.26e-10 / 1.38e-11 + interior solution dual residual (abs/rel): 2.64e-09 / 6.57e-12 + interior solution objective gap (abs/rel): 2.04e-07 / 8.51e-12 + basic solution primal infeasibility: 4.44e-15 + basic solution dual infeasibility: 2.08e-15 + Ipx: IPM optimal + Ipx: Crossover optimal + Solving the original LP from the solution after postsolve + Model status : Optimal + IPM iterations: 67 + Crossover iterations: 1428 + Objective value : 2.3903739324e+04 + HiGHS run time : 19.55 + LP solved for primal + + + + + + (A JuMP Model + Minimization problem with: + Variables: 120139 + Objective function type: AffExpr + `AffExpr`-in-`MathOptInterface.EqualTo{Float64}`: 35112 constraints + `AffExpr`-in-`MathOptInterface.GreaterThan{Float64}`: 20331 constraints + `AffExpr`-in-`MathOptInterface.LessThan{Float64}`: 97952 constraints + `VariableRef`-in-`MathOptInterface.EqualTo{Float64}`: 4 constraints + `VariableRef`-in-`MathOptInterface.GreaterThan{Float64}`: 116439 constraints + Model mode: AUTOMATIC + CachingOptimizer state: ATTACHED_OPTIMIZER + Solver name: HiGHS + Names registered in the model: FuelCalculationCommit_single, cCO2Emissions_systemwide, cFuelCalculation_single, cMaxCap, cMaxCapEnergy, cMaxCapEnergyDuration, cMaxFlow_in, cMaxFlow_out, cMaxLineReinforcement, cMaxNSE, cMaxRetCommit, cMaxRetEnergy, cMaxRetNoCommit, cMinCap, cMinCapEnergy, cMinCapEnergyDuration, cNSEPerSeg, cPowerBalance, cSoCBalInterior, cSoCBalStart, cStartFuel_single, cTAuxLimit, cTAuxSum, cTLoss, eAvail_Trans_Cap, eCFix, eCFixEnergy, eCFuelOut, eCFuelStart, eCNSE, eCStart, eCVar_in, eCVar_out, eELOSS, eELOSSByZone, eEmissionsByPlant, eEmissionsByZone, eExistingCap, eExistingCapEnergy, eFuelConsumption, eFuelConsumptionYear, eFuelConsumption_single, eGenerationByThermAll, eGenerationByVRE, eGenerationByZone, eLosses_By_Zone, eNet_Export_Flows, eObj, ePlantCFuelOut, ePlantCFuelStart, ePlantFuel_generation, ePlantFuel_start, ePowerBalance, ePowerBalanceDisp, ePowerBalanceLossesByZone, ePowerBalanceNetExportFlows, ePowerBalanceNse, ePowerBalanceStor, ePowerBalanceThermCommit, eStartFuel, eTotalCFix, eTotalCFixEnergy, eTotalCFuelOut, eTotalCFuelStart, eTotalCNSE, eTotalCNSET, eTotalCNSETS, eTotalCNetworkExp, eTotalCStart, eTotalCStartT, eTotalCVarIn, eTotalCVarInT, eTotalCVarOut, eTotalCVarOutT, eTotalCap, eTotalCapEnergy, eTransMax, eZonalCFuelOut, eZonalCFuelStart, vCAP, vCAPENERGY, vCHARGE, vCO2Cap_slack, vCOMMIT, vFLOW, vFuel, vNEW_TRANS_CAP, vNSE, vP, vRETCAP, vRETCAPENERGY, vS, vSHUT, vSTART, vStartFuel, vTAUX_NEG, vTAUX_POS, vTLOSS, vZERO, 19.870718002319336) + + + + +```julia +totCap3 = value.(EP3[:eTotalCap]) + +totCapB3 = [totCap3[1] + totCap3[2] + totCap3[3], totCap3[4] + totCap3[6], + totCap3[5] + totCap3[7], totCap3[8] + totCap3[9] + totCap3[10]] + +println(DataFrame([RT totCap3],["Resource Type","Total Capacity"])) +println(" ") + +println("Objective Value: ", objective_value(EP3)) + +G3 = groupedbar(transpose(totCapB3), bar_position = :stack, bar_width=0.1,size=(400,450), xticks=[ ],ylabel="GW", + labels=["Natural Gas" "Solar" "Wind" "Battery"],color=colors, + title="CO2 Mass Cap, Zero Tolerance \n Obj Val: $(round(objective_value(EP3),digits=6))") + +plot(G3,G2,size=(800,450),titlefontsize=8) +``` + + 10×2 DataFrame + Row│Resource Type Total Capacity + │Any Any + ─────┼─────────────────────────────────────────────── + 1 │ MA_natural_gas_combined_cycle 0.0 + 2 │ CT_natural_gas_combined_cycle 0.0 + 3 │ ME_natural_gas_combined_cycle 0.0 + 4 │ MA_solar_pv 44.2331 + 5 │ CT_onshore_wind 0.0 + 6 │ CT_solar_pv 71.8741 + 7 │ ME_onshore_wind 5.55301 + 8 │ MA_battery 15.1583 + 9 │ CT_battery 30.3461 + 10 │ ME_battery 2.16509 + + Objective Value: 23903.739324217397 + + +![svg](./files/t7_2p_mass_zero.svg) + + +As you can see, the use of natural gas has been eliminated compeltely. Note that the objective value increases here as well as renewable energy tends to cost more than natural gas. + +#### CO2 Slack + +Another thing we can do is, instead of demanding that the model 100% meet the CO$_2$ cap, we can add a penalty for if it violates the cap. This lets the system allow some CO$_2$ emmissions if it's determined the cost of the grid with some emmissions is low enough that it will offset the cost from the penalty variable. GenX will automatically incorporate this feature if a file by the name "CO2_cap_slack.csv" is in the policies folder of the directory. For more information on other types of policy slack variables in GenX, see the documentation on [Policy Slack Variables]. + +Here, the CO$_2$ slack cap models a [carbon tax](https://en.wikipedia.org/wiki/Carbon_tax#:~:text=A%20carbon%20tax%20is%20a,like%20more%20severe%20weather%20events.of) of \$250 per ton of emissions. + + +```julia +CO2Cap_slack = DataFrame(["CO_2_Cap_Zone_1" 250; "CO_2_Cap_Zone_2" 250; "CO_2_Cap_Zone_2" 250],["CO2_Cap_Constraint","PriceCap"]) +``` + + + +```@raw html +
3×2 DataFrame
RowCO2_Cap_ConstraintPriceCap
AnyAny
1CO_2_Cap_Zone_1250
2CO_2_Cap_Zone_2250
3CO_2_Cap_Zone_2250
+``` + + + +```julia +CSV.write(joinpath(case,"policies/CO2_cap_slack.csv"),CO2Cap_slack) +``` + + + + + "example_systems/1_three_zones/policies/CO2_cap_slack.csv" + + + +And run it again, + + +```julia +inputs = GenX.load_inputs(setup, case) +EP4 = GenX.generate_model(setup,inputs,OPTIMIZER) +GenX.solve_model(EP4,setup) +``` + + Reading Input CSV Files + Network.csv Successfully Read! + Demand (load) data Successfully Read! + Fuels_data.csv Successfully Read! + + Summary of resources loaded into the model: + ------------------------------------------------------- + Resource type Number of resources + ======================================================= + Thermal 3 + VRE 4 + Storage 3 + ======================================================= + Total number of resources: 10 + ------------------------------------------------------- + Generators_variability.csv Successfully Read! + Validating time basis + CO2_cap.csv Successfully Read! + CSV Files Successfully Read In From example_systems/1_three_zones + Discharge Module + Non-served Energy Module + + + Thermal.csv Successfully Read. + Vre.csv Successfully Read. + Storage.csv Successfully Read. + Resource_energy_share_requirement.csv Successfully Read. + Resource_capacity_reserve_margin.csv Successfully Read. + Resource_minimum_capacity_requirement.csv Successfully Read. + + Investment Discharge Module + Unit Commitment Module + Fuel Module + CO2 Module + Investment Transmission Module + Transmission Module + Dispatchable Resources Module + Storage Resources Module + Storage Investment Module + Storage Core Resources Module + Storage Resources with Symmetric Charge/Discharge Capacity Module + Thermal (Unit Commitment) Resources Module + CO2 Policies Module + Running HiGHS 1.6.0: Copyright (c) 2023 HiGHS under MIT licence terms + Presolving model + 118155 rows, 81204 cols, 422835 nonzeros + 110998 rows, 74047 cols, 423349 nonzeros + Presolve : Reductions: rows 110998(-42397); columns 74047(-46092); elements 423349(-58873) + Solving the presolved LP + IPX model has 110998 rows, 74047 columns and 423349 nonzeros + Input + Number of variables: 74047 + Number of free variables: 3696 + Number of constraints: 110998 + Number of equality constraints: 16867 + Number of matrix entries: 423349 + Matrix range: [4e-07, 1e+01] + RHS range: [7e-01, 2e+01] + Objective range: [1e-04, 4e+02] + Bounds range: [2e-03, 2e+01] + Preprocessing + Dualized model: no + Number of dense columns: 15 + Range of scaling factors: [5.00e-01, 4.00e+00] + IPX version 1.0 + Interior Point Solve + Iter P.res D.res P.obj D.obj mu Time + 0 8.63e+00 3.73e+02 3.32376039e+06 -5.09189122e+06 3.23e+03 0s + 1 4.16e+00 1.08e+02 3.13266750e+05 -4.82993364e+06 1.41e+03 0s + 2 3.81e+00 7.25e+01 2.97980565e+05 -1.07780002e+07 1.30e+03 0s + 3 1.46e+00 3.93e+01 -1.26079071e+05 -1.28105929e+07 7.47e+02 1s + Constructing starting basis... + 4 3.74e-01 1.45e+01 3.73463343e+05 -8.41202259e+06 2.69e+02 3s + 5 2.49e-01 2.56e+00 3.78613518e+05 -3.12923412e+06 7.93e+01 5s + 6 1.75e-02 1.04e+00 2.12473288e+05 -1.52758882e+06 2.44e+01 6s + 7 9.80e-04 2.35e-01 1.33331872e+05 -4.99760720e+05 6.87e+00 7s + 8 2.77e-04 7.80e-02 7.82710921e+04 -1.61510326e+05 2.34e+00 9s + 9 1.09e-04 2.70e-02 5.47955571e+04 -6.12119167e+04 9.57e-01 10s + 10 3.77e-05 7.08e-03 4.55523212e+04 -2.90274240e+04 4.91e-01 11s + 11 1.64e-05 2.18e-03 3.11621436e+04 -7.43748662e+03 2.29e-01 12s + 12 7.39e-06 8.70e-04 2.26704941e+04 3.11900828e+02 1.27e-01 14s + 13 3.59e-06 2.72e-04 1.86489763e+04 4.05425979e+03 8.00e-02 16s + 14 1.90e-06 1.28e-04 1.56168633e+04 6.05326027e+03 5.19e-02 17s + 15 1.47e-06 6.25e-05 1.47851456e+04 7.20102502e+03 4.10e-02 20s + 16 1.02e-06 4.01e-05 1.38005552e+04 7.70711780e+03 3.28e-02 23s + 17 5.81e-07 1.81e-05 1.24913828e+04 8.58555743e+03 2.10e-02 26s + 18 3.56e-07 1.37e-05 1.18351621e+04 8.78570877e+03 1.64e-02 31s + 19 2.30e-07 1.13e-05 1.14288935e+04 8.93097599e+03 1.34e-02 33s + 20 1.42e-07 9.21e-06 1.12192893e+04 9.03497649e+03 1.17e-02 36s + 21 1.04e-07 5.91e-06 1.11251312e+04 9.20617340e+03 1.03e-02 37s + 22 6.54e-08 5.00e-06 1.09266006e+04 9.30098656e+03 8.73e-03 38s + 23 3.83e-08 3.59e-06 1.07816598e+04 9.45321722e+03 7.13e-03 40s + 24 3.66e-08 2.80e-06 1.07744564e+04 9.52604230e+03 6.70e-03 41s + 25 1.07e-08 2.21e-06 1.06144079e+04 9.59738956e+03 5.46e-03 43s + 26 5.77e-09 1.68e-06 1.05031968e+04 9.71311617e+03 4.24e-03 44s + 27 4.09e-09 1.48e-06 1.04791745e+04 9.74918379e+03 3.92e-03 45s + 28 2.28e-09 1.14e-06 1.04271389e+04 9.82526444e+03 3.23e-03 45s + 29 1.81e-09 9.51e-07 1.04093731e+04 9.87287016e+03 2.88e-03 46s + 30 1.59e-09 7.09e-07 1.04047419e+04 9.92089105e+03 2.59e-03 47s + 31 1.03e-09 3.29e-07 1.03752597e+04 1.00341461e+04 1.83e-03 47s + 32 6.21e-10 3.01e-07 1.03644649e+04 1.00391050e+04 1.74e-03 48s + 33 3.74e-10 2.57e-07 1.03380266e+04 1.00569556e+04 1.51e-03 49s + 34 1.20e-10 1.65e-07 1.03095303e+04 1.00951029e+04 1.15e-03 49s + 35 8.55e-11 1.39e-07 1.03018149e+04 1.01086583e+04 1.04e-03 50s + 36 5.07e-11 9.62e-08 1.02845256e+04 1.01405678e+04 7.72e-04 50s + 37 4.04e-11 8.97e-08 1.02823253e+04 1.01443584e+04 7.39e-04 51s + 38 2.54e-11 5.51e-08 1.02817374e+04 1.01594329e+04 6.55e-04 51s + 39 1.73e-11 3.65e-08 1.02723681e+04 1.01802747e+04 4.93e-04 52s + 40 1.50e-11 3.37e-08 1.02704612e+04 1.01830824e+04 4.68e-04 52s + 41 2.61e-12 2.79e-08 1.02584016e+04 1.01890289e+04 3.72e-04 52s + 42 2.42e-12 2.59e-08 1.02587461e+04 1.01906586e+04 3.65e-04 53s + 43 2.04e-12 2.16e-08 1.02588163e+04 1.01942759e+04 3.46e-04 53s + 44 1.69e-12 2.05e-08 1.02582342e+04 1.01954859e+04 3.36e-04 54s + 45 1.04e-12 1.61e-08 1.02537386e+04 1.02028401e+04 2.73e-04 54s + 46 9.38e-13 1.33e-08 1.02531945e+04 1.02071474e+04 2.47e-04 55s + 47 3.94e-13 7.40e-09 1.02494139e+04 1.02173046e+04 1.72e-04 55s + 48 2.31e-13 4.66e-09 1.02462035e+04 1.02243167e+04 1.17e-04 55s + 49 1.24e-13 1.54e-09 1.02449049e+04 1.02308377e+04 7.54e-05 56s + 50 9.59e-14 1.22e-09 1.02441557e+04 1.02318785e+04 6.58e-05 56s + 51 8.53e-14 7.46e-10 1.02438516e+04 1.02335717e+04 5.51e-05 57s + 52 1.42e-14 3.41e-10 1.02409154e+04 1.02358650e+04 2.71e-05 57s + 53 1.07e-14 1.08e-10 1.02400964e+04 1.02379817e+04 1.13e-05 57s + 54 1.07e-14 3.88e-11 1.02398951e+04 1.02384972e+04 7.49e-06 58s + 55 1.07e-14 2.85e-11 1.02397667e+04 1.02386509e+04 5.98e-06 58s + 56 1.07e-14 5.40e-12 1.02395468e+04 1.02389275e+04 3.32e-06 58s + 57 1.07e-14 1.24e-12 1.02394822e+04 1.02391409e+04 1.83e-06 59s + 58 1.07e-14 5.68e-13 1.02394092e+04 1.02392128e+04 1.05e-06 59s + 59 1.42e-14 2.56e-13 1.02393456e+04 1.02392605e+04 4.56e-07 59s + 60 1.07e-14 1.99e-13 1.02393207e+04 1.02392697e+04 2.73e-07 60s + 61 1.07e-14 5.19e-13 1.02393026e+04 1.02392801e+04 1.21e-07 60s + 62 1.42e-14 4.83e-13 1.02392989e+04 1.02392826e+04 8.73e-08 60s + 63 1.42e-14 7.18e-13 1.02392933e+04 1.02392837e+04 5.18e-08 61s + 64 1.42e-14 2.95e-13 1.02392903e+04 1.02392845e+04 3.08e-08 61s + 65 1.42e-14 1.46e-12 1.02392900e+04 1.02392874e+04 1.37e-08 61s + 66 1.07e-14 5.17e-12 1.02392895e+04 1.02392888e+04 3.76e-09 62s + 67* 1.42e-14 3.08e-12 1.02392891e+04 1.02392890e+04 3.57e-10 62s + 68* 1.42e-14 4.08e-12 1.02392891e+04 1.02392891e+04 4.85e-11 62s + 69* 1.42e-14 3.69e-12 1.02392891e+04 1.02392891e+04 9.52e-12 62s + 70* 1.42e-14 1.83e-12 1.02392891e+04 1.02392891e+04 1.83e-12 62s + 71* 1.78e-14 7.22e-12 1.02392891e+04 1.02392891e+04 4.13e-14 63s + 72* 1.42e-14 6.20e-12 1.02392891e+04 1.02392891e+04 4.51e-18 63s + Running crossover as requested + Primal residual before push phase: 4.73e-09 + Dual residual before push phase: 6.20e-12 + Number of dual pushes required: 31290 + Number of primal pushes required: 2494 + Summary + Runtime: 62.84s + Status interior point solve: optimal + Status crossover: optimal + objective value: 1.02392891e+04 + interior solution primal residual (abs/rel): 2.10e-13 / 1.28e-14 + interior solution dual residual (abs/rel): 6.20e-12 / 1.54e-14 + interior solution objective gap (abs/rel): -2.04e-10 / -1.99e-14 + basic solution primal infeasibility: 7.64e-14 + basic solution dual infeasibility: 2.10e-15 + Ipx: IPM optimal + Ipx: Crossover optimal + Solving the original LP from the solution after postsolve + Model status : Optimal + IPM iterations: 72 + Crossover iterations: 1881 + Objective value : 1.0239289084e+04 + HiGHS run time : 63.07 + LP solved for primal + + + + + + (A JuMP Model + Minimization problem with: + Variables: 120139 + Objective function type: AffExpr + `AffExpr`-in-`MathOptInterface.EqualTo{Float64}`: 35112 constraints + `AffExpr`-in-`MathOptInterface.GreaterThan{Float64}`: 20331 constraints + `AffExpr`-in-`MathOptInterface.LessThan{Float64}`: 97952 constraints + `VariableRef`-in-`MathOptInterface.EqualTo{Float64}`: 1 constraint + `VariableRef`-in-`MathOptInterface.GreaterThan{Float64}`: 116442 constraints + Model mode: AUTOMATIC + CachingOptimizer state: ATTACHED_OPTIMIZER + Solver name: HiGHS + Names registered in the model: FuelCalculationCommit_single, cCO2Emissions_systemwide, cFuelCalculation_single, cMaxCap, cMaxCapEnergy, cMaxCapEnergyDuration, cMaxFlow_in, cMaxFlow_out, cMaxLineReinforcement, cMaxNSE, cMaxRetCommit, cMaxRetEnergy, cMaxRetNoCommit, cMinCap, cMinCapEnergy, cMinCapEnergyDuration, cNSEPerSeg, cPowerBalance, cSoCBalInterior, cSoCBalStart, cStartFuel_single, cTAuxLimit, cTAuxSum, cTLoss, eAvail_Trans_Cap, eCCO2Cap_slack, eCFix, eCFixEnergy, eCFuelOut, eCFuelStart, eCNSE, eCStart, eCTotalCO2CapSlack, eCVar_in, eCVar_out, eELOSS, eELOSSByZone, eEmissionsByPlant, eEmissionsByZone, eExistingCap, eExistingCapEnergy, eFuelConsumption, eFuelConsumptionYear, eFuelConsumption_single, eGenerationByThermAll, eGenerationByVRE, eGenerationByZone, eLosses_By_Zone, eNet_Export_Flows, eObj, ePlantCFuelOut, ePlantCFuelStart, ePlantFuel_generation, ePlantFuel_start, ePowerBalance, ePowerBalanceDisp, ePowerBalanceLossesByZone, ePowerBalanceNetExportFlows, ePowerBalanceNse, ePowerBalanceStor, ePowerBalanceThermCommit, eStartFuel, eTotalCFix, eTotalCFixEnergy, eTotalCFuelOut, eTotalCFuelStart, eTotalCNSE, eTotalCNSET, eTotalCNSETS, eTotalCNetworkExp, eTotalCStart, eTotalCStartT, eTotalCVarIn, eTotalCVarInT, eTotalCVarOut, eTotalCVarOutT, eTotalCap, eTotalCapEnergy, eTransMax, eZonalCFuelOut, eZonalCFuelStart, vCAP, vCAPENERGY, vCHARGE, vCO2Cap_slack, vCOMMIT, vFLOW, vFuel, vNEW_TRANS_CAP, vNSE, vP, vRETCAP, vRETCAPENERGY, vS, vSHUT, vSTART, vStartFuel, vTAUX_NEG, vTAUX_POS, vTLOSS, vZERO, 63.40430498123169) + + + + +```julia +value.(EP4[:eCTotalCO2CapSlack]) +``` + + + + + 2816.8936379034667 + + + + +```julia +totCap4 = value.(EP4[:eTotalCap]) + +totCapB4 = [totCap4[1] + totCap4[2] + totCap4[3], totCap4[4] + totCap4[6], + totCap4[5] + totCap4[7], totCap4[8] + totCap4[9] + totCap4[10]] + +println(DataFrame([RT totCap4],["Resource Type","Total Capacity"])) +println(" ") + +println("Objective Value: ", objective_value(EP4)) + +G4 = groupedbar(transpose(totCapB4), bar_position = :stack, bar_width=0.1,size=(400,450), xticks=[ ],ylabel="GW", + labels=["Natural Gas" "Solar" "Wind" "Battery"],legend=false,color=colors, + title="CO2 Mass Cap, Zero + Slack Tolerance \n Obj Val: $(round(objective_value(EP4),digits=6))") + +plot(G4,G3,G2,size=(900,450),layout=(1,3),titlefontsize=8) +``` + + DataFrame + Row │Resource Type Total Capacity + │Any Any + ─────┼─────────────────────────────────────────────── + 1 │ MA_natural_gas_combined_cycle 6.20579 + 2 │ CT_natural_gas_combined_cycle 7.42028 + 3 │ ME_natural_gas_combined_cycle 0.0 + 4 │ MA_solar_pv 14.2444 + 5 │ CT_onshore_wind 12.0451 + 6 │ CT_solar_pv 5.26796 + 7 │ ME_onshore_wind 8.48774 + 8 │ MA_battery 2.29813 + 9 │ CT_battery 2.86763 + 10 │ ME_battery 0.390909 + + Objective Value: 10239.289084181632 + + +![svg](./files/t7_3p_slack.svg) + + + +Adding in the slack variables allowed for some natural gas to be used once again and decreased the overall cost (objective function). + +### Load-based Cap + +Another way to set the CO$_2$ emissions cap is to limit emissions as a function of the total demand in that region. This can be done by setting `CO2Cap` to "2" in the setup: + + +```julia +setup["CO2Cap"] = 2; +``` + +Let's set the CO2_cap.csv back to it's original data, and remove the slack cap: + + +```julia +rm(joinpath(case,"policies/CO2_cap_slack.csv")) +CSV.write(joinpath(case,"policies/CO2_cap.csv"),CO2Cap) +``` + + + + + "example_systems/1_three_zones/policies/CO2_cap.csv" + + + + +```julia +inputs = GenX.load_inputs(setup, case) +EP5 = GenX.generate_model(setup,inputs,OPTIMIZER) +GenX.solve_model(EP5,setup) +``` + + Reading Input CSV Files + Network.csv Successfully Read! + Demand (load) data Successfully Read! + Fuels_data.csv Successfully Read! + + Summary of resources loaded into the model: + ------------------------------------------------------- + Resource type Number of resources + ======================================================= + Thermal 3 + VRE 4 + Storage 3 + ======================================================= + Total number of resources: 10 + ------------------------------------------------------- + Generators_variability.csv Successfully Read! + Validating time basis + CO2_cap.csv Successfully Read! + CSV Files Successfully Read In From example_systems/1_three_zones + Discharge Module + Non-served Energy Module + + + Thermal.csv Successfully Read. + Vre.csv Successfully Read. + Storage.csv Successfully Read. + Resource_energy_share_requirement.csv Successfully Read. + Resource_capacity_reserve_margin.csv Successfully Read. + Resource_minimum_capacity_requirement.csv Successfully Read. + + + Investment Discharge Module + Unit Commitment Module + Fuel Module + CO2 Module + Investment Transmission Module + Transmission Module + Dispatchable Resources Module + Storage Resources Module + Storage Investment Module + Storage Core Resources Module + Storage Resources with Symmetric Charge/Discharge Capacity Module + Thermal (Unit Commitment) Resources Module + CO2 Policies Module + Running HiGHS 1.6.0: Copyright (c) 2023 HiGHS under MIT licence terms + Presolving model + 118158 rows, 81204 cols, 467187 nonzeros + 110739 rows, 73785 cols, 468729 nonzeros + Presolve : Reductions: rows 110739(-42656); columns 73785(-46354); elements 468729(-46757) + Solving the presolved LP + IPX model has 110739 rows, 73785 columns and 468729 nonzeros + Input + Number of variables: 73785 + Number of free variables: 3696 + Number of constraints: 110739 + Number of equality constraints: 16605 + Number of matrix entries: 468729 + Matrix range: [4e-07, 1e+01] + RHS range: [7e-01, 4e+03] + Objective range: [1e-04, 4e+02] + Bounds range: [2e-03, 2e+01] + Preprocessing + Dualized model: no + Number of dense columns: 15 + Range of scaling factors: [5.00e-01, 4.00e+00] + IPX version 1.0 + Interior Point Solve + Iter P.res D.res P.obj D.obj mu Time + 0 1.71e+03 3.41e+02 2.96378701e+06 -4.23587199e+06 5.75e+05 0s + 1 1.13e+03 1.31e+02 -4.89641638e+08 -8.84226731e+06 3.61e+05 0s + 2 1.09e+03 1.07e+02 -4.90812561e+08 -3.51675964e+07 4.11e+05 1s + 3 3.74e+02 5.23e+01 -3.72143959e+08 -3.93800995e+07 1.67e+05 1s + Constructing starting basis... + 4 1.74e+02 2.59e+01 -1.89656388e+08 -4.38688644e+07 8.52e+04 3s + 5 1.55e+02 2.32e+01 -1.66927356e+08 -4.54471496e+07 8.00e+04 5s + 6 2.66e+01 8.62e+00 9.40431644e+06 -4.92415912e+07 2.67e+04 6s + 7 2.77e+00 7.85e-01 1.11318915e+07 -4.76427177e+07 3.01e+03 8s + 8 6.28e-01 3.10e-01 1.16295577e+07 -3.03610025e+07 1.13e+03 10s + 9 2.91e-01 1.04e-01 1.07968497e+07 -1.69468432e+07 4.87e+02 11s + 10 1.59e-01 8.85e-02 1.01816820e+07 -1.56044371e+07 4.27e+02 12s + 11 8.12e-02 5.53e-02 8.96818818e+06 -1.14192748e+07 2.85e+02 13s + 12 3.88e-02 3.75e-02 7.95015583e+06 -9.07252228e+06 2.05e+02 14s + 13 1.86e-02 2.40e-02 6.21487140e+06 -6.41755461e+06 1.30e+02 15s + 14 6.01e-03 1.42e-02 4.80373854e+06 -4.66630024e+06 8.23e+01 18s + 15 2.80e-03 8.10e-03 3.07276238e+06 -2.78932663e+06 4.44e+01 19s + 16 1.05e-08 1.61e-03 1.54522332e+06 -1.15219019e+06 1.60e+01 20s + 17 3.81e-09 1.40e-04 4.33083581e+05 -9.75643163e+04 2.89e+00 21s + 18 2.40e-09 3.64e-05 1.17725979e+05 -2.92212886e+04 7.91e-01 22s + 19 9.04e-10 1.11e-05 6.41480326e+04 -1.64155777e+04 4.33e-01 23s + 20 7.36e-10 6.49e-06 4.91641901e+04 -1.03802768e+04 3.20e-01 24s + 21 6.56e-10 4.00e-06 4.47995622e+04 -6.50448658e+03 2.75e-01 25s + 22 3.74e-10 2.11e-06 3.33171246e+04 -2.19844201e+03 1.91e-01 25s + 23 4.11e-10 1.17e-06 3.13473710e+04 -2.25128297e+02 1.69e-01 27s + 24 5.32e-10 9.05e-07 3.07180077e+04 6.59187318e+02 1.61e-01 30s + 25 2.07e-10 5.74e-07 2.40909020e+04 2.36880496e+03 1.17e-01 31s + 26 1.46e-10 3.48e-07 1.95591688e+04 3.82235596e+03 8.44e-02 33s + 27 9.56e-11 2.77e-07 1.91099002e+04 4.16735208e+03 8.02e-02 35s + 28 1.16e-10 1.72e-07 1.68130157e+04 5.05922686e+03 6.31e-02 37s + 29 1.10e-10 1.14e-07 1.58420855e+04 5.68901990e+03 5.45e-02 39s + 30 1.00e-10 9.13e-08 1.50304186e+04 6.09927663e+03 4.79e-02 41s + 31 4.79e-11 4.62e-08 1.44849049e+04 6.49417572e+03 4.29e-02 43s + 32 5.38e-11 3.14e-08 1.33010538e+04 7.09832458e+03 3.33e-02 44s + 33 1.97e-11 2.65e-08 1.31349015e+04 7.15881992e+03 3.21e-02 48s + 34 4.25e-11 2.05e-08 1.26742180e+04 7.39203286e+03 2.83e-02 49s + 35 3.24e-11 1.56e-08 1.23110122e+04 7.63239675e+03 2.51e-02 51s + 36 4.28e-11 1.03e-08 1.20230845e+04 7.85317825e+03 2.24e-02 53s + 37 1.56e-11 7.64e-09 1.12986653e+04 8.11596241e+03 1.71e-02 54s + 38 2.81e-11 5.87e-09 1.11528506e+04 8.19787914e+03 1.59e-02 56s + 39 2.28e-11 3.67e-09 1.06041200e+04 8.51424102e+03 1.12e-02 59s + 40 1.25e-11 3.02e-09 1.05115674e+04 8.58142162e+03 1.04e-02 62s + 41 7.03e-12 2.68e-09 1.03942162e+04 8.63482986e+03 9.44e-03 63s + 42 1.77e-11 2.01e-09 1.03904932e+04 8.70538318e+03 9.04e-03 65s + 43 4.66e-11 1.63e-09 1.03579306e+04 8.76566015e+03 8.54e-03 66s + 44 1.96e-11 1.36e-09 1.01427363e+04 8.86378332e+03 6.86e-03 67s + 45 3.52e-11 9.40e-10 1.00522553e+04 9.00927911e+03 5.60e-03 69s + 46 4.02e-11 6.12e-10 9.95594984e+03 9.12097703e+03 4.48e-03 70s + 47 4.90e-12 4.94e-10 9.90731026e+03 9.18680967e+03 3.87e-03 72s + 48 1.55e-11 4.61e-10 9.90647522e+03 9.19643998e+03 3.81e-03 73s + 49 1.17e-11 4.18e-10 9.90023591e+03 9.20935084e+03 3.71e-03 74s + 50 3.61e-11 3.73e-10 9.89683026e+03 9.21663764e+03 3.65e-03 75s + 51 1.84e-11 3.46e-10 9.85096096e+03 9.23460950e+03 3.31e-03 76s + 52 7.42e-12 2.95e-10 9.78480033e+03 9.27774527e+03 2.72e-03 77s + 53 3.37e-11 2.51e-10 9.78057662e+03 9.30246448e+03 2.56e-03 78s + 54 1.43e-11 1.70e-10 9.77649230e+03 9.34200543e+03 2.33e-03 79s + 55 9.01e-12 1.18e-10 9.73939839e+03 9.39121374e+03 1.87e-03 80s + 56 4.50e-11 9.00e-11 9.72960001e+03 9.41702931e+03 1.68e-03 81s + 57 3.25e-11 6.25e-11 9.70849173e+03 9.44892074e+03 1.39e-03 82s + 58 9.01e-11 5.72e-11 9.69018439e+03 9.45729846e+03 1.25e-03 83s + 59 6.80e-11 4.15e-11 9.68317655e+03 9.47569218e+03 1.11e-03 84s + 60 1.68e-11 2.38e-11 9.67429417e+03 9.51041472e+03 8.79e-04 85s + 61 4.47e-11 1.79e-11 9.65746593e+03 9.53257688e+03 6.70e-04 86s + 62 3.95e-11 1.43e-11 9.65470621e+03 9.54043028e+03 6.13e-04 87s + 63 7.96e-11 8.76e-12 9.64849756e+03 9.56065424e+03 4.71e-04 87s + 64 6.17e-12 5.12e-12 9.64469171e+03 9.57460001e+03 3.76e-04 88s + 65 7.14e-11 4.06e-12 9.63840771e+03 9.58058736e+03 3.10e-04 89s + 66 1.43e-11 2.70e-12 9.63355725e+03 9.58844693e+03 2.42e-04 90s + 67 8.82e-12 2.42e-12 9.62962513e+03 9.59238503e+03 2.00e-04 90s + 68 5.56e-11 1.68e-12 9.62718274e+03 9.59579105e+03 1.68e-04 91s + 69 6.50e-12 1.05e-12 9.62258889e+03 9.60351437e+03 1.02e-04 92s + 70 2.18e-11 9.95e-13 9.62203860e+03 9.60540964e+03 8.92e-05 92s + 71 1.79e-10 3.84e-13 9.62055756e+03 9.61022195e+03 5.54e-05 93s + 72 5.57e-11 6.82e-13 9.61970045e+03 9.61377588e+03 3.18e-05 94s + 73 6.29e-11 5.40e-13 9.61958853e+03 9.61443607e+03 2.76e-05 94s + 74 1.14e-10 3.13e-13 9.61921628e+03 9.61498366e+03 2.27e-05 95s + 75 6.88e-11 1.85e-13 9.61887340e+03 9.61542318e+03 1.85e-05 96s + 76 5.60e-10 3.69e-13 9.61870185e+03 9.61564680e+03 1.64e-05 96s + 77 5.59e-10 4.83e-13 9.61842859e+03 9.61678126e+03 8.84e-06 97s + 78 6.89e-10 5.12e-13 9.61807911e+03 9.61688976e+03 6.38e-06 97s + 79 3.62e-10 3.98e-13 9.61785021e+03 9.61733220e+03 2.78e-06 98s + 80 1.09e-10 3.98e-13 9.61769612e+03 9.61747059e+03 1.21e-06 99s + 81 2.20e-10 3.02e-13 9.61761335e+03 9.61750430e+03 5.85e-07 99s + 82 1.17e-09 8.24e-13 9.61757268e+03 9.61751083e+03 3.32e-07 100s + 83 4.79e-10 8.53e-13 9.61755931e+03 9.61753499e+03 1.30e-07 101s + 84 5.65e-10 1.72e-12 9.61755460e+03 9.61753993e+03 7.87e-08 101s + 85 6.45e-10 3.25e-12 9.61754798e+03 9.61754458e+03 1.83e-08 102s + 86 5.31e-10 5.47e-12 9.61754612e+03 9.61754560e+03 2.83e-09 103s + 87 2.06e-10 2.54e-12 9.61754586e+03 9.61754575e+03 5.86e-10 103s + 88* 1.53e-09 8.29e-12 9.61754581e+03 9.61754579e+03 7.40e-11 105s + 89* 8.48e-10 4.73e-12 9.61754580e+03 9.61754580e+03 1.14e-11 107s + Running crossover as requested + Primal residual before push phase: 1.05e-05 + Dual residual before push phase: 3.29e-06 + Number of dual pushes required: 25720 + Number of primal pushes required: 3952 + Summary + Runtime: 110.60s + Status interior point solve: optimal + Status crossover: optimal + objective value: 9.61754580e+03 + interior solution primal residual (abs/rel): 2.90e-08 / 7.03e-12 + interior solution dual residual (abs/rel): 9.59e-09 / 2.38e-11 + interior solution objective gap (abs/rel): 2.01e-06 / 2.09e-10 + basic solution primal infeasibility: 7.86e-13 + basic solution dual infeasibility: 1.59e-15 + Ipx: IPM optimal + Ipx: Crossover optimal + Solving the original LP from the solution after postsolve + Model status : Optimal + IPM iterations: 89 + Crossover iterations: 2840 + Objective value : 9.6175458026e+03 + HiGHS run time : 110.86 + LP solved for primal + + + + + + (A JuMP Model + Minimization problem with: + Variables: 120139 + Objective function type: AffExpr + `AffExpr`-in-`MathOptInterface.EqualTo{Float64}`: 35112 constraints + `AffExpr`-in-`MathOptInterface.GreaterThan{Float64}`: 20331 constraints + `AffExpr`-in-`MathOptInterface.LessThan{Float64}`: 97952 constraints + `VariableRef`-in-`MathOptInterface.EqualTo{Float64}`: 4 constraints + `VariableRef`-in-`MathOptInterface.GreaterThan{Float64}`: 116439 constraints + Model mode: AUTOMATIC + CachingOptimizer state: ATTACHED_OPTIMIZER + Solver name: HiGHS + Names registered in the model: FuelCalculationCommit_single, cCO2Emissions_systemwide, cFuelCalculation_single, cMaxCap, cMaxCapEnergy, cMaxCapEnergyDuration, cMaxFlow_in, cMaxFlow_out, cMaxLineReinforcement, cMaxNSE, cMaxRetCommit, cMaxRetEnergy, cMaxRetNoCommit, cMinCap, cMinCapEnergy, cMinCapEnergyDuration, cNSEPerSeg, cPowerBalance, cSoCBalInterior, cSoCBalStart, cStartFuel_single, cTAuxLimit, cTAuxSum, cTLoss, eAvail_Trans_Cap, eCFix, eCFixEnergy, eCFuelOut, eCFuelStart, eCNSE, eCStart, eCVar_in, eCVar_out, eELOSS, eELOSSByZone, eEmissionsByPlant, eEmissionsByZone, eExistingCap, eExistingCapEnergy, eFuelConsumption, eFuelConsumptionYear, eFuelConsumption_single, eGenerationByThermAll, eGenerationByVRE, eGenerationByZone, eLosses_By_Zone, eNet_Export_Flows, eObj, ePlantCFuelOut, ePlantCFuelStart, ePlantFuel_generation, ePlantFuel_start, ePowerBalance, ePowerBalanceDisp, ePowerBalanceLossesByZone, ePowerBalanceNetExportFlows, ePowerBalanceNse, ePowerBalanceStor, ePowerBalanceThermCommit, eStartFuel, eTotalCFix, eTotalCFixEnergy, eTotalCFuelOut, eTotalCFuelStart, eTotalCNSE, eTotalCNSET, eTotalCNSETS, eTotalCNetworkExp, eTotalCStart, eTotalCStartT, eTotalCVarIn, eTotalCVarInT, eTotalCVarOut, eTotalCVarOutT, eTotalCap, eTotalCapEnergy, eTransMax, eZonalCFuelOut, eZonalCFuelStart, vCAP, vCAPENERGY, vCHARGE, vCO2Cap_slack, vCOMMIT, vFLOW, vFuel, vNEW_TRANS_CAP, vNSE, vP, vRETCAP, vRETCAPENERGY, vS, vSHUT, vSTART, vStartFuel, vTAUX_NEG, vTAUX_POS, vTLOSS, vZERO, 111.20706605911255) + + + + +```julia +totCap5 = value.(EP5[:eTotalCap]) + +totCapB5 = [totCap5[1] + totCap5[2] + totCap5[3], totCap5[4] + totCap5[6], + totCap5[5] + totCap5[7], totCap5[8] + totCap5[9] + totCap5[10]] + +toPlot = [transpose(totCapB2);transpose(totCapB5)] + +println(DataFrame([RT totCap5],["Resource Type","Total Capacity"])) +println(" ") + +G5 = groupedbar(transpose(totCapB5), bar_position = :stack, bar_width=.8,size=(500,450), xticks=[ ],ylabel="GW", + labels=["Natural Gas" "Solar" "Wind" "Battery"],color=colors, + title="CO2 Load Rate Cap \n Obj Val: $(round(objective_value(EP5),digits=6))") +plot(G5,G2,size=(800,450), titlefontsize=9) +``` + + 10x2 DataFrame + Row | Resource Type Total Capacity + │ Any Any + ─────┼─────────────────────────────────────────────── + 1 │ MA_natural_gas_combined_cycle 8.3383 + 2 │ CT_natural_gas_combined_cycle 2.0596 + 3 │ ME_natural_gas_combined_cycle 0.527558 + 4 │ MA_solar_pv 18.4836 + 5 │ CT_onshore_wind 13.1932 + 6 │ CT_solar_pv 13.4186 + 7 │ ME_onshore_wind 11.3012 + 8 │ MA_battery 4.24838 + 9 │ CT_battery 4.50189 + 10 │ ME_battery 1.02331 + + +![svg](./files/t7_2p_load_mass.svg) + + + +### Generator-based Cap + +Finally, the third type of emissions cap in GenX is where the constraint is based on the ratio between the CO$_2$ cap and the generation of each node. + + +```julia +setup["CO2Cap"] = 3; +``` + + +```julia +inputs = GenX.load_inputs(setup, case) +EP6 = GenX.generate_model(setup,inputs,OPTIMIZER) +GenX.solve_model(EP6,setup) +``` + + Reading Input CSV Files + Network.csv Successfully Read! + Demand (load) data Successfully Read! + Fuels_data.csv Successfully Read! + + Summary of resources loaded into the model: + ------------------------------------------------------- + Resource type Number of resources + ======================================================= + Thermal 3 + VRE 4 + Storage 3 + ======================================================= + Total number of resources: 10 + ------------------------------------------------------- + Generators_variability.csv Successfully Read! + Validating time basis + CO2_cap.csv Successfully Read! + CSV Files Successfully Read In From example_systems/1_three_zones + Discharge Module + + + Thermal.csv Successfully Read. + Vre.csv Successfully Read. + Storage.csv Successfully Read. + Resource_energy_share_requirement.csv Successfully Read. + Resource_capacity_reserve_margin.csv Successfully Read. + Resource_minimum_capacity_requirement.csv Successfully Read. + + + Non-served Energy Module + Investment Discharge Module + Unit Commitment Module + Fuel Module + CO2 Module + Investment Transmission Module + Transmission Module + Dispatchable Resources Module + Storage Resources Module + Storage Investment Module + Storage Core Resources Module + Storage Resources with Symmetric Charge/Discharge Capacity Module + Thermal (Unit Commitment) Resources Module + CO2 Policies Module + Running HiGHS 1.6.0: Copyright (c) 2023 HiGHS under MIT licence terms + Presolving model + 118158 rows, 81204 cols, 439344 nonzeros + 110739 rows, 73785 cols, 440886 nonzeros + Presolve : Reductions: rows 110739(-42656); columns 73785(-46354); elements 440886(-54272) + Solving the presolved LP + IPX model has 110739 rows, 73785 columns and 440886 nonzeros + Input + Number of variables: 73785 + Number of free variables: 3696 + Number of constraints: 110739 + Number of equality constraints: 16605 + Number of matrix entries: 440886 + Matrix range: [4e-07, 1e+01] + RHS range: [7e-01, 2e+01] + Objective range: [1e-04, 4e+02] + Bounds range: [2e-03, 2e+01] + Preprocessing + Dualized model: no + Number of dense columns: 15 + Range of scaling factors: [5.00e-01, 4.00e+00] + IPX version 1.0 + Interior Point Solve + Iter P.res D.res P.obj D.obj mu Time + 0 8.76e+00 3.68e+02 3.33955953e+06 -4.97437905e+06 3.24e+03 0s + 1 4.28e+00 1.43e+02 -2.33497546e+05 -5.06214948e+06 1.60e+03 0s + 2 3.87e+00 8.66e+01 -3.47321172e+05 -1.15250215e+07 1.45e+03 1s + 3 2.40e+00 4.93e+01 -6.12062937e+05 -1.60606245e+07 1.08e+03 1s + Constructing starting basis... + 4 1.00e+00 2.05e+01 1.18453276e+05 -1.28403154e+07 4.55e+02 3s + 5 7.57e-01 1.68e+01 1.98998600e+05 -1.20496551e+07 3.90e+02 7s + 6 4.01e-01 1.11e+01 3.68752096e+05 -9.85292291e+06 2.70e+02 9s + 7 1.69e-01 2.89e+00 4.01592303e+05 -3.46185082e+06 8.23e+01 10s + 8 1.94e-02 8.41e-01 2.49236130e+05 -1.42423792e+06 2.40e+01 12s + 9 3.13e-03 3.34e-01 1.41607109e+05 -6.32482360e+05 9.69e+00 13s + 10 1.53e-03 2.07e-01 1.23187989e+05 -4.48100415e+05 6.79e+00 14s + 11 4.26e-04 1.34e-01 1.20904571e+05 -3.54933673e+05 5.58e+00 15s + 12 1.92e-04 8.44e-02 1.04908501e+05 -2.54959177e+05 3.93e+00 16s + 13 8.09e-05 4.83e-02 8.49988300e+04 -1.63933485e+05 2.44e+00 18s + 14 4.62e-05 2.72e-02 7.06545133e+04 -1.02323699e+05 1.51e+00 19s + 15 1.94e-05 1.68e-02 5.24639561e+04 -6.95227074e+04 9.61e-01 20s + 16 1.02e-05 9.84e-03 3.77185714e+04 -4.17894192e+04 5.70e-01 22s + 17 6.81e-06 6.93e-03 3.08242939e+04 -2.91084107e+04 4.08e-01 23s + 18 3.58e-06 3.36e-03 2.57406414e+04 -1.44027309e+04 2.52e-01 24s + 19 1.77e-06 2.13e-03 2.24464783e+04 -8.64942680e+03 1.87e-01 26s + 20 8.10e-07 1.60e-03 2.00886717e+04 -5.98699450e+03 1.53e-01 27s + 21 4.67e-07 9.13e-04 1.76885055e+04 -1.32644122e+03 1.08e-01 29s + 22 2.19e-07 5.64e-04 1.53684619e+04 1.46187383e+03 7.78e-02 31s + 23 1.74e-07 4.77e-04 1.49485301e+04 2.12578093e+03 7.14e-02 34s + 24 1.03e-07 2.97e-04 1.37756560e+04 3.92211046e+03 5.43e-02 35s + 25 8.23e-08 2.03e-04 1.33224182e+04 4.96097392e+03 4.58e-02 37s + 26 6.69e-08 1.35e-04 1.28827892e+04 5.89161111e+03 3.81e-02 39s + 27 5.54e-08 5.85e-05 1.24932478e+04 7.23288405e+03 2.85e-02 41s + 28 3.67e-08 4.30e-05 1.17469879e+04 7.63959217e+03 2.22e-02 43s + 29 3.44e-08 3.56e-05 1.16936961e+04 7.73126184e+03 2.14e-02 46s + 30 1.64e-08 2.46e-05 1.09468658e+04 8.09132155e+03 1.54e-02 48s + 31 1.28e-08 1.98e-05 1.08111979e+04 8.22677378e+03 1.39e-02 50s + 32 8.98e-09 1.32e-05 1.06618652e+04 8.43574381e+03 1.20e-02 54s + 33 6.40e-09 1.23e-05 1.05084042e+04 8.48572171e+03 1.09e-02 55s + 34 6.03e-09 8.69e-06 1.04928796e+04 8.61992062e+03 1.01e-02 56s + 35 3.31e-09 6.98e-06 1.02642253e+04 8.74639498e+03 8.17e-03 58s + 36 3.12e-09 6.41e-06 1.02570400e+04 8.76891800e+03 8.00e-03 59s + 37 2.68e-09 4.60e-06 1.02120829e+04 8.91995727e+03 6.95e-03 60s + 38 1.41e-09 3.18e-06 1.01085720e+04 9.01554382e+03 5.87e-03 62s + 39 1.37e-09 2.31e-06 1.01047794e+04 9.08685475e+03 5.47e-03 63s + 40 1.06e-09 1.98e-06 1.00606064e+04 9.12387842e+03 5.03e-03 64s + 41 5.24e-10 1.13e-06 9.91648667e+03 9.28792782e+03 3.38e-03 65s + 42 4.98e-10 1.10e-06 9.91527075e+03 9.29016011e+03 3.36e-03 67s + 43 3.69e-10 1.02e-06 9.88770629e+03 9.30443644e+03 3.13e-03 69s + 44 3.55e-10 7.63e-07 9.88504084e+03 9.33991073e+03 2.93e-03 70s + 45 2.06e-10 4.05e-07 9.82906056e+03 9.43570351e+03 2.11e-03 71s + 46 2.02e-10 3.62e-07 9.82827522e+03 9.44146127e+03 2.08e-03 74s + 47 1.90e-10 3.22e-07 9.82379653e+03 9.45113014e+03 2.00e-03 75s + 48 9.94e-11 2.15e-07 9.78341274e+03 9.48706810e+03 1.59e-03 76s + 49 8.38e-11 2.01e-07 9.77551388e+03 9.49181672e+03 1.52e-03 77s + 50 4.59e-11 1.75e-07 9.74064260e+03 9.50762968e+03 1.25e-03 78s + 51 3.34e-11 1.19e-07 9.72951560e+03 9.54230950e+03 1.00e-03 79s + 52 2.62e-11 9.58e-08 9.72573311e+03 9.55294569e+03 9.27e-04 79s + 53 2.08e-11 4.63e-08 9.70432411e+03 9.59417118e+03 5.91e-04 80s + 54 9.60e-12 4.06e-08 9.69614280e+03 9.59920478e+03 5.20e-04 82s + 55 6.07e-12 2.67e-08 9.69178503e+03 9.61073037e+03 4.35e-04 82s + 56 1.02e-11 1.94e-08 9.68738193e+03 9.62154935e+03 3.53e-04 83s + 57 2.69e-11 8.60e-09 9.67895715e+03 9.63526917e+03 2.34e-04 84s + 58 1.57e-11 4.55e-09 9.67420309e+03 9.64278621e+03 1.69e-04 85s + 59 2.28e-11 2.55e-09 9.67122848e+03 9.64774778e+03 1.26e-04 85s + 60 3.88e-11 1.82e-09 9.66945757e+03 9.65096502e+03 9.92e-05 86s + 61 2.83e-11 1.00e-09 9.66699692e+03 9.65455335e+03 6.68e-05 86s + 62 2.73e-11 8.69e-10 9.66606426e+03 9.65556481e+03 5.63e-05 87s + 63 1.83e-11 7.05e-10 9.66498918e+03 9.65678888e+03 4.40e-05 88s + 64 1.56e-11 6.47e-10 9.66447640e+03 9.65724431e+03 3.88e-05 88s + 65 2.19e-11 2.62e-10 9.66409009e+03 9.66025109e+03 2.06e-05 89s + 66 7.78e-12 9.83e-11 9.66368883e+03 9.66207263e+03 8.67e-06 89s + 67 1.84e-11 7.20e-11 9.66349991e+03 9.66236162e+03 6.11e-06 90s + 68 2.47e-11 1.74e-11 9.66350655e+03 9.66290499e+03 3.23e-06 90s + 69 1.18e-11 5.33e-12 9.66339437e+03 9.66318656e+03 1.11e-06 91s + 70 1.13e-11 1.59e-12 9.66335658e+03 9.66328211e+03 3.99e-07 91s + 71 4.69e-11 4.26e-13 9.66334140e+03 9.66331249e+03 1.55e-07 92s + 72 1.37e-11 1.99e-13 9.66333277e+03 9.66332230e+03 5.62e-08 93s + 73 9.00e-12 4.83e-13 9.66332930e+03 9.66332548e+03 2.05e-08 93s + 74 1.55e-11 1.62e-12 9.66332820e+03 9.66332729e+03 4.84e-09 94s + 75 5.26e-11 1.23e-12 9.66332779e+03 9.66332745e+03 1.79e-09 94s + 76* 5.68e-11 2.59e-12 9.66332773e+03 9.66332767e+03 3.36e-10 95s + 77* 2.07e-11 5.92e-12 9.66332772e+03 9.66332771e+03 4.70e-11 96s + 78* 1.07e-10 7.45e-12 9.66332771e+03 9.66332771e+03 5.07e-12 98s + 79* 2.20e-10 9.85e-12 9.66332771e+03 9.66332771e+03 3.44e-13 98s + 80* 1.15e-10 6.51e-12 9.66332771e+03 9.66332771e+03 6.25e-14 99s + Running crossover as requested + Primal residual before push phase: 3.25e-07 + Dual residual before push phase: 2.20e-08 + Number of dual pushes required: 26184 + Number of primal pushes required: 3501 + Summary + Runtime: 101.61s + Status interior point solve: optimal + Status crossover: optimal + objective value: 9.66332771e+03 + interior solution primal residual (abs/rel): 1.27e-09 / 7.78e-11 + interior solution dual residual (abs/rel): 1.40e-10 / 3.48e-13 + interior solution objective gap (abs/rel): 6.66e-09 / 6.89e-13 + basic solution primal infeasibility: 5.33e-14 + basic solution dual infeasibility: 4.34e-14 + Ipx: IPM optimal + Ipx: Crossover optimal + Solving the original LP from the solution after postsolve + Model status : Optimal + IPM iterations: 80 + Crossover iterations: 2929 + Objective value : 9.6633277143e+03 + HiGHS run time : 101.86 + LP solved for primal + + + + + + (A JuMP Model + Minimization problem with: + Variables: 120139 + Objective function type: AffExpr + `AffExpr`-in-`MathOptInterface.EqualTo{Float64}`: 35112 constraints + `AffExpr`-in-`MathOptInterface.GreaterThan{Float64}`: 20331 constraints + `AffExpr`-in-`MathOptInterface.LessThan{Float64}`: 97952 constraints + `VariableRef`-in-`MathOptInterface.EqualTo{Float64}`: 4 constraints + `VariableRef`-in-`MathOptInterface.GreaterThan{Float64}`: 116439 constraints + Model mode: AUTOMATIC + CachingOptimizer state: ATTACHED_OPTIMIZER + Solver name: HiGHS + Names registered in the model: FuelCalculationCommit_single, cCO2Emissions_systemwide, cFuelCalculation_single, cMaxCap, cMaxCapEnergy, cMaxCapEnergyDuration, cMaxFlow_in, cMaxFlow_out, cMaxLineReinforcement, cMaxNSE, cMaxRetCommit, cMaxRetEnergy, cMaxRetNoCommit, cMinCap, cMinCapEnergy, cMinCapEnergyDuration, cNSEPerSeg, cPowerBalance, cSoCBalInterior, cSoCBalStart, cStartFuel_single, cTAuxLimit, cTAuxSum, cTLoss, eAvail_Trans_Cap, eCFix, eCFixEnergy, eCFuelOut, eCFuelStart, eCNSE, eCStart, eCVar_in, eCVar_out, eELOSS, eELOSSByZone, eEmissionsByPlant, eEmissionsByZone, eExistingCap, eExistingCapEnergy, eFuelConsumption, eFuelConsumptionYear, eFuelConsumption_single, eGenerationByThermAll, eGenerationByVRE, eGenerationByZone, eLosses_By_Zone, eNet_Export_Flows, eObj, ePlantCFuelOut, ePlantCFuelStart, ePlantFuel_generation, ePlantFuel_start, ePowerBalance, ePowerBalanceDisp, ePowerBalanceLossesByZone, ePowerBalanceNetExportFlows, ePowerBalanceNse, ePowerBalanceStor, ePowerBalanceThermCommit, eStartFuel, eTotalCFix, eTotalCFixEnergy, eTotalCFuelOut, eTotalCFuelStart, eTotalCNSE, eTotalCNSET, eTotalCNSETS, eTotalCNetworkExp, eTotalCStart, eTotalCStartT, eTotalCVarIn, eTotalCVarInT, eTotalCVarOut, eTotalCVarOutT, eTotalCap, eTotalCapEnergy, eTransMax, eZonalCFuelOut, eZonalCFuelStart, vCAP, vCAPENERGY, vCHARGE, vCO2Cap_slack, vCOMMIT, vFLOW, vFuel, vNEW_TRANS_CAP, vNSE, vP, vRETCAP, vRETCAPENERGY, vS, vSHUT, vSTART, vStartFuel, vTAUX_NEG, vTAUX_POS, vTLOSS, vZERO, 102.19283390045166) + + + + +```julia +totCap6 = value.(EP6[:eTotalCap]) + +totCapB6 = [totCap6[1] + totCap6[2] + totCap6[3], totCap6[4] + totCap6[6], + totCap6[5] + totCap6[7], totCap6[8] + totCap6[9] + totCap6[10]] + +println(DataFrame([RT totCap6],["Resource Type","Total Capacity"])) +println(" ") + +G6 = groupedbar(transpose(totCapB6), bar_position = :stack, bar_width=.7,size=(500,450), xticks=[ ],ylabel="GW", + labels=["Natural Gas" "Solar" "Wind" "Battery"],legend=false,color=colors, + title="CO2 Generation Rate Cap \n Obj Val: $(round(objective_value(EP6),digits=6))",ylabelfontsize=8) +plot(G2,G5,G6,size=(900,450), titlefontsize=8,layout=(1,3),) +``` + + 10×2 DataFrame + Row │ Resource Type Total Capacity + │ Any Any + ─────┼─────────────────────────────────────────────── + 1 │ MA_natural_gas_combined_cycle 4.08764 + 2 │ CT_natural_gas_combined_cycle 5.84863 + 3 │ ME_natural_gas_combined_cycle 0.887681 + 4 │ MA_solar_pv 20.0639 + 5 │ CT_onshore_wind 13.6798 + 6 │ CT_solar_pv 13.3512 + 7 │ ME_onshore_wind 9.61782 + 8 │ MA_battery 3.73538 + 9 │ CT_battery 4.85316 + 10 │ ME_battery 1.21815 + + + +![svg](./files/t7_3p_mass_load_gen.svg) + + + +## Energy Share Requirement + +Many countries have policies that demand a certain percentage of energy provided to consumers comes from renewable energy (in the US, these are called [renewable portfolio standards](https://www.eia.gov/energyexplained/renewable-sources/portfolio-standards.php#:~:text=Renewable%20portfolio%20standards%20(RPS)%2C,energy%20sources%20for%20electricity%20generation). In GenX, this policy can be implemented by setting `Energy_share_requirement` to 1 in the setup, and adding a file called `Energy_share_requirement.csv` to the policies folder. + + +```julia +ESR = CSV.read(joinpath(case,"policies/Energy_share_requirement.csv"),DataFrame,missingstring="NA") +``` + + + +```@raw html +
3×4 DataFrame
RowColumn1Network_zonesESR_1ESR_2
String3String3Float64Float64
1MAz10.2590.348
2CTz20.440.44
3MEz30.7760.776
+``` + + + +```julia +setup["CO2Cap"] = 0 # set back to 0 to compare +setup["EnergyShareRequirement"] = 1; +``` + + +```julia +inputs = GenX.load_inputs(setup, case) +EP7 = GenX.generate_model(setup,inputs,OPTIMIZER) +GenX.solve_model(EP7,setup) +``` + + Reading Input CSV Files + Network.csv Successfully Read! + Demand (load) data Successfully Read! + Fuels_data.csv Successfully Read! + + Summary of resources loaded into the model: + ------------------------------------------------------- + Resource type Number of resources + ======================================================= + Thermal 3 + VRE 4 + Storage 3 + ======================================================= + Total number of resources: 10 + ------------------------------------------------------- + Generators_variability.csv Successfully Read! + Validating time basis + Energy_share_requirement.csv Successfully Read! + CSV Files Successfully Read In From example_systems/1_three_zones + Discharge Module + + + Thermal.csv Successfully Read. + Vre.csv Successfully Read. + Storage.csv Successfully Read. + Resource_energy_share_requirement.csv Successfully Read. + Resource_capacity_reserve_margin.csv Successfully Read. + Resource_minimum_capacity_requirement.csv Successfully Read. + + + Non-served Energy Module + Investment Discharge Module + Unit Commitment Module + Fuel Module + CO2 Module + Investment Transmission Module + Transmission Module + Dispatchable Resources Module + Storage Resources Module + Storage Investment Module + Storage Core Resources Module + Storage Resources with Symmetric Charge/Discharge Capacity Module + Thermal (Unit Commitment) Resources Module + Energy Share Requirement Policies Module + Running HiGHS 1.6.0: Copyright (c) 2023 HiGHS under MIT licence terms + Presolving model + 118157 rows, 81204 cols, 433677 nonzeros + 110999 rows, 74047 cols, 428770 nonzeros + Presolve : Reductions: rows 110999(-42395); columns 74047(-46089); elements 428770(-57145) + Solving the presolved LP + IPX model has 110999 rows, 74047 columns and 428770 nonzeros + Input + Number of variables: 74047 + Number of free variables: 3696 + Number of constraints: 110999 + Number of equality constraints: 16867 + Number of matrix entries: 428770 + Matrix range: [4e-07, 1e+01] + RHS range: [7e-01, 5e+04] + Objective range: [1e-04, 4e+02] + Bounds range: [2e-03, 2e+01] + Preprocessing + Dualized model: no + Number of dense columns: 15 + Range of scaling factors: [5.00e-01, 1.00e+00] + IPX version 1.0 + Interior Point Solve + Iter P.res D.res P.obj D.obj mu Time + 0 3.25e+03 3.28e+02 2.96057527e+06 -4.42499145e+06 1.05e+06 0s + 1 2.15e+03 1.54e+02 -3.67625092e+08 -9.92129159e+06 7.04e+05 0s + 2 2.00e+03 1.45e+02 -4.48234657e+08 -1.62120314e+07 6.90e+05 0s + Constructing starting basis... + 3 1.80e+03 1.38e+02 -1.57967721e+09 -1.32554204e+07 6.35e+05 3s + 4 4.26e+02 4.25e+01 -8.07136821e+08 -1.80687324e+07 1.79e+05 4s + 5 2.74e+02 1.46e+01 -4.80957944e+08 -2.11824340e+07 8.47e+04 5s + 6 2.31e+02 1.06e+01 -3.88916132e+08 -2.25389566e+07 7.06e+04 7s + 7 3.97e+01 2.56e+00 -8.90467652e+06 -2.42140640e+07 1.74e+04 8s + 8 1.49e+00 1.05e+00 1.55238132e+07 -2.27145059e+07 4.78e+03 9s + 9 4.23e-01 9.61e-02 1.52981739e+07 -2.07731077e+07 7.27e+02 9s + 10 2.43e-02 1.03e-02 8.29803125e+06 -3.74951138e+06 1.06e+02 10s + 11 2.19e-03 2.07e-03 2.70601909e+06 -1.07157895e+06 2.38e+01 14s + 12 3.20e-04 3.41e-05 7.22109443e+05 -1.33114881e+05 4.62e+00 15s + 13 2.18e-05 5.81e-06 4.97580885e+04 -2.36873031e+04 3.94e-01 16s + 14 8.07e-06 1.45e-06 3.01069381e+04 -8.59032120e+03 2.08e-01 18s + 15 4.16e-06 6.72e-07 1.93254730e+04 -3.46407201e+03 1.22e-01 19s + 16 2.30e-06 3.55e-07 1.43100849e+04 -8.01714933e+02 8.10e-02 22s + 17 1.34e-06 1.67e-07 1.16397836e+04 8.01991381e+02 5.81e-02 25s + 18 7.61e-07 9.17e-08 9.47348310e+03 1.85600560e+03 4.08e-02 27s + 19 4.47e-07 5.75e-08 8.09797935e+03 2.54641687e+03 2.98e-02 30s + 20 3.33e-07 3.47e-08 7.62653275e+03 3.04869441e+03 2.45e-02 35s + 21 1.98e-07 1.57e-08 6.79376066e+03 3.83391059e+03 1.59e-02 40s + 22 1.00e-07 1.07e-08 6.23436569e+03 4.07366075e+03 1.16e-02 43s + 23 5.72e-08 8.56e-09 5.90537240e+03 4.22893896e+03 8.98e-03 52s + 24 3.15e-08 7.40e-09 5.69783264e+03 4.32217640e+03 7.37e-03 56s + 25 1.96e-08 6.71e-09 5.60290779e+03 4.37626021e+03 6.57e-03 60s + 26 1.62e-08 6.58e-09 5.58260471e+03 4.38624817e+03 6.41e-03 61s + 27 9.83e-09 4.68e-09 5.47138439e+03 4.58570163e+03 4.75e-03 63s + 28 7.08e-09 4.32e-09 5.43675159e+03 4.61957103e+03 4.38e-03 66s + 29 5.90e-09 3.78e-09 5.44745537e+03 4.64477569e+03 4.30e-03 68s + 30 2.60e-09 3.39e-09 5.38567861e+03 4.68523417e+03 3.75e-03 69s + 31 1.86e-09 3.13e-09 5.40360769e+03 4.70093259e+03 3.76e-03 71s + 32 5.79e-10 1.84e-09 5.32616721e+03 4.86253824e+03 2.48e-03 72s + 33 5.26e-10 1.66e-09 5.32704123e+03 4.87901733e+03 2.40e-03 74s + 34 3.48e-10 1.52e-09 5.32856456e+03 4.89218629e+03 2.34e-03 75s + 35 1.82e-10 1.07e-09 5.29562500e+03 4.96409486e+03 1.78e-03 76s + 36 1.42e-10 8.01e-10 5.28983765e+03 5.00359981e+03 1.53e-03 78s + 37 8.66e-11 7.18e-10 5.28033482e+03 5.01649890e+03 1.41e-03 79s + 38 4.00e-11 5.38e-10 5.27675684e+03 5.03862584e+03 1.28e-03 81s + 39 2.74e-11 4.02e-10 5.26154650e+03 5.07181296e+03 1.02e-03 82s + 40 1.33e-11 3.51e-10 5.26154482e+03 5.07895810e+03 9.78e-04 83s + 41 9.37e-12 1.52e-10 5.25770976e+03 5.11634640e+03 7.57e-04 84s + 42 6.61e-11 6.56e-11 5.24263265e+03 5.14509162e+03 5.23e-04 85s + 43 2.70e-11 5.14e-11 5.23939943e+03 5.15308245e+03 4.62e-04 86s + 44 1.07e-11 4.52e-11 5.23760193e+03 5.15544594e+03 4.40e-04 87s + 45 2.14e-11 3.78e-11 5.23695511e+03 5.15880466e+03 4.19e-04 88s + 46 4.52e-11 2.37e-11 5.22793680e+03 5.16829652e+03 3.20e-04 89s + 47 3.88e-11 1.68e-11 5.22613710e+03 5.17238445e+03 2.88e-04 90s + 48 3.04e-11 9.86e-12 5.22130177e+03 5.18278901e+03 2.06e-04 91s + 49 1.60e-11 8.28e-12 5.21769964e+03 5.18522708e+03 1.74e-04 91s + 50 2.50e-11 5.20e-12 5.21732077e+03 5.18949974e+03 1.49e-04 92s + 51 1.46e-11 2.94e-12 5.21565661e+03 5.19332457e+03 1.20e-04 93s + 52 6.28e-12 1.96e-12 5.21252321e+03 5.19649375e+03 8.59e-05 93s + 53 1.10e-11 1.34e-12 5.21137346e+03 5.19788591e+03 7.23e-05 94s + 54 4.52e-11 1.05e-12 5.20952040e+03 5.19902667e+03 5.62e-05 95s + 55 1.97e-13 9.66e-13 5.20946552e+03 5.19915263e+03 5.52e-05 95s + 56 1.03e-10 6.57e-13 5.20878855e+03 5.20069905e+03 4.33e-05 96s + 57 1.00e-10 7.67e-13 5.20790740e+03 5.20157745e+03 3.39e-05 97s + 58 1.08e-11 3.41e-13 5.20718489e+03 5.20231634e+03 2.61e-05 97s + 59 5.84e-11 5.04e-13 5.20697661e+03 5.20275991e+03 2.26e-05 98s + 60 6.10e-11 4.83e-13 5.20642800e+03 5.20316879e+03 1.75e-05 98s + 61 1.21e-11 2.34e-13 5.20626891e+03 5.20342248e+03 1.52e-05 99s + 62 4.88e-11 5.97e-13 5.20609848e+03 5.20403285e+03 1.11e-05 99s + 63 4.11e-11 2.27e-13 5.20583889e+03 5.20443275e+03 7.53e-06 100s + 64 4.08e-11 2.84e-13 5.20572643e+03 5.20464048e+03 5.82e-06 100s + 65 1.77e-12 2.84e-13 5.20565183e+03 5.20500872e+03 3.45e-06 101s + 66 3.26e-12 5.12e-13 5.20546080e+03 5.20514462e+03 1.69e-06 101s + 67 8.10e-11 2.27e-13 5.20539707e+03 5.20527700e+03 6.43e-07 102s + 68 1.14e-10 2.84e-13 5.20537158e+03 5.20530783e+03 3.42e-07 102s + 69 7.20e-11 5.19e-13 5.20535889e+03 5.20534226e+03 8.91e-08 102s + 70 8.66e-11 2.51e-12 5.20535373e+03 5.20534725e+03 3.47e-08 103s + 71 1.74e-11 3.55e-12 5.20534982e+03 5.20534876e+03 5.66e-09 103s + 72 7.05e-12 3.38e-12 5.20534916e+03 5.20534900e+03 8.61e-10 104s + 73* 3.63e-13 3.95e-12 5.20534910e+03 5.20534906e+03 1.93e-10 104s + 74* 2.57e-11 7.55e-12 5.20534908e+03 5.20534907e+03 3.92e-11 105s + 75* 9.13e-11 2.43e-12 5.20534908e+03 5.20534908e+03 5.53e-12 105s + Running crossover as requested + Primal residual before push phase: 4.25e-06 + Dual residual before push phase: 1.04e-06 + Number of dual pushes required: 31721 + Number of primal pushes required: 2358 + Summary + Runtime: 105.88s + Status interior point solve: optimal + Status crossover: optimal + objective value: 5.20534908e+03 + interior solution primal residual (abs/rel): 1.25e-10 / 2.61e-15 + interior solution dual residual (abs/rel): 4.75e-09 / 1.18e-11 + interior solution objective gap (abs/rel): 1.03e-06 / 1.98e-10 + basic solution primal infeasibility: 2.45e-14 + basic solution dual infeasibility: 2.50e-16 + Ipx: IPM optimal + Ipx: Crossover optimal + Solving the original LP from the solution after postsolve + Model status : Optimal + IPM iterations: 75 + Crossover iterations: 2950 + Objective value : 5.2053490785e+03 + HiGHS run time : 106.12 + LP solved for primal + + + + + + (A JuMP Model + Minimization problem with: + Variables: 120136 + Objective function type: AffExpr + `AffExpr`-in-`MathOptInterface.EqualTo{Float64}`: 35112 constraints + `AffExpr`-in-`MathOptInterface.GreaterThan{Float64}`: 20333 constraints + `AffExpr`-in-`MathOptInterface.LessThan{Float64}`: 97949 constraints + `VariableRef`-in-`MathOptInterface.EqualTo{Float64}`: 1 constraint + `VariableRef`-in-`MathOptInterface.GreaterThan{Float64}`: 116439 constraints + Model mode: AUTOMATIC + CachingOptimizer state: ATTACHED_OPTIMIZER + Solver name: HiGHS + Names registered in the model: FuelCalculationCommit_single, cESRShare, cFuelCalculation_single, cMaxCap, cMaxCapEnergy, cMaxCapEnergyDuration, cMaxFlow_in, cMaxFlow_out, cMaxLineReinforcement, cMaxNSE, cMaxRetCommit, cMaxRetEnergy, cMaxRetNoCommit, cMinCap, cMinCapEnergy, cMinCapEnergyDuration, cNSEPerSeg, cPowerBalance, cSoCBalInterior, cSoCBalStart, cStartFuel_single, cTAuxLimit, cTAuxSum, cTLoss, eAvail_Trans_Cap, eCFix, eCFixEnergy, eCFuelOut, eCFuelStart, eCNSE, eCStart, eCVar_in, eCVar_out, eELOSS, eELOSSByZone, eESR, eESRDischarge, eEmissionsByPlant, eEmissionsByZone, eExistingCap, eExistingCapEnergy, eFuelConsumption, eFuelConsumptionYear, eFuelConsumption_single, eGenerationByThermAll, eGenerationByVRE, eGenerationByZone, eLosses_By_Zone, eNet_Export_Flows, eObj, ePlantCFuelOut, ePlantCFuelStart, ePlantFuel_generation, ePlantFuel_start, ePowerBalance, ePowerBalanceDisp, ePowerBalanceLossesByZone, ePowerBalanceNetExportFlows, ePowerBalanceNse, ePowerBalanceStor, ePowerBalanceThermCommit, eStartFuel, eTotalCFix, eTotalCFixEnergy, eTotalCFuelOut, eTotalCFuelStart, eTotalCNSE, eTotalCNSET, eTotalCNSETS, eTotalCNetworkExp, eTotalCStart, eTotalCStartT, eTotalCVarIn, eTotalCVarInT, eTotalCVarOut, eTotalCVarOutT, eTotalCap, eTotalCapEnergy, eTransMax, eZonalCFuelOut, eZonalCFuelStart, vCAP, vCAPENERGY, vCHARGE, vCOMMIT, vFLOW, vFuel, vNEW_TRANS_CAP, vNSE, vP, vRETCAP, vRETCAPENERGY, vS, vSHUT, vSTART, vStartFuel, vTAUX_NEG, vTAUX_POS, vTLOSS, vZERO, 106.45424795150757) + + + + +```julia +totCap7 = value.(EP7[:eTotalCap]) + +totCapB7 = [totCap7[1] + totCap7[2] + totCap7[3], totCap7[4] + totCap7[6], + totCap7[5] + totCap7[7], totCap7[8] + totCap7[9] + totCap7[10]] + +println(DataFrame([RT totCap7],["Resource Type","Total Capacity"])) +println(" ") + +G7 = groupedbar(transpose(totCapB7), bar_position = :stack, bar_width=.7,size=(500,450), xticks=[ ],ylabel="GW", + labels=["Natural Gas" "Solar" "Wind" "Battery"],legend=false,color=colors, + title="Energy Share Requirement \n Obj Val: $(round(objective_value(EP7),digits=6))",ylabelfontsize=8) +plot(G7,G2,G5,G6,size=(900,900), titlefontsize=8,layout=(2,2)) +``` + + 10×2 DataFrame + Row │ Resource Type Total Capacity + │ Any Any + ─────┼─────────────────────────────────────────────── + 1 │ MA_natural_gas_combined_cycle 8.58778 + 2 │ CT_natural_gas_combined_cycle 9.43521 + 3 │ ME_natural_gas_combined_cycle 0.0 + 4 │ MA_solar_pv 2.99333 + 5 │ CT_onshore_wind 5.61694 + 6 │ CT_solar_pv 0.242229 + 7 │ ME_onshore_wind 6.28897 + 8 │ MA_battery 0.253804 + 9 │ CT_battery 0.0 + 10 │ ME_battery 0.723348 + + + +![svg](./files/t7_4p_esr_mass_load_gen.svg) + + +The Energy Share Requriement policy also has the possibiliy to be run with slack variables + +## Capacity Reserve Margin + +The Capacity Reserve Margin constraint demands that a certain amount of energy always be available in each zone, expressed as a fraction of the demand. Once again, we can enforce a Capacity Reserve Margin by setting its option to "1" in the setup and adding the relevant file, `Capacity_reserve_margin.csv`. + + +```julia +CapacityReserve = CSV.read(joinpath(case,"policies/Capacity_reserve_margin.csv"),DataFrame,missingstring="NA") +``` + + + +``` @raw html +
3×3 DataFrame
RowColumn1Network_zonesCapRes_1
String3String3Float64
1MAz10.156
2CTz20.156
3MEz30.156
+``` + + + +```julia +setup["CapacityReserveMargin"] = 1; +``` + + +```julia +inputs = GenX.load_inputs(setup, case) +EP8 = GenX.generate_model(setup,inputs,OPTIMIZER) +GenX.solve_model(EP8,setup) +``` + + Reading Input CSV Files + Network.csv Successfully Read! + Demand (load) data Successfully Read! + Fuels_data.csv Successfully Read! + + Summary of resources loaded into the model: + ------------------------------------------------------- + Resource type Number of resources + ======================================================= + Thermal 3 + VRE 4 + Storage 3 + ======================================================= + Total number of resources: 10 + ------------------------------------------------------- + Generators_variability.csv Successfully Read! + Validating time basis + Capacity_reserve_margin.csv Successfully Read! + + + Thermal.csv Successfully Read. + Vre.csv Successfully Read. + Storage.csv Successfully Read. + Resource_energy_share_requirement.csv Successfully Read. + Resource_capacity_reserve_margin.csv Successfully Read. + Resource_minimum_capacity_requirement.csv Successfully Read. + + + Energy_share_requirement.csv Successfully Read! + CSV Files Successfully Read In From example_systems/1_three_zones + Discharge Module + Non-served Energy Module + Investment Discharge Module + Unit Commitment Module + Fuel Module + CO2 Module + Investment Transmission Module + Transmission Module + Dispatchable Resources Module + Storage Resources Module + Storage Investment Module + Storage Core Resources Module + Storage Resources with Symmetric Charge/Discharge Capacity Module + Thermal (Unit Commitment) Resources Module + Energy Share Requirement Policies Module + Capacity Reserve Margin Policies Module + Running HiGHS 1.6.0: Copyright (c) 2023 HiGHS under MIT licence terms + Presolving model + 131093 rows, 97836 cols, 538890 nonzeros + 123634 rows, 90378 cols, 543137 nonzeros + Presolve : Reductions: rows 123634(-42696); columns 90378(-46390); elements 543137(-47991) + Solving the presolved LP + IPX model has 123634 rows, 90378 columns and 543137 nonzeros + Input + Number of variables: 90378 + Number of free variables: 3696 + Number of constraints: 123634 + Number of equality constraints: 22110 + Number of matrix entries: 543137 + Matrix range: [4e-07, 1e+01] + RHS range: [7e-01, 5e+04] + Objective range: [1e-04, 4e+02] + Bounds range: [2e-03, 2e+01] + Preprocessing + Dualized model: no + Number of dense columns: 15 + Range of scaling factors: [5.00e-01, 1.00e+00] + IPX version 1.0 + Interior Point Solve + Iter P.res D.res P.obj D.obj mu Time + 0 4.21e+03 3.50e+02 3.04628199e+06 -5.21772232e+06 1.46e+06 0s + 1 2.81e+03 1.90e+02 -5.15281627e+08 -1.06058690e+07 1.02e+06 0s + 2 2.45e+03 1.56e+02 -6.44386109e+08 -2.55847744e+07 9.81e+05 1s + 3 1.18e+03 1.41e+02 -8.43467846e+08 -2.60789874e+07 6.78e+05 1s + Constructing starting basis... + 4 7.54e+02 4.73e+01 -7.60379207e+08 -3.49951229e+07 3.40e+05 3s + 5 6.33e+01 1.39e+01 -5.44467100e+05 -3.65314462e+07 6.81e+04 4s + 6 2.27e+01 2.74e+00 -1.77683713e+06 -4.81835016e+07 1.90e+04 10s + 7 9.77e-01 6.10e-01 1.48697352e+07 -4.12620606e+07 3.32e+03 13s + 8 1.52e-01 4.78e-02 1.42095518e+07 -1.48990864e+07 4.03e+02 14s + 9 1.65e-02 1.88e-02 7.71351293e+06 -6.37275792e+06 1.39e+02 15s + 10 3.46e-03 2.65e-03 4.64755286e+06 -1.55341587e+06 3.68e+01 18s + 11 4.76e-04 1.28e-03 9.51929576e+05 -7.48785467e+05 8.94e+00 20s + 12 8.26e-05 6.15e-05 2.94145739e+05 -7.77919316e+04 1.78e+00 21s + 13 3.54e-05 6.20e-06 1.35212127e+05 -1.45337724e+04 7.13e-01 22s + 14 2.53e-05 7.28e-12 1.14387567e+05 -2.21312822e+04 6.50e-01 23s + 15 2.33e-05 7.28e-12 1.13339554e+05 -2.13641483e+04 6.41e-01 24s + 16 7.71e-06 4.55e-12 4.42306187e+04 -1.29028659e+04 2.72e-01 25s + 17 7.64e-07 1.82e-12 1.81970294e+04 -4.92740686e+03 1.10e-01 26s + 18 3.72e-07 3.64e-12 1.39021979e+04 -1.32749886e+03 7.24e-02 30s + 19 2.03e-07 9.09e-13 1.13821414e+04 7.45236578e+02 5.06e-02 32s + 20 1.05e-07 9.09e-13 9.13639056e+03 2.30432056e+03 3.25e-02 35s + 21 7.73e-08 4.55e-13 8.59719038e+03 3.17398498e+03 2.58e-02 38s + 22 3.33e-08 3.69e-13 7.37618406e+03 3.92635014e+03 1.64e-02 43s + 23 1.53e-08 9.09e-13 6.71318380e+03 4.46082497e+03 1.07e-02 52s + 24 7.16e-09 2.98e-13 6.28228488e+03 4.71698031e+03 7.44e-03 56s + 25 5.19e-09 9.09e-13 6.18110316e+03 4.82937601e+03 6.43e-03 62s + 26 2.24e-09 4.55e-13 5.92131571e+03 5.04272325e+03 4.18e-03 66s + 27 1.26e-09 4.55e-13 5.85499040e+03 5.13102116e+03 3.44e-03 73s + 28 5.94e-10 4.55e-13 5.79725408e+03 5.18715779e+03 2.90e-03 76s + 29 3.86e-10 4.83e-13 5.76431193e+03 5.26258597e+03 2.38e-03 79s + 30 2.36e-10 3.69e-13 5.74367493e+03 5.28432035e+03 2.18e-03 81s + 31 1.68e-10 2.56e-13 5.72937706e+03 5.32860855e+03 1.90e-03 83s + 32 6.05e-11 2.27e-13 5.70998585e+03 5.35418213e+03 1.69e-03 85s + 33 5.53e-11 2.42e-13 5.70850651e+03 5.40373271e+03 1.45e-03 86s + 34 2.90e-11 2.42e-13 5.68596077e+03 5.43998304e+03 1.17e-03 88s + 35 2.67e-11 4.26e-13 5.67637386e+03 5.45111176e+03 1.07e-03 91s + 36 2.99e-11 2.27e-13 5.67168974e+03 5.46214213e+03 9.96e-04 92s + 37 1.99e-11 1.42e-13 5.65940618e+03 5.48204771e+03 8.43e-04 93s + 38 6.70e-11 4.55e-13 5.65532213e+03 5.49686649e+03 7.53e-04 94s + 39 6.23e-11 3.13e-13 5.64168791e+03 5.51202371e+03 6.16e-04 95s + 40 3.89e-11 3.27e-13 5.63853210e+03 5.52780109e+03 5.26e-04 97s + 41 1.20e-11 1.99e-13 5.63441684e+03 5.54194604e+03 4.40e-04 98s + 42 1.79e-11 4.55e-13 5.63069684e+03 5.54896357e+03 3.88e-04 100s + 43 2.83e-11 2.42e-13 5.62970471e+03 5.55078636e+03 3.75e-04 101s + 44 7.99e-11 5.40e-13 5.61992128e+03 5.55549414e+03 3.06e-04 103s + 45 3.20e-11 2.56e-13 5.61211218e+03 5.56079764e+03 2.44e-04 104s + 46 1.21e-11 3.41e-13 5.61177375e+03 5.56505674e+03 2.22e-04 106s + 47 4.72e-11 2.56e-13 5.60993414e+03 5.56837792e+03 1.98e-04 107s + 48 4.70e-12 3.41e-13 5.60465748e+03 5.57191765e+03 1.56e-04 108s + 49 2.44e-11 3.69e-13 5.60465096e+03 5.57377173e+03 1.47e-04 109s + 50 2.47e-11 3.41e-13 5.60235807e+03 5.58002161e+03 1.06e-04 109s + 51 3.24e-11 2.56e-13 5.60149552e+03 5.58255142e+03 9.00e-05 110s + 52 3.31e-11 1.71e-13 5.60101493e+03 5.58433865e+03 7.93e-05 111s + 53 2.00e-11 3.13e-13 5.59892601e+03 5.58668138e+03 5.82e-05 112s + 54 7.84e-11 1.60e-13 5.59885342e+03 5.58685974e+03 5.70e-05 112s + 55 4.61e-11 2.27e-13 5.59865140e+03 5.58847015e+03 4.84e-05 113s + 56 7.16e-11 2.56e-13 5.59762653e+03 5.58975937e+03 3.74e-05 114s + 57 1.03e-11 2.56e-13 5.59747335e+03 5.59024473e+03 3.44e-05 115s + 58 7.64e-12 3.13e-13 5.59715338e+03 5.59061047e+03 3.11e-05 115s + 59 2.17e-11 3.13e-13 5.59711186e+03 5.59070395e+03 3.05e-05 116s + 60 1.40e-11 2.27e-13 5.59696797e+03 5.59107740e+03 2.80e-05 117s + 61 7.00e-11 4.26e-13 5.59606722e+03 5.59162281e+03 2.11e-05 117s + 62 4.94e-11 6.25e-13 5.59525601e+03 5.59289962e+03 1.12e-05 118s + 63 1.92e-11 2.27e-13 5.59505259e+03 5.59297104e+03 9.89e-06 119s + 64 1.11e-11 3.69e-13 5.59462252e+03 5.59327739e+03 6.39e-06 120s + 65 8.66e-13 3.13e-13 5.59453155e+03 5.59338209e+03 5.46e-06 120s + 66 2.17e-11 3.13e-13 5.59446120e+03 5.59350884e+03 4.53e-06 121s + 67 1.12e-11 3.13e-13 5.59440854e+03 5.59367478e+03 3.49e-06 122s + 68 4.11e-11 3.98e-13 5.59427052e+03 5.59378913e+03 2.29e-06 122s + 69 1.04e-11 2.27e-13 5.59416188e+03 5.59391997e+03 1.15e-06 123s + 70 4.31e-12 2.56e-13 5.59412273e+03 5.59393435e+03 8.95e-07 124s + 71 8.96e-11 3.98e-13 5.59408396e+03 5.59399407e+03 4.27e-07 124s + 72 1.26e-10 4.26e-13 5.59405507e+03 5.59402936e+03 1.22e-07 125s + 73 3.69e-11 3.98e-13 5.59405082e+03 5.59403909e+03 5.58e-08 126s + 74 1.24e-12 2.36e-12 5.59404612e+03 5.59404129e+03 2.30e-08 126s + 75 1.33e-11 5.07e-12 5.59404352e+03 5.59404265e+03 4.10e-09 127s + 76 1.91e-11 4.70e-12 5.59404309e+03 5.59404298e+03 5.09e-10 127s + 77* 6.85e-11 7.82e-12 5.59404305e+03 5.59404303e+03 1.27e-10 128s + 78* 6.30e-11 2.17e-12 5.59404305e+03 5.59404304e+03 2.84e-11 128s + 79* 1.53e-11 1.32e-12 5.59404305e+03 5.59404304e+03 6.24e-12 129s + Running crossover as requested + Primal residual before push phase: 1.85e-05 + Dual residual before push phase: 1.60e-06 + Number of dual pushes required: 40477 + Number of primal pushes required: 2267 + Summary + Runtime: 129.76s + Status interior point solve: optimal + Status crossover: optimal + objective value: 5.59404305e+03 + interior solution primal residual (abs/rel): 4.70e-09 / 9.83e-14 + interior solution dual residual (abs/rel): 6.10e-09 / 1.52e-11 + interior solution objective gap (abs/rel): 1.29e-06 / 2.30e-10 + basic solution primal infeasibility: 3.88e-14 + basic solution dual infeasibility: 1.21e-15 + Ipx: IPM optimal + Ipx: Crossover optimal + Solving the original LP from the solution after postsolve + Model status : Optimal + IPM iterations: 79 + Crossover iterations: 5019 + Objective value : 5.5940430448e+03 + HiGHS run time : 130.05 + LP solved for primal + + + + + + (A JuMP Model + Minimization problem with: + Variables: 136768 + Objective function type: AffExpr + `AffExpr`-in-`MathOptInterface.EqualTo{Float64}`: 40656 constraints + `AffExpr`-in-`MathOptInterface.GreaterThan{Float64}`: 27725 constraints + `AffExpr`-in-`MathOptInterface.LessThan{Float64}`: 97949 constraints + `VariableRef`-in-`MathOptInterface.EqualTo{Float64}`: 1 constraint + `VariableRef`-in-`MathOptInterface.GreaterThan{Float64}`: 133071 constraints + Model mode: AUTOMATIC + CachingOptimizer state: ATTACHED_OPTIMIZER + Solver name: HiGHS + Names registered in the model: FuelCalculationCommit_single, cCapacityResMargin, cESRShare, cFuelCalculation_single, cMaxCap, cMaxCapEnergy, cMaxCapEnergyDuration, cMaxFlow_in, cMaxFlow_out, cMaxLineReinforcement, cMaxNSE, cMaxRetCommit, cMaxRetEnergy, cMaxRetNoCommit, cMinCap, cMinCapEnergy, cMinCapEnergyDuration, cNSEPerSeg, cPowerBalance, cSOCMinCapRes, cSoCBalInterior, cSoCBalStart, cStartFuel_single, cTAuxLimit, cTAuxSum, cTLoss, cVSoCBalInterior, cVSoCBalStart, eAvail_Trans_Cap, eCFix, eCFixEnergy, eCFuelOut, eCFuelStart, eCNSE, eCStart, eCVar_in, eCVar_in_virtual, eCVar_out, eCVar_out_virtual, eCapResMarBalance, eCapResMarBalanceNSE, eCapResMarBalanceStor, eCapResMarBalanceStorVirtual, eCapResMarBalanceThermal, eCapResMarBalanceTrans, eCapResMarBalanceVRE, eELOSS, eELOSSByZone, eESR, eESRDischarge, eEmissionsByPlant, eEmissionsByZone, eExistingCap, eExistingCapEnergy, eFuelConsumption, eFuelConsumptionYear, eFuelConsumption_single, eGenerationByThermAll, eGenerationByVRE, eGenerationByZone, eLosses_By_Zone, eNet_Export_Flows, eObj, ePlantCFuelOut, ePlantCFuelStart, ePlantFuel_generation, ePlantFuel_start, ePowerBalance, ePowerBalanceDisp, ePowerBalanceLossesByZone, ePowerBalanceNetExportFlows, ePowerBalanceNse, ePowerBalanceStor, ePowerBalanceThermCommit, eStartFuel, eTotalCFix, eTotalCFixEnergy, eTotalCFuelOut, eTotalCFuelStart, eTotalCNSE, eTotalCNSET, eTotalCNSETS, eTotalCNetworkExp, eTotalCStart, eTotalCStartT, eTotalCVarIn, eTotalCVarInT, eTotalCVarInT_virtual, eTotalCVarIn_virtual, eTotalCVarOut, eTotalCVarOutT, eTotalCVarOutT_virtual, eTotalCVarOut_virtual, eTotalCap, eTotalCapEnergy, eTransMax, eZonalCFuelOut, eZonalCFuelStart, vCAP, vCAPENERGY, vCAPRES_charge, vCAPRES_discharge, vCAPRES_socinreserve, vCHARGE, vCOMMIT, vFLOW, vFuel, vNEW_TRANS_CAP, vNSE, vP, vRETCAP, vRETCAPENERGY, vS, vSHUT, vSTART, vStartFuel, vTAUX_NEG, vTAUX_POS, vTLOSS, vZERO, 130.46769380569458) + + + + +```julia +totCap8 = value.(EP8[:eTotalCap]) + +totCapB8 = [totCap8[1] + totCap8[2] + totCap8[3], totCap8[4] + totCap8[6], + totCap8[5] + totCap8[7], totCap8[8] + totCap8[9] + totCap8[10]] + +println(DataFrame([RT totCap8],["Resource Type","Total Capacity"])) +println(" ") + +G8 = groupedbar(transpose(totCapB8), bar_position = :stack, bar_width=.7,size=(500,450), xticks=[ ],ylabel="GW", + labels=["Natural Gas" "Solar" "Wind" "Battery"],color=colors, + title="Capacity Reserve Margin + ESR \n Obj Val: $(round(objective_value(EP8),digits=6))",ylabelfontsize=8) + +plot(G8,G7,G2,size=(900,450), titlefontsize=8,layout=(1,3)) +``` + + 10×2 DataFrame + Row │ Resource Type Total Capacity + │ Any Any + ─────┼─────────────────────────────────────────────── + 1 │ MA_natural_gas_combined_cycle 8.59939 + 2 │ CT_natural_gas_combined_cycle 14.7124 + 3 │ ME_natural_gas_combined_cycle 0.0 + 4 │ MA_solar_pv 2.89028 + 5 │ CT_onshore_wind 5.82195 + 6 │ CT_solar_pv 0.0 + 7 │ ME_onshore_wind 6.28764 + 8 │ MA_battery 0.346698 + 9 │ CT_battery 0.0 + 10 │ ME_battery 0.586507 + + + + +![svg](./files/t7_3p_csm_esr_mass.svg) + + + +Capacity Reserve Margin also has the possibiliy to be run with slack variables. + +## Minimum Capacity Requirement + +The last policy we'll talk about is [Minimum Capacity Requirement], which requires the grid to produce at least a certain amount of energy from renewables, as specified in the input file: + + +```julia +MinCapacity = CSV.read(joinpath(case,"policies/Minimum_capacity_requirement.csv"),DataFrame,missingstring="NA") +``` + + + +```@raw html +
3×3 DataFrame
RowMinCapReqConstraintConstraintDescriptionMin_MW
Int64String15Int64
11MA_PV5000
22CT_Wind10000
33All_Batteries6000
+``` + + +This policy ensures some renewable energy is used in the grid regardless of emissions constraints. If a fourth column containing price cap requirements exists (not shown above), that column is treated as a slack variable. + + +```julia +setup["CapacityReserveMargin"] = 0 +setup["EnergyShareRequirement"] = 0 +setup["MinCapReq"] = 1; +``` + + +```julia +inputs = GenX.load_inputs(setup, case) +EP9 = GenX.generate_model(setup,inputs,OPTIMIZER) +GenX.solve_model(EP9,setup) +``` + + Reading Input CSV Files + Network.csv Successfully Read! + Demand (load) data Successfully Read! + Fuels_data.csv Successfully Read! + + Summary of resources loaded into the model: + ------------------------------------------------------- + Resource type Number of resources + ======================================================= + Thermal 3 + VRE 4 + Storage 3 + ======================================================= + Total number of resources: 10 + ------------------------------------------------------- + Generators_variability.csv Successfully Read! + Validating time basis + Minimum_capacity_requirement.csv Successfully Read! + CSV Files Successfully Read In From example_systems/1_three_zones + Discharge Module + Non-served Energy Module + + + Thermal.csv Successfully Read. + Vre.csv Successfully Read. + Storage.csv Successfully Read. + Resource_energy_share_requirement.csv Successfully Read. + Resource_capacity_reserve_margin.csv Successfully Read. + Resource_minimum_capacity_requirement.csv Successfully Read. + + + Investment Discharge Module + Unit Commitment Module + Fuel Module + CO2 Module + Investment Transmission Module + Transmission Module + Dispatchable Resources Module + Storage Resources Module + Storage Investment Module + Storage Core Resources Module + Storage Resources with Symmetric Charge/Discharge Capacity Module + Thermal (Unit Commitment) Resources Module + Minimum Capacity Requirement Module + Running HiGHS 1.6.0: Copyright (c) 2023 HiGHS under MIT licence terms + Presolving model + 118156 rows, 81204 cols, 422838 nonzeros + 110999 rows, 74047 cols, 423352 nonzeros + Presolve : Reductions: rows 110999(-42396); columns 74047(-46089); elements 423352(-47784) + Solving the presolved LP + IPX model has 110999 rows, 74047 columns and 423352 nonzeros + Input + Number of variables: 74047 + Number of free variables: 3696 + Number of constraints: 110999 + Number of equality constraints: 16867 + Number of matrix entries: 423352 + Matrix range: [4e-07, 1e+01] + RHS range: [7e-01, 2e+01] + Objective range: [1e-04, 4e+02] + Bounds range: [2e-03, 2e+01] + Preprocessing + Dualized model: no + Number of dense columns: 15 + Range of scaling factors: [5.00e-01, 1.00e+00] + IPX version 1.0 + Interior Point Solve + Iter P.res D.res P.obj D.obj mu Time + 0 2.34e+01 3.82e+02 3.30488922e+06 -5.33223155e+06 8.91e+03 0s + 1 1.39e+01 1.13e+02 -3.26111454e+06 -8.08059083e+06 4.78e+03 0s + 2 1.34e+01 8.13e+01 -3.30809039e+06 -2.14818717e+07 5.04e+03 0s + 3 5.10e+00 4.43e+01 -3.63727490e+06 -2.49670027e+07 2.64e+03 1s + Constructing starting basis... + 4 2.75e+00 1.66e+01 -1.55659888e+06 -2.27832740e+07 1.26e+03 3s + 5 2.41e+00 1.37e+01 -1.18396821e+06 -2.13578247e+07 1.11e+03 4s + 6 1.39e+00 4.43e+00 -4.94217397e+04 -1.44985483e+07 5.28e+02 6s + 7 6.13e-02 7.91e-01 8.34862433e+05 -6.93418288e+06 9.35e+01 7s + 8 3.76e-03 7.97e-02 4.24408301e+05 -1.09263439e+06 1.31e+01 8s + 9 2.88e-04 1.00e-02 1.65630895e+05 -1.75207574e+05 2.37e+00 9s + 10 6.57e-05 2.60e-03 6.19697724e+04 -5.01450660e+04 6.76e-01 10s + 11 3.78e-05 3.11e-04 4.59001513e+04 -1.47138829e+04 3.33e-01 10s + 12 2.79e-05 3.01e-04 5.07134714e+04 -1.34380343e+04 3.51e-01 11s + 13 1.85e-05 3.28e-05 3.85580458e+04 -4.46651304e+03 2.31e-01 12s + 14 1.19e-05 1.47e-05 3.43652640e+04 -5.85635923e+03 2.16e-01 13s + 15 5.83e-06 5.94e-06 2.04363590e+04 -8.78181923e+02 1.14e-01 13s + 16 3.62e-06 3.07e-06 1.57058304e+04 7.47097166e+02 8.02e-02 15s + 17 1.73e-06 1.98e-06 1.14474070e+04 1.51572830e+03 5.33e-02 18s + 18 7.85e-07 1.23e-06 8.92169592e+03 2.32540240e+03 3.54e-02 19s + 19 4.32e-07 8.80e-07 7.80843740e+03 2.81479174e+03 2.68e-02 20s + 20 2.62e-07 4.48e-07 7.20548085e+03 3.52684341e+03 1.97e-02 22s + 21 1.47e-07 2.24e-07 6.67664231e+03 4.07755176e+03 1.39e-02 25s + 22 6.82e-08 1.82e-07 6.25539980e+03 4.21836942e+03 1.09e-02 28s + 23 5.27e-08 1.49e-07 6.14225406e+03 4.35222081e+03 9.59e-03 30s + 24 5.23e-08 1.48e-07 6.19401540e+03 4.41443087e+03 9.54e-03 32s + 25 4.08e-08 1.37e-07 6.11156615e+03 4.46928744e+03 8.80e-03 34s + 26 2.62e-08 1.04e-07 5.99485482e+03 4.65688588e+03 7.17e-03 35s + 27 1.01e-08 5.40e-08 5.91238184e+03 4.88375941e+03 5.51e-03 36s + 28 9.25e-09 4.67e-08 5.88906267e+03 4.95194014e+03 5.02e-03 39s + 29 8.47e-09 4.18e-08 5.87830260e+03 4.97446884e+03 4.84e-03 42s + 30 2.90e-09 3.49e-08 5.71712195e+03 5.04256956e+03 3.61e-03 45s + 31 1.58e-09 1.80e-08 5.66248175e+03 5.22792362e+03 2.33e-03 50s + 32 9.36e-10 1.16e-08 5.62108661e+03 5.31620530e+03 1.63e-03 53s + 33 7.54e-10 9.50e-09 5.61131446e+03 5.34069905e+03 1.45e-03 56s + 34 3.64e-10 6.78e-09 5.58350503e+03 5.37914296e+03 1.09e-03 57s + 35 1.61e-10 4.60e-09 5.57381755e+03 5.40507612e+03 9.04e-04 58s + 36 9.58e-11 3.34e-09 5.55921197e+03 5.43192782e+03 6.82e-04 60s + 37 5.96e-11 1.90e-09 5.55589825e+03 5.45455261e+03 5.43e-04 62s + 38 3.27e-11 6.28e-10 5.54709869e+03 5.48816147e+03 3.16e-04 63s + 39 2.12e-11 4.02e-10 5.54216424e+03 5.49586924e+03 2.48e-04 64s + 40 1.40e-11 2.87e-10 5.53737841e+03 5.50195705e+03 1.90e-04 64s + 41 8.74e-12 7.64e-11 5.53451883e+03 5.51121430e+03 1.25e-04 65s + 42 4.45e-12 5.82e-11 5.53082515e+03 5.51293739e+03 9.58e-05 65s + 43 2.69e-12 4.91e-11 5.52895320e+03 5.51381040e+03 8.11e-05 66s + 44 1.49e-12 3.09e-11 5.52752865e+03 5.51632502e+03 6.00e-05 66s + 45 1.12e-12 2.18e-11 5.52678261e+03 5.51817779e+03 4.61e-05 66s + 46 9.73e-13 1.64e-11 5.52653183e+03 5.51873543e+03 4.18e-05 67s + 47 6.75e-13 1.09e-11 5.52588469e+03 5.51976155e+03 3.28e-05 67s + 48 5.12e-13 7.28e-12 5.52538622e+03 5.52088538e+03 2.41e-05 68s + 49 3.62e-13 7.28e-12 5.52505789e+03 5.52116706e+03 2.08e-05 68s + 50 1.21e-13 1.46e-11 5.52401306e+03 5.52162795e+03 1.28e-05 68s + 51 7.82e-14 7.28e-12 5.52389039e+03 5.52195984e+03 1.03e-05 69s + 52 4.97e-14 7.28e-12 5.52369651e+03 5.52214739e+03 8.30e-06 69s + 53 2.13e-14 7.28e-12 5.52355424e+03 5.52243013e+03 6.02e-06 69s + 54 2.13e-14 7.28e-12 5.52339956e+03 5.52263709e+03 4.08e-06 70s + 55 2.13e-14 7.28e-12 5.52340007e+03 5.52266984e+03 3.91e-06 70s + 56 2.84e-14 7.28e-12 5.52339771e+03 5.52268533e+03 3.82e-06 70s + 57 2.13e-14 7.28e-12 5.52333480e+03 5.52273001e+03 3.24e-06 71s + 58 2.13e-14 7.28e-12 5.52333465e+03 5.52276044e+03 3.08e-06 71s + 59 2.13e-14 7.28e-12 5.52331312e+03 5.52282782e+03 2.60e-06 71s + 60 2.13e-14 7.28e-12 5.52329636e+03 5.52284551e+03 2.42e-06 72s + 61 2.13e-14 7.28e-12 5.52328626e+03 5.52292698e+03 1.92e-06 72s + 62 2.13e-14 7.28e-12 5.52328402e+03 5.52294138e+03 1.84e-06 72s + 63 2.13e-14 1.92e-13 5.52322974e+03 5.52307280e+03 8.41e-07 73s + 64 2.84e-14 7.28e-12 5.52320719e+03 5.52313866e+03 3.67e-07 73s + 65 2.84e-14 1.17e-13 5.52319028e+03 5.52316431e+03 1.39e-07 73s + 66 2.13e-14 1.82e-12 5.52318739e+03 5.52317535e+03 6.45e-08 73s + 67 2.13e-14 4.55e-13 5.52318545e+03 5.52317686e+03 4.60e-08 74s + 68 2.13e-14 7.28e-12 5.52318141e+03 5.52317952e+03 1.01e-08 74s + 69 2.84e-14 7.28e-12 5.52318051e+03 5.52318024e+03 1.41e-09 74s + 70* 2.13e-14 6.90e-12 5.52318041e+03 5.52318036e+03 2.41e-10 74s + 71* 2.84e-14 4.07e-12 5.52318040e+03 5.52318040e+03 3.09e-11 75s + 72* 3.55e-14 5.53e-12 5.52318040e+03 5.52318040e+03 5.06e-12 75s + 73* 4.26e-14 7.88e-12 5.52318040e+03 5.52318040e+03 6.38e-13 75s + 74* 3.55e-14 2.81e-12 5.52318040e+03 5.52318040e+03 1.07e-13 75s + Running crossover as requested + Primal residual before push phase: 2.03e-08 + Dual residual before push phase: 5.16e-09 + Number of dual pushes required: 26618 + Number of primal pushes required: 2605 + Summary + Runtime: 75.26s + Status interior point solve: optimal + Status crossover: optimal + objective value: 5.52318040e+03 + interior solution primal residual (abs/rel): 7.73e-11 / 4.73e-12 + interior solution dual residual (abs/rel): 4.28e-10 / 1.07e-12 + interior solution objective gap (abs/rel): 9.06e-09 / 1.64e-12 + basic solution primal infeasibility: 6.22e-15 + basic solution dual infeasibility: 3.25e-15 + Ipx: IPM optimal + Ipx: Crossover optimal + Solving the original LP from the solution after postsolve + Model status : Optimal + IPM iterations: 74 + Crossover iterations: 2077 + Objective value : 5.5231804025e+03 + HiGHS run time : 75.47 + LP solved for primal + + + + + + (A JuMP Model + Minimization problem with: + Variables: 120136 + Objective function type: AffExpr + `AffExpr`-in-`MathOptInterface.EqualTo{Float64}`: 35112 constraints + `AffExpr`-in-`MathOptInterface.GreaterThan{Float64}`: 20334 constraints + `AffExpr`-in-`MathOptInterface.LessThan{Float64}`: 97949 constraints + `VariableRef`-in-`MathOptInterface.EqualTo{Float64}`: 1 constraint + `VariableRef`-in-`MathOptInterface.GreaterThan{Float64}`: 116439 constraints + Model mode: AUTOMATIC + CachingOptimizer state: ATTACHED_OPTIMIZER + Solver name: HiGHS + Names registered in the model: FuelCalculationCommit_single, cFuelCalculation_single, cMaxCap, cMaxCapEnergy, cMaxCapEnergyDuration, cMaxFlow_in, cMaxFlow_out, cMaxLineReinforcement, cMaxNSE, cMaxRetCommit, cMaxRetEnergy, cMaxRetNoCommit, cMinCap, cMinCapEnergy, cMinCapEnergyDuration, cNSEPerSeg, cPowerBalance, cSoCBalInterior, cSoCBalStart, cStartFuel_single, cTAuxLimit, cTAuxSum, cTLoss, cZoneMinCapReq, eAvail_Trans_Cap, eCFix, eCFixEnergy, eCFuelOut, eCFuelStart, eCNSE, eCStart, eCVar_in, eCVar_out, eELOSS, eELOSSByZone, eEmissionsByPlant, eEmissionsByZone, eExistingCap, eExistingCapEnergy, eFuelConsumption, eFuelConsumptionYear, eFuelConsumption_single, eGenerationByThermAll, eGenerationByVRE, eGenerationByZone, eLosses_By_Zone, eMinCapRes, eMinCapResInvest, eNet_Export_Flows, eObj, ePlantCFuelOut, ePlantCFuelStart, ePlantFuel_generation, ePlantFuel_start, ePowerBalance, ePowerBalanceDisp, ePowerBalanceLossesByZone, ePowerBalanceNetExportFlows, ePowerBalanceNse, ePowerBalanceStor, ePowerBalanceThermCommit, eStartFuel, eTotalCFix, eTotalCFixEnergy, eTotalCFuelOut, eTotalCFuelStart, eTotalCNSE, eTotalCNSET, eTotalCNSETS, eTotalCNetworkExp, eTotalCStart, eTotalCStartT, eTotalCVarIn, eTotalCVarInT, eTotalCVarOut, eTotalCVarOutT, eTotalCap, eTotalCapEnergy, eTransMax, eZonalCFuelOut, eZonalCFuelStart, vCAP, vCAPENERGY, vCHARGE, vCOMMIT, vFLOW, vFuel, vNEW_TRANS_CAP, vNSE, vP, vRETCAP, vRETCAPENERGY, vS, vSHUT, vSTART, vStartFuel, vTAUX_NEG, vTAUX_POS, vTLOSS, vZERO, 75.7959840297699) + + + + +```julia +totCap9 = value.(EP9[:eTotalCap]) + +totCapB9 = [totCap9[1] + totCap9[2] + totCap9[3], totCap9[4] + totCap9[6], + totCap9[5] + totCap9[7], totCap9[8] + totCap9[9] + totCap9[10]] + +println(DataFrame([RT totCap9],["Resource Type","Total Capacity"])) +println(" ") + +G9 = groupedbar(transpose(totCapB9), bar_position = :stack, bar_width=.7,size=(500,450), xticks=[ ],ylabel="GW", + labels=["Natural Gas" "Solar" "Wind" "Battery"],legend=false,color=colors, + title="Minimum Capacity Requirement \n Obj Val: $(round(objective_value(EP9),digits=6))",ylabelfontsize=8) + +plot(G9,G8,G7,G2,size=(900,900), titlefontsize=8,layout=(2,2)) +``` + + 10×2 DataFrame + Row │ Resource Type Total Capacity + │ Any Any + ─────┼─────────────────────────────────────────────── + 1 │ MA_natural_gas_combined_cycle 9.47511 + 2 │ CT_natural_gas_combined_cycle 8.40465 + 3 │ ME_natural_gas_combined_cycle 0.0 + 4 │ MA_solar_pv 5.0 + 5 │ CT_onshore_wind 10.0 + 6 │ CT_solar_pv 0.0 + 7 │ ME_onshore_wind 0.0 + 8 │ MA_battery 0.128507 + 9 │ CT_battery 4.28065 + 10 │ ME_battery 1.59085 + + +![svg](./files/t7_4p_mcr_csm_esr_mass.svg) + + +## All Together + +`1_three_zones` has defaults of `CO2Cap = 2` and `MinCapReq = 1`. To see how everything comes together, let's add ESR and CRM in as well: + + +```julia +setup["MinCapReq"] = 1 +setup["CO2Cap"] = 2 +setup["EnergyShareRequirement"] = 1 +setup["CapacityReserveMargin"] = 1; +``` + + +```julia +inputs = GenX.load_inputs(setup, case) +EP10 = GenX.generate_model(setup,inputs,OPTIMIZER) +GenX.solve_model(EP10,setup) +``` + + Reading Input CSV Files + Network.csv Successfully Read! + Demand (load) data Successfully Read! + Fuels_data.csv Successfully Read! + + Summary of resources loaded into the model: + ------------------------------------------------------- + Resource type Number of resources + ======================================================= + Thermal 3 + VRE 4 + Storage 3 + ======================================================= + Total number of resources: 10 + ------------------------------------------------------- + Generators_variability.csv Successfully Read! + Validating time basis + Capacity_reserve_margin.csv Successfully Read! + Minimum_capacity_requirement.csv Successfully Read! + Energy_share_requirement.csv Successfully Read! + CO2_cap.csv Successfully Read! + CSV Files Successfully Read In From example_systems/1_three_zones + Discharge Module + + + Thermal.csv Successfully Read. + Vre.csv Successfully Read. + Storage.csv Successfully Read. + Resource_energy_share_requirement.csv Successfully Read. + Resource_capacity_reserve_margin.csv Successfully Read. + Resource_minimum_capacity_requirement.csv Successfully Read. + + + Non-served Energy Module + Investment Discharge Module + Unit Commitment Module + Fuel Module + CO2 Module + Investment Transmission Module + Transmission Module + Dispatchable Resources Module + Storage Resources Module + Storage Investment Module + Storage Core Resources Module + Storage Resources with Symmetric Charge/Discharge Capacity Module + Thermal (Unit Commitment) Resources Module + CO2 Policies Module + Energy Share Requirement Policies Module + Capacity Reserve Margin Policies Module + Minimum Capacity Requirement Module + Running HiGHS 1.6.0: Copyright (c) 2023 HiGHS under MIT licence terms + Presolving model + 131097 rows, 97836 cols, 583245 nonzeros + 123901 rows, 90641 cols, 586406 nonzeros + Presolve : Reductions: rows 123901(-42435); columns 90641(-46130); elements 586406(-49082) + Solving the presolved LP + IPX model has 123901 rows, 90641 columns and 586406 nonzeros + Input + Number of variables: 90641 + Number of free variables: 3696 + Number of constraints: 123901 + Number of equality constraints: 22373 + Number of matrix entries: 586406 + Matrix range: [4e-07, 1e+01] + RHS range: [7e-01, 5e+04] + Objective range: [1e-04, 4e+02] + Bounds range: [2e-03, 2e+01] + Preprocessing + Dualized model: no + Number of dense columns: 15 + Range of scaling factors: [5.00e-01, 4.00e+00] + IPX version 1.0 + Interior Point Solve + Iter P.res D.res P.obj D.obj mu Time + 0 5.30e+03 3.22e+02 3.13997460e+06 -4.50780216e+06 1.69e+06 0s + 1 3.53e+03 1.84e+02 -3.86867578e+08 -1.04799903e+07 1.20e+06 0s + 2 2.99e+03 1.79e+02 -4.64684777e+08 -1.23686026e+07 1.12e+06 1s + Constructing starting basis... + 3 2.03e+03 1.48e+02 -2.85325444e+09 -1.06915573e+07 9.68e+05 3s + 4 1.45e+03 5.36e+01 -2.20472171e+09 -2.15853094e+07 5.20e+05 6s + 5 5.72e+02 2.55e+01 -7.34160531e+08 -2.85605099e+07 2.70e+05 8s + 6 1.52e+02 9.55e+00 -6.47264055e+07 -2.78488489e+07 9.45e+04 10s + 7 2.02e+01 1.79e+00 2.31123203e+07 -2.71227296e+07 1.70e+04 14s + 8 1.25e+00 1.73e-01 2.78294575e+07 -2.15984056e+07 1.70e+03 17s + 9 6.87e-01 1.01e-01 2.46933049e+07 -1.61205280e+07 1.04e+03 20s + 10 3.29e-01 6.17e-02 2.40546197e+07 -1.56062552e+07 7.59e+02 22s + 11 1.55e-01 3.97e-02 2.03010629e+07 -1.19472220e+07 4.90e+02 24s + 12 6.17e-02 2.00e-02 1.58293799e+07 -7.68786756e+06 2.58e+02 26s + 13 3.21e-02 7.58e-03 1.16341185e+07 -3.68819752e+06 1.19e+02 28s + 14 5.73e-03 2.53e-03 4.99822439e+06 -1.84838835e+06 4.06e+01 31s + 15 2.04e-04 2.53e-09 9.67831330e+05 -2.16110538e+05 5.64e+00 33s + 16 6.41e-05 6.62e-10 3.11314108e+05 -5.67635644e+04 1.75e+00 37s + 17 1.87e-05 1.78e-10 1.02250933e+05 -1.96360216e+04 5.79e-01 39s + 18 9.31e-06 9.91e-11 7.06100466e+04 -1.48397467e+04 4.06e-01 40s + 19 3.70e-06 5.73e-11 3.96082759e+04 -8.05455770e+03 2.26e-01 41s + 20 3.28e-06 4.27e-11 3.82171130e+04 -6.25733365e+03 2.11e-01 43s + 21 1.59e-06 2.73e-11 2.68362068e+04 -2.39421912e+03 1.39e-01 44s + 22 1.15e-06 2.36e-11 2.56652324e+04 -1.92773607e+03 1.31e-01 48s + 23 6.03e-07 1.36e-11 2.08387443e+04 9.30026259e+02 9.45e-02 50s + 24 3.68e-07 8.19e-12 1.81640559e+04 2.92313732e+03 7.24e-02 52s + 25 3.10e-07 5.91e-12 1.72489969e+04 3.83317942e+03 6.37e-02 56s + 26 2.39e-07 4.09e-12 1.61203457e+04 4.88465026e+03 5.33e-02 59s + 27 1.73e-07 3.64e-12 1.48295799e+04 5.55407232e+03 4.40e-02 61s + 28 1.44e-07 2.73e-12 1.42754988e+04 6.29135803e+03 3.79e-02 64s + 29 8.49e-08 1.14e-12 1.28069756e+04 7.07462835e+03 2.72e-02 67s + 30 7.31e-08 9.09e-13 1.26298795e+04 7.31806811e+03 2.52e-02 71s + 31 5.30e-08 9.09e-13 1.20848925e+04 7.73537437e+03 2.06e-02 73s + 32 4.21e-08 1.82e-12 1.18122159e+04 8.13586949e+03 1.75e-02 75s + 33 2.39e-08 9.09e-13 1.14149641e+04 8.30558496e+03 1.48e-02 77s + 34 1.60e-08 4.55e-13 1.10801208e+04 8.63533586e+03 1.16e-02 79s + 35 1.30e-08 3.69e-13 1.09411593e+04 8.83346002e+03 1.00e-02 83s + 36 8.91e-09 4.83e-13 1.07431756e+04 8.89580519e+03 8.77e-03 85s + 37 5.99e-09 2.27e-13 1.05961030e+04 9.02764246e+03 7.45e-03 88s + 38 4.85e-09 3.13e-13 1.05383085e+04 9.05248333e+03 7.05e-03 89s + 39 4.45e-09 3.41e-13 1.05136942e+04 9.13561353e+03 6.54e-03 91s + 40 4.23e-09 1.99e-13 1.04966448e+04 9.18695690e+03 6.22e-03 93s + 41 2.40e-09 6.25e-13 1.03049603e+04 9.27134867e+03 4.91e-03 94s + 42 1.47e-09 4.55e-13 1.02280746e+04 9.41441297e+03 3.86e-03 96s + 43 1.03e-09 2.27e-13 1.01746657e+04 9.47565695e+03 3.32e-03 99s + 44 8.01e-10 3.41e-13 1.01513061e+04 9.48693587e+03 3.15e-03 101s + 45 5.03e-10 2.84e-13 1.00966206e+04 9.52771181e+03 2.70e-03 102s + 46 3.06e-10 2.84e-13 1.00688557e+04 9.57071855e+03 2.36e-03 103s + 47 2.44e-10 5.40e-13 1.00557587e+04 9.57656156e+03 2.27e-03 105s + 48 1.89e-10 1.81e-13 1.00328512e+04 9.61968546e+03 1.96e-03 106s + 49 1.64e-10 5.97e-13 1.00260933e+04 9.63360286e+03 1.86e-03 107s + 50 1.86e-10 2.27e-13 9.99901149e+03 9.65578822e+03 1.63e-03 108s + 51 8.00e-11 8.81e-13 9.99387541e+03 9.67539200e+03 1.51e-03 109s + 52 8.00e-11 2.56e-13 9.99333474e+03 9.68646833e+03 1.46e-03 110s + 53 6.18e-11 3.98e-13 9.98008312e+03 9.70915205e+03 1.29e-03 110s + 54 8.00e-11 2.27e-13 9.96536538e+03 9.73027009e+03 1.12e-03 111s + 55 5.46e-11 5.68e-13 9.94153050e+03 9.78635740e+03 7.37e-04 112s + 56 2.95e-11 3.13e-13 9.93239666e+03 9.79987086e+03 6.29e-04 113s + 57 4.37e-11 6.25e-13 9.92881475e+03 9.81037466e+03 5.62e-04 114s + 58 9.09e-11 2.56e-13 9.92687487e+03 9.81524886e+03 5.30e-04 115s + 59 8.37e-11 3.13e-13 9.91943642e+03 9.82987624e+03 4.25e-04 116s + 60 2.94e-11 7.39e-13 9.90937510e+03 9.84468538e+03 3.07e-04 117s + 61 1.09e-11 1.99e-13 9.90384575e+03 9.84900802e+03 2.60e-04 118s + 62 4.73e-11 1.99e-13 9.89878174e+03 9.86141678e+03 1.77e-04 119s + 63 1.43e-11 2.84e-13 9.89460308e+03 9.86618770e+03 1.35e-04 120s + 64 5.21e-11 1.99e-13 9.89193935e+03 9.86936430e+03 1.07e-04 120s + 65 4.37e-11 7.11e-13 9.89058874e+03 9.87123409e+03 9.19e-05 121s + 66 1.06e-10 1.42e-13 9.88773562e+03 9.87325788e+03 6.87e-05 122s + 67 2.18e-11 4.55e-13 9.88672573e+03 9.87498544e+03 5.57e-05 123s + 68 6.55e-11 5.68e-13 9.88612597e+03 9.87857947e+03 3.58e-05 124s + 69 1.75e-10 4.26e-13 9.88493177e+03 9.87912210e+03 2.76e-05 124s + 70 3.96e-10 2.27e-13 9.88436495e+03 9.88063726e+03 1.77e-05 125s + 71 2.09e-10 3.13e-13 9.88433519e+03 9.88080074e+03 1.68e-05 126s + 72 1.12e-10 6.82e-13 9.88391819e+03 9.88129195e+03 1.25e-05 126s + 73 4.78e-10 8.24e-13 9.88340973e+03 9.88203826e+03 6.51e-06 127s + 74 2.83e-10 3.13e-13 9.88340067e+03 9.88230236e+03 5.21e-06 128s + 75 3.21e-10 1.21e-13 9.88336541e+03 9.88238526e+03 4.65e-06 129s + 76 1.84e-10 2.56e-13 9.88330624e+03 9.88266293e+03 3.05e-06 129s + 77 3.08e-10 3.41e-13 9.88330730e+03 9.88300302e+03 1.44e-06 130s + 78 3.40e-10 1.34e-12 9.88324655e+03 9.88310691e+03 6.63e-07 131s + 79 5.61e-10 6.22e-13 9.88321826e+03 9.88313132e+03 4.13e-07 132s + 80 1.33e-10 1.04e-12 9.88320942e+03 9.88313520e+03 3.52e-07 132s + 81 6.55e-10 1.40e-12 9.88319429e+03 9.88314640e+03 2.27e-07 133s + 82 3.26e-10 7.11e-13 9.88318935e+03 9.88315310e+03 1.72e-07 134s + 83 3.31e-10 1.55e-12 9.88318186e+03 9.88315734e+03 1.16e-07 134s + 84 1.07e-09 3.55e-12 9.88317725e+03 9.88316598e+03 5.35e-08 135s + 85 4.30e-10 3.68e-12 9.88317303e+03 9.88316877e+03 2.03e-08 135s + 86 6.50e-10 4.82e-12 9.88317188e+03 9.88317115e+03 3.46e-09 136s + 87* 4.99e-10 5.93e-12 9.88317181e+03 9.88317173e+03 3.40e-10 137s + 88* 1.01e-09 8.27e-12 9.88317178e+03 9.88317177e+03 5.01e-11 138s + 89* 4.77e-10 1.42e-11 9.88317178e+03 9.88317178e+03 8.36e-12 140s + Running crossover as requested + Primal residual before push phase: 3.57e-05 + Dual residual before push phase: 2.51e-06 + Number of dual pushes required: 30806 + Number of primal pushes required: 4202 + Summary + Runtime: 144.53s + Status interior point solve: optimal + Status crossover: optimal + objective value: 9.88317178e+03 + interior solution primal residual (abs/rel): 3.06e-08 / 6.40e-13 + interior solution dual residual (abs/rel): 7.13e-09 / 1.77e-11 + interior solution objective gap (abs/rel): 1.66e-06 / 1.68e-10 + basic solution primal infeasibility: 3.19e-12 + basic solution dual infeasibility: 6.25e-08 + Ipx: IPM optimal + Ipx: Crossover optimal + Solving the original LP from the solution after postsolve + Model status : Optimal + IPM iterations: 89 + Crossover iterations: 3032 + Objective value : 9.8831717769e+03 + HiGHS run time : 144.82 + LP solved for primal + + + + + + (A JuMP Model + Minimization problem with: + Variables: 136771 + Objective function type: AffExpr + `AffExpr`-in-`MathOptInterface.EqualTo{Float64}`: 40656 constraints + `AffExpr`-in-`MathOptInterface.GreaterThan{Float64}`: 27728 constraints + `AffExpr`-in-`MathOptInterface.LessThan{Float64}`: 97952 constraints + `VariableRef`-in-`MathOptInterface.EqualTo{Float64}`: 4 constraints + `VariableRef`-in-`MathOptInterface.GreaterThan{Float64}`: 133071 constraints + Model mode: AUTOMATIC + CachingOptimizer state: ATTACHED_OPTIMIZER + Solver name: HiGHS + Names registered in the model: FuelCalculationCommit_single, cCO2Emissions_systemwide, cCapacityResMargin, cESRShare, cFuelCalculation_single, cMaxCap, cMaxCapEnergy, cMaxCapEnergyDuration, cMaxFlow_in, cMaxFlow_out, cMaxLineReinforcement, cMaxNSE, cMaxRetCommit, cMaxRetEnergy, cMaxRetNoCommit, cMinCap, cMinCapEnergy, cMinCapEnergyDuration, cNSEPerSeg, cPowerBalance, cSOCMinCapRes, cSoCBalInterior, cSoCBalStart, cStartFuel_single, cTAuxLimit, cTAuxSum, cTLoss, cVSoCBalInterior, cVSoCBalStart, cZoneMinCapReq, eAvail_Trans_Cap, eCFix, eCFixEnergy, eCFuelOut, eCFuelStart, eCNSE, eCStart, eCVar_in, eCVar_in_virtual, eCVar_out, eCVar_out_virtual, eCapResMarBalance, eCapResMarBalanceNSE, eCapResMarBalanceStor, eCapResMarBalanceStorVirtual, eCapResMarBalanceThermal, eCapResMarBalanceTrans, eCapResMarBalanceVRE, eELOSS, eELOSSByZone, eESR, eESRDischarge, eEmissionsByPlant, eEmissionsByZone, eExistingCap, eExistingCapEnergy, eFuelConsumption, eFuelConsumptionYear, eFuelConsumption_single, eGenerationByThermAll, eGenerationByVRE, eGenerationByZone, eLosses_By_Zone, eMinCapRes, eMinCapResInvest, eNet_Export_Flows, eObj, ePlantCFuelOut, ePlantCFuelStart, ePlantFuel_generation, ePlantFuel_start, ePowerBalance, ePowerBalanceDisp, ePowerBalanceLossesByZone, ePowerBalanceNetExportFlows, ePowerBalanceNse, ePowerBalanceStor, ePowerBalanceThermCommit, eStartFuel, eTotalCFix, eTotalCFixEnergy, eTotalCFuelOut, eTotalCFuelStart, eTotalCNSE, eTotalCNSET, eTotalCNSETS, eTotalCNetworkExp, eTotalCStart, eTotalCStartT, eTotalCVarIn, eTotalCVarInT, eTotalCVarInT_virtual, eTotalCVarIn_virtual, eTotalCVarOut, eTotalCVarOutT, eTotalCVarOutT_virtual, eTotalCVarOut_virtual, eTotalCap, eTotalCapEnergy, eTransMax, eZonalCFuelOut, eZonalCFuelStart, vCAP, vCAPENERGY, vCAPRES_charge, vCAPRES_discharge, vCAPRES_socinreserve, vCHARGE, vCO2Cap_slack, vCOMMIT, vFLOW, vFuel, vNEW_TRANS_CAP, vNSE, vP, vRETCAP, vRETCAPENERGY, vS, vSHUT, vSTART, vStartFuel, vTAUX_NEG, vTAUX_POS, vTLOSS, vZERO, 145.17469787597656) + + + + +```julia +totCap10 = value.(EP10[:eTotalCap]) + +totCapB10 = [totCap10[1] + totCap10[2] + totCap10[3], totCap10[4] + totCap10[6], + totCap10[5] + totCap10[7], totCap10[8] + totCap10[9] + totCap10[10]] + +println(DataFrame([RT totCap10],["Resource Type","Total Capacity"])) +println(" ") + +G10 = groupedbar(transpose(totCapB10), bar_position = :stack, bar_width=.7,size=(500,450), xticks=[ ],ylabel="GW", + labels=["Natural Gas" "Solar" "Wind" "Battery"],color=colors, + title="MCR + ESR + CSM + CO2 Load Cap \n Obj Val: $(round(objective_value(EP10),digits=6))",ylabelfontsize=8) + +plot(G10, titlefontsize=8) +``` + + 10×2 DataFrame + Row │ Resource Type Total Capacity + │ Any Any + ─────┼─────────────────────────────────────────────── + 1 │ MA_natural_gas_combined_cycle 8.51325 + 2 │ CT_natural_gas_combined_cycle 5.43676 + 3 │ ME_natural_gas_combined_cycle 0.552834 + 4 │ MA_solar_pv 17.9707 + 5 │ CT_onshore_wind 12.5249 + 6 │ CT_solar_pv 14.9714 + 7 │ ME_onshore_wind 11.4099 + 8 │ MA_battery 4.55918 + 9 │ CT_battery 4.08421 + 10 │ ME_battery 0.764153 + + + +![svg](./files/t7_4p_mcr_csm_esr_mass.svg) + + +```julia + +``` diff --git a/docs/src/Tutorials/Tutorial_8_outputs.md b/docs/src/Tutorials/Tutorial_8_outputs.md new file mode 100644 index 0000000000..c1b6f8ee19 --- /dev/null +++ b/docs/src/Tutorials/Tutorial_8_outputs.md @@ -0,0 +1,875 @@ +# Tutorial 8: Outputs + +[Interactive Notebook of the tutorial](https://github.com/GenXProject/GenX-Tutorials/blob/main/Tutorials/Tutorial_8_Outputs.ipynb) + +Once an instance of GenX is run, a series of csv files describing the outputs are created and put in to a folder titled `results`. This folder will appear automatically in the case folder. For a detailed description of all files, see the [GenX Outputs](@ref) documentation. This tutorial goes over key files in `results` and visualizes some of the outputs. + +### Table of Contents +* [Power](#power) +* [Cost and Revenue](#cost) +* [Emmissions](#emms) + +Let's get things started by running an instance of GenX using `Run.jl`. You can skip this step if you already have a results folder you would like to analyze. + + +```julia +using DataFrames +using CSV +using YAML +using GraphRecipes +using Plots +using PlotlyJS +using VegaLite +using StatsPlots +``` + + +```julia +case = joinpath("example_systems/1_three_zones"); +``` + + +```julia +include("example_systems/1_three_zones/Run.jl") +``` + + Configuring Settings + Clustering Time Series Data (Grouped)... + Reading Input CSV Files + Network.csv Successfully Read! + Demand (load) data Successfully Read! + Fuels_data.csv Successfully Read! + + + Thermal.csv Successfully Read. + Vre.csv Successfully Read. + Storage.csv Successfully Read. + Resource_energy_share_requirement.csv Successfully Read. + Resource_capacity_reserve_margin.csv Successfully Read. + Resource_minimum_capacity_requirement.csv Successfully Read. + + + + Summary of resources loaded into the model: + ------------------------------------------------------- + Resource type Number of resources + ======================================================= + Thermal 3 + VRE 4 + Storage 3 + ======================================================= + Total number of resources: 10 + ------------------------------------------------------- + Generators_variability.csv Successfully Read! + Validating time basis + Minimum_capacity_requirement.csv Successfully Read! + CO2_cap.csv Successfully Read! + CSV Files Successfully Read In From /Users/mayamutic/Desktop/GenX-Tutorials/Tutorials/example_systems/1_three_zones + Configuring Solver + Loading Inputs + Reading Input CSV Files + Network.csv Successfully Read! + Demand (load) data Successfully Read! + Fuels_data.csv Successfully Read! + + Summary of resources loaded into the model: + ------------------------------------------------------- + Resource type Number of resources + ======================================================= + Thermal 3 + VRE 4 + Storage 3 + ======================================================= + Total number of resources: 10 + ------------------------------------------------------- + Generators_variability.csv Successfully Read! + Validating time basis + Minimum_capacity_requirement.csv Successfully Read! + CO2_cap.csv Successfully Read! + CSV Files Successfully Read In From /Users/mayamutic/Desktop/GenX-Tutorials/Tutorials/example_systems/1_three_zones + Generating the Optimization Model + + + Thermal.csv Successfully Read. + Vre.csv Successfully Read. + Storage.csv Successfully Read. + Resource_energy_share_requirement.csv Successfully Read. + Resource_capacity_reserve_margin.csv Successfully Read. + Resource_minimum_capacity_requirement.csv Successfully Read. + + Discharge Module + Non-served Energy Module + Investment Discharge Module + Unit Commitment Module + Fuel Module + CO2 Module + Investment Transmission Module + Transmission Module + Dispatchable Resources Module + Storage Resources Module + Storage Investment Module + Storage Core Resources Module + Storage Resources with Symmetric Charge/Discharge Capacity Module + Thermal (Unit Commitment) Resources Module + CO2 Policies Module + Minimum Capacity Requirement Module + Time elapsed for model building is + 5.887781667 + Solving Model + Running HiGHS 1.6.0: Copyright (c) 2023 HiGHS under MIT licence terms + Presolving model + 118038 rows, 81083 cols, 466827 nonzeros + 110619 rows, 73664 cols, 468369 nonzeros + Presolve : Reductions: rows 110619(-42779); columns 73664(-46475); elements 468369(-47001) + Solving the presolved LP + IPX model has 110619 rows, 73664 columns and 468369 nonzeros + Input + Number of variables: 73664 + Number of free variables: 3696 + Number of constraints: 110619 + Number of equality constraints: 16605 + Number of matrix entries: 468369 + Matrix range: [4e-07, 1e+01] + RHS range: [8e-01, 4e+03] + Objective range: [1e-04, 7e+02] + Bounds range: [2e-03, 2e+01] + Preprocessing + Dualized model: no + Number of dense columns: 15 + Range of scaling factors: [5.00e-01, 8.00e+00] + IPX version 1.0 + Interior Point Solve + Iter P.res D.res P.obj D.obj mu Time + 0 1.82e+02 5.20e+02 2.74110935e+06 -9.20003322e+06 9.39e+04 0s + 1 1.19e+02 1.85e+02 -5.15491323e+07 -1.56990646e+07 5.70e+04 0s + 2 1.16e+02 1.50e+02 -5.24235846e+07 -4.43639951e+07 6.32e+04 1s + 3 3.82e+01 7.90e+01 -3.78240082e+07 -4.91067811e+07 2.48e+04 1s + Constructing starting basis... + 4 1.50e+01 4.69e+01 -1.55669921e+07 -5.24473820e+07 1.26e+04 4s + 5 1.08e+01 3.68e+01 -1.04270740e+07 -5.34777833e+07 1.03e+04 5s + 6 2.02e+00 1.32e+01 1.78253836e+06 -4.42778415e+07 3.28e+03 6s + 7 2.13e-01 1.56e+00 2.27050184e+06 -1.69980996e+07 4.42e+02 8s + 8 1.99e-02 3.66e-01 1.38286053e+06 -4.54007601e+06 1.02e+02 9s + 9 7.25e-03 1.59e-01 1.04876462e+06 -2.67941780e+06 5.28e+01 10s + 10 5.00e-03 1.18e-01 9.54695852e+05 -2.19740360e+06 4.20e+01 11s + 11 2.55e-03 8.92e-02 9.62825668e+05 -2.02235540e+06 3.86e+01 12s + 12 1.23e-03 5.57e-02 9.06154486e+05 -1.61397732e+06 2.88e+01 13s + 13 8.25e-04 4.55e-02 8.50982220e+05 -1.42601127e+06 2.47e+01 14s + 14 4.67e-04 2.99e-02 7.87488727e+05 -1.12886839e+06 1.86e+01 15s + 15 2.66e-04 2.09e-02 7.13648088e+05 -9.18336741e+05 1.44e+01 16s + 16 1.08e-04 1.25e-02 5.49221416e+05 -6.47879126e+05 9.37e+00 17s + 17 2.48e-05 7.94e-03 3.46061560e+05 -4.61304054e+05 5.71e+00 19s + 18 1.01e-05 3.42e-03 1.83821789e+05 -2.00914905e+05 2.43e+00 20s + 19 4.07e-06 3.05e-03 8.92521427e+04 -1.79940971e+05 1.62e+00 21s + 20 2.32e-06 6.40e-04 7.78040127e+04 -5.60566929e+04 7.50e-01 22s + 21 8.58e-07 2.28e-04 4.31894440e+04 -2.23356614e+04 3.58e-01 23s + 22 6.57e-07 1.54e-04 4.00530649e+04 -1.69523138e+04 3.10e-01 24s + 23 3.28e-07 9.35e-05 2.85728271e+04 -9.35216394e+03 2.05e-01 25s + 24 3.19e-07 8.61e-05 2.84621914e+04 -8.84896370e+03 2.02e-01 27s + 25 1.88e-07 7.27e-05 2.38512793e+04 -7.05855824e+03 1.67e-01 28s + 26 1.25e-07 4.25e-05 2.06483772e+04 -2.29444044e+03 1.24e-01 29s + 27 8.52e-08 2.94e-05 1.93840344e+04 -7.43312356e+02 1.08e-01 30s + 28 4.26e-08 1.57e-05 1.55476712e+04 2.51649304e+03 7.01e-02 32s + 29 3.28e-08 1.03e-05 1.48244443e+04 3.71569832e+03 5.97e-02 34s + 30 1.97e-08 5.79e-06 1.34432901e+04 5.04894425e+03 4.51e-02 36s + 31 1.60e-08 4.11e-06 1.29661877e+04 5.70523948e+03 3.90e-02 40s + 32 1.29e-08 2.50e-06 1.25046561e+04 6.46897465e+03 3.24e-02 43s + 33 1.10e-08 1.77e-06 1.21681482e+04 6.95806253e+03 2.80e-02 45s + 34 9.03e-09 1.20e-06 1.17942632e+04 7.37735035e+03 2.37e-02 47s + 35 8.36e-09 8.75e-07 1.16689507e+04 7.61101800e+03 2.18e-02 50s + 36 5.05e-09 7.46e-07 1.09362653e+04 7.76380519e+03 1.70e-02 51s + 37 2.31e-09 4.73e-07 1.04305053e+04 8.02182477e+03 1.29e-02 53s + 38 1.54e-09 3.37e-07 1.01805357e+04 8.29224222e+03 1.01e-02 54s + 39 1.40e-09 2.47e-07 1.01569154e+04 8.40516136e+03 9.41e-03 57s + 40 1.32e-09 2.29e-07 1.01417025e+04 8.43289860e+03 9.18e-03 61s + 41 1.30e-09 1.85e-07 1.01370889e+04 8.50324712e+03 8.78e-03 62s + 42 1.09e-09 1.37e-07 1.00815866e+04 8.60006398e+03 7.96e-03 63s + 43 8.94e-10 1.09e-07 1.00184156e+04 8.66341527e+03 7.28e-03 65s + 44 7.15e-10 7.94e-08 9.96247094e+03 8.73797483e+03 6.58e-03 66s + 45 3.38e-10 5.48e-08 9.81246478e+03 8.82514229e+03 5.30e-03 67s + 46 1.96e-10 3.88e-08 9.73048449e+03 8.91021222e+03 4.41e-03 68s + 47 1.58e-10 3.09e-08 9.70120639e+03 8.96100550e+03 3.98e-03 69s + 48 8.73e-11 1.87e-08 9.63351889e+03 9.05587884e+03 3.10e-03 70s + 49 4.53e-11 8.50e-09 9.58546144e+03 9.14690441e+03 2.36e-03 72s + 50 3.66e-11 4.10e-09 9.56746403e+03 9.21860822e+03 1.87e-03 73s + 51 2.08e-11 3.03e-09 9.52048699e+03 9.24367874e+03 1.49e-03 76s + 52 1.09e-11 2.43e-09 9.49319901e+03 9.26588343e+03 1.22e-03 77s + 53 5.73e-12 1.94e-09 9.47529880e+03 9.28196289e+03 1.04e-03 78s + 54 5.27e-12 1.59e-09 9.46285225e+03 9.29875030e+03 8.82e-04 79s + 55 3.67e-12 1.52e-09 9.45597250e+03 9.30095244e+03 8.33e-04 80s + 56 1.75e-12 1.22e-09 9.45179686e+03 9.31195889e+03 7.51e-04 81s + 57 3.04e-11 1.00e-09 9.44732720e+03 9.32669073e+03 6.48e-04 82s + 58 2.55e-11 5.84e-10 9.43948980e+03 9.34808459e+03 4.91e-04 83s + 59 2.06e-11 3.64e-10 9.43331328e+03 9.36391778e+03 3.73e-04 84s + 60 5.92e-12 2.32e-10 9.43127125e+03 9.37217204e+03 3.17e-04 85s + 61 5.80e-12 7.80e-11 9.42595625e+03 9.38759526e+03 2.06e-04 85s + 62 2.43e-11 4.87e-11 9.42485062e+03 9.39270257e+03 1.73e-04 86s + 63 6.10e-12 2.84e-11 9.42230551e+03 9.39721826e+03 1.35e-04 87s + 64 3.02e-11 2.06e-11 9.41851226e+03 9.39922232e+03 1.04e-04 88s + 65 7.68e-12 1.31e-11 9.41545711e+03 9.40267444e+03 6.87e-05 89s + 66 3.56e-11 5.95e-12 9.41476857e+03 9.40633324e+03 4.53e-05 89s + 67 2.52e-11 5.71e-12 9.41439309e+03 9.40677438e+03 4.09e-05 90s + 68 6.47e-11 4.77e-12 9.41368241e+03 9.40741592e+03 3.37e-05 91s + 69 1.02e-11 3.50e-12 9.41352178e+03 9.40807301e+03 2.93e-05 91s + 70 1.81e-11 2.61e-12 9.41317436e+03 9.40888102e+03 2.31e-05 92s + 71 4.19e-11 9.41e-13 9.41301486e+03 9.41024157e+03 1.49e-05 92s + 72 2.36e-10 7.39e-13 9.41247409e+03 9.41108729e+03 7.45e-06 93s + 73 3.36e-10 5.12e-13 9.41246723e+03 9.41147776e+03 5.32e-06 94s + 74 1.71e-10 1.99e-13 9.41229429e+03 9.41188577e+03 2.19e-06 94s + 75 3.35e-10 4.01e-13 9.41222697e+03 9.41203307e+03 1.04e-06 95s + 76 1.60e-10 6.82e-13 9.41217628e+03 9.41205260e+03 6.64e-07 96s + 77 5.99e-10 1.42e-12 9.41215300e+03 9.41209472e+03 3.13e-07 96s + 78 6.44e-11 7.21e-13 9.41214245e+03 9.41212653e+03 8.55e-08 97s + 79 6.69e-11 6.79e-13 9.41213955e+03 9.41213348e+03 3.26e-08 98s + 80 4.03e-10 1.36e-12 9.41213754e+03 9.41213419e+03 1.80e-08 98s + 81 3.37e-10 2.61e-12 9.41213656e+03 9.41213608e+03 2.60e-09 99s + 82* 2.65e-10 6.98e-12 9.41213642e+03 9.41213636e+03 3.52e-10 99s + 83* 2.52e-10 6.65e-12 9.41213641e+03 9.41213640e+03 6.97e-11 101s + 84* 2.01e-10 4.96e-12 9.41213641e+03 9.41213641e+03 1.32e-11 104s + 85* 1.31e-10 6.08e-12 9.41213641e+03 9.41213641e+03 1.20e-12 104s + Running crossover as requested + Primal residual before push phase: 3.02e-07 + Dual residual before push phase: 4.01e-07 + Number of dual pushes required: 24726 + Number of primal pushes required: 3458 + Summary + Runtime: 107.62s + Status interior point solve: optimal + Status crossover: optimal + objective value: 9.41213641e+03 + interior solution primal residual (abs/rel): 3.75e-09 / 9.14e-13 + interior solution dual residual (abs/rel): 2.40e-09 / 3.42e-12 + interior solution objective gap (abs/rel): 1.95e-07 / 2.08e-11 + basic solution primal infeasibility: 5.02e-14 + basic solution dual infeasibility: 1.09e-15 + Ipx: IPM optimal + Ipx: Crossover optimal + Solving the original LP from the solution after postsolve + Model status : Optimal + IPM iterations: 85 + Crossover iterations: 2764 + Objective value : 9.4121364078e+03 + HiGHS run time : 107.89 + LP solved for primal + Writing Output + Time elapsed for writing costs is + 0.8427745 + Time elapsed for writing capacity is + 0.277263333 + Time elapsed for writing power is + 0.6167225 + Time elapsed for writing charge is + 0.18541725 + Time elapsed for writing capacity factor is + 0.235379791 + Time elapsed for writing storage is + 0.132649083 + Time elapsed for writing curtailment is + 0.155876791 + Time elapsed for writing nse is + 0.438298833 + Time elapsed for writing power balance is + 0.297414291 + Time elapsed for writing transmission flows is + 0.103818667 + Time elapsed for writing transmission losses is + 0.097776166 + Time elapsed for writing network expansion is + 0.080605 + Time elapsed for writing emissions is + 0.280204166 + Time elapsed for writing reliability is + 0.093714833 + Time elapsed for writing storage duals is + 0.391718917 + Time elapsed for writing commitment is + 0.085516291 + Time elapsed for writing startup is + 0.045873 + Time elapsed for writing shutdown is + 0.027687542 + Time elapsed for writing fuel consumption is + 0.31172675 + Time elapsed for writing co2 is + 0.053309291 + Time elapsed for writing price is + 0.056254791 + Time elapsed for writing energy revenue is + 0.21793425 + Time elapsed for writing charging cost is + 0.155090166 + Time elapsed for writing subsidy is + 0.244266583 + Time elapsed for writing time weights is + 0.061457459 + Time elapsed for writing co2 cap is + 0.084762792 + Time elapsed for writing minimum capacity requirement is + 0.090502375 + Time elapsed for writing net revenue is + 0.8798285 + Wrote outputs to /Users/mayamutic/Desktop/GenX-Tutorials/Tutorials/example_systems/1_three_zones/results + Time elapsed for writing is + 6.909353542 + + +Below are all 33 files output by running GenX: + + +```julia +results = cd(readdir,joinpath(case,"results")) +``` + + + + + 33-element Vector{String}: + "CO2_prices_and_penalties.csv" + "ChargingCost.csv" + "EnergyRevenue.csv" + "FuelConsumption_plant_MMBTU.csv" + "FuelConsumption_total_MMBTU.csv" + "Fuel_cost_plant.csv" + "MinCapReq_prices_and_penalties.csv" + "NetRevenue.csv" + "RegSubsidyRevenue.csv" + "SubsidyRevenue.csv" + "capacity.csv" + "capacityfactor.csv" + "charge.csv" + ⋮ + "power.csv" + "power_balance.csv" + "prices.csv" + "reliability.csv" + "run_settings.yml" + "shutdown.csv" + "start.csv" + "status.csv" + "storage.csv" + "storagebal_duals.csv" + "time_weights.csv" + "tlosses.csv" + + + +### Power + +The file `power.csv`, shown below, outputs the power in MW discharged by each node at each time step. Note that if TimeDomainReduction is in use the file will be shorter. The first row states which zone each node is part of, and the total power per year is located in the second row. After that, each row represents one time step of the series. + + +```julia +power = CSV.read(joinpath(case,"results/power.csv"),DataFrame,missingstring="NA") +``` +``` @raw html +
1850×12 DataFrame
1825 rows omitted
RowResourceMA_natural_gas_combined_cycleCT_natural_gas_combined_cycleME_natural_gas_combined_cycleMA_solar_pvCT_onshore_windCT_solar_pvME_onshore_windMA_batteryCT_batteryME_batteryTotal
String15Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64
1Zone1.02.03.01.02.02.03.01.02.03.00.0
2AnnualSum1.04015e73.42459e68.94975e52.47213e72.90683e72.69884e72.625e75.06354e61.45833e74.90368e61.463e8
3t1-0.0-0.0-0.0-0.08510.78-0.05300.610.02537.45673.3417022.2
4t2-0.0-0.0-0.0-0.08420.78-0.06282.040.02537.450.017240.3
5t3-0.0-0.0-0.0-0.08367.78-0.02409.840.02537.451828.2415143.3
6t4-0.0-0.0-0.0-0.08353.78-0.02762.241591.462537.450.015244.9
7t5-0.0-0.0-0.0-0.07482.39-0.00.01617.462980.641384.6213465.1
8t6-0.0-0.0-0.0-0.02429.93-0.02797.241717.965535.370.012480.5
9t7-0.0-0.0-0.0-0.011868.8-0.01374.731320.78871.4431340.6716776.4
10t8-0.0-0.0-0.0-0.02656.93-0.00.02115.965535.371452.6211760.9
11t9-0.0-0.0-0.03061.280.03110.82982.24868.8175389.440.015412.6
12t10-0.0-0.0-0.06100.227597.995543.690.00.00.01521.1220763.0
13t11-0.0-0.0-0.08314.290.06341.983080.240.02458.820.020195.3
1839t1837-0.0-0.0-0.06712.182541.66736.37305.6081410.33763.7261427.8219897.6
1840t1838-0.0-0.0-0.06514.150.06847.243153.240.03464.220.019978.9
1841t1839-0.0-0.0-0.05582.073848.886280.20.0195.4222048.31571.1219526.0
1842t1840-0.0-0.0-0.03688.139349.984892.73490.611006.020.00.022427.4
1843t1841-0.0-0.0-0.0509.228124.991351.083653.061218.52507.81828.2419192.9
1844t1842-0.0-0.0-0.0-0.02918.2-0.06896.822194.615535.37256.86317801.9
1845t1843-0.0-0.0-0.0-0.06800.37-0.07324.661838.113950.1541.947219955.2
1846t1844-0.0-0.0-0.0-0.09505.82-0.05683.661744.782567.93838.07720340.3
1847t1845-0.0-0.0-0.0-0.03491.93-0.05128.561597.615535.371107.4916861.0
1848t1846-0.0-0.0-0.0-0.012135.6-0.05021.751341.111140.561125.920764.9
1849t1847-0.0-0.0-0.0-0.08875.71-0.03605.98974.612665.481783.7917905.6
1850t1848-0.0-0.0-0.0-0.013549.1-0.04098.0541.61205.311478.2719872.3
+``` + + +Below is a visualization of the production over the first 168 hours, with the load demand curve from all three zones plotted on top: + + +```julia +# Pre-processing +tstart = 3 +tend = 170 +names_power = ["Solar","Natural_Gas","Battery","Wind"] + +power_tot = DataFrame([power[!,5]+power[!,7] power[!,2]+power[!,3]+power[!,4] power[!,9]+power[!,10]+power[!,11] power[!,6]+power[!,8]], + ["Solar","Natural_Gas","Battery","Wind"]) + +power_plot = DataFrame([collect(1:length(power_tot[tstart:tend,1])) power_tot[tstart:tend,1] repeat([names_power[1]],length(power_tot[tstart:tend,1]))], + ["Hour","MW", "Resource_Type"]); + +for i in range(2,4) + power_plot_temp = DataFrame([collect(1:length(power_tot[tstart:tend,i])) power_tot[tstart:tend,i] repeat([names_power[i]],length(power_tot[tstart:tend,i]))],["Hour","MW", "Resource_Type"]) + power_plot = [power_plot; power_plot_temp] +end + +loads = CSV.read(joinpath(case,"system/Demand_data.csv"),DataFrame,missingstring="NA") +loads_tot = loads[!,"Demand_MW_z1"]+loads[!,"Demand_MW_z2"]+loads[!,"Demand_MW_z3"] +power_plot[!,"Demand_Total"] = repeat(loads_tot[tstart:tend],4); +``` + + +```julia +power_plot |> +@vlplot()+ +@vlplot(mark={:area}, + x={:Hour,title="Time Step (hours)",labels="Resource_Type:n",axis={values=0:12:168}}, y={:MW,title="Load (MW)",type="quantitative"}, + color={"Resource_Type:n",scale={scheme="accent"},sort="descending"},order={field="Resource_Type:n"},width=845,height=400)+ +@vlplot(mark=:line,x=:Hour,y=:Demand_Total,lables="Demand",color={datum="Demand",legend={title=nothing}},title="Resource Capacity per Hour with Load Demand Curve, all Zones") +``` + +![svg](./files/t8_cap.svg) + + +We can separate it by zone in the following plot: + + +```julia +Zone1 = [power[2,2] power[2,5] 0 power[2,9]] +Zone2 = [power[2,3] power[2,7] power[2,6] power[2,10]] +Zone3 = [power[2,4] 0 power[2,8] power[2,11]] + +colors=[:silver :yellow :deepskyblue :violetred3] + +groupedbar(["Zone 1", "Zone 2", "Zone 3"],[Zone1; Zone2; Zone3], bar_position = :stack, bar_width=0.5,size=(400,450), + labels=["Natural Gas" "Solar" "Wind" "Battery"], + title="Resource Allocation in MW Per Zone",ylabel="MW",color=colors, titlefontsize=10) +``` + +![svg](./files/t8_resource_allocation.svg) + +Below is a heatmap for the natural gas plant in Massachusetts. It is normalized by the end capacity in `capcity.csv`. To change which plant the heat map plots, change the DataFrame column in `power` when defining `power_cap` below, and the corresponding capacity. + + +```julia +capacity = CSV.read(joinpath(case,"results/capacity.csv"),DataFrame,missingstring="NA") +Period_map = CSV.read(joinpath(case,"TDR_results/Period_map.csv"),DataFrame,missingstring="NA") + +# Take the EndCap and power of MA_natural_gas_combined_cycle +cap = capacity[1,"EndCap"] +power_cap = power[3:end,"MA_natural_gas_combined_cycle"]/cap; + +# Reconstruction of all hours of the year from TDR +recon = [] +for i in range(1,52) + index = Period_map[i,"Rep_Period_Index"] + recon_temp = power_cap[(168*index-167):(168*index)] + recon = [recon; recon_temp] +end + +# Convert to matrix format +heat = recon[1:24] +for i in range(1,364) + heat = [heat recon[(i*24-23):(i*24)]] +end + +``` + + +```julia +Plots.heatmap(heat,yticks=0:4:24,xticks=([15:30:364;], + ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sept","Oct","Nov","Dec"]), + size=(900,200),c=:lajolla) +``` + +![svg](./files/t8_heatmap.svg) + + + +### Cost and Revenue + +The basic cost of each power plant and the revenue it generates can be found in files `costs.csv`, `NetRevenue.csv`,and `EnergyRevenue.csv`. `NetRevenue.csv` breaks down each specific cost per node in each zone, which is useful to visualize what the cost is coming from. + + +```julia +netrevenue = CSV.read(joinpath(case,"results/NetRevenue.csv"),DataFrame,missingstring="NA") +``` + + + +``` @raw html +
10×28 DataFrame
RowregionResourcezoneClusterR_IDInv_cost_MWInv_cost_MWhInv_cost_charge_MWFixed_OM_cost_MWFixed_OM_cost_MWhFixed_OM_cost_charge_MWVar_OM_cost_outFuel_costVar_OM_cost_inStartCostCharge_costCO2SequestrationCostEnergyRevenueSubsidyRevenueOperatingReserveRevenueOperatingRegulationRevenueReserveMarginRevenueESRRevenueEmissionsCostRegSubsidyRevenueRevenueCostProfit
String3String31Int64Int64Int64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64
1MAMA_natural_gas_combined_cycle1115.54734e80.00.08.72561e70.00.03.69253e72.10416e80.03.84832e70.00.02.77103e90.00.00.00.00.01.84321e90.02.77103e92.77103e91.43051e-6
2CTCT_natural_gas_combined_cycle2121.42906e80.00.02.11911e70.00.01.22258e74.97792e70.07.75292e60.00.08.4423e80.00.00.00.00.06.10375e80.08.4423e88.4423e81.19209e-7
3MEME_natural_gas_combined_cycle3133.52336e70.00.08.77661e60.00.04.02739e62.26505e70.03.33663e60.00.02.19267e80.00.00.00.00.01.45243e80.02.19267e82.19267e80.0
4MAMA_solar_pv1141.27007e90.00.02.79327e80.00.00.00.00.00.00.00.01.5494e90.00.00.00.00.00.00.01.5494e91.5494e9-2.86102e-6
5CTCT_onshore_wind2151.40748e90.00.06.25617e80.00.02.90683e60.00.00.00.00.02.036e90.00.00.00.00.00.00.02.036e92.036e9-5.00679e-6
6CTCT_solar_pv2161.35108e90.00.02.97142e80.00.00.00.00.00.00.00.01.64822e90.00.00.00.00.00.00.01.64822e91.64822e99.53674e-7
7MEME_onshore_wind3171.03673e90.00.04.60821e80.00.02.625e60.00.00.00.00.01.50017e90.00.00.00.00.00.00.01.50017e91.50017e92.38419e-6
8MAMA_battery1084.29792e72.23673e80.01.07426e75.59033e70.07.59532e50.08.97367e50.01.3432e80.04.48833e80.00.00.00.00.00.00.04.48833e84.69275e8-2.0442e7
9CTCT_battery2091.08405e85.73615e80.02.70957e71.43365e80.02.1875e60.02.58447e60.05.24177e80.01.31941e90.00.00.00.00.00.00.01.31941e91.38143e9-6.20165e7
10MEME_battery30103.58043e71.03994e80.08.94925e62.59915e70.07.35552e50.08.69036e50.03.81057e70.02.03732e80.00.00.00.00.00.00.02.03732e82.14449e8-1.0717e7
+``` + + + +```julia +xnames = netrevenue[!,2] +names1 = ["Investment cost" "Fixed OM cost" "Variable OM cost" "Fuel cost" "Start Cost" "Battery charge cost" "CO2 Sequestration Cost" "Revenue"] + +netrev = [netrevenue[!,6]+netrevenue[!,7]+netrevenue[!,8] netrevenue[!,9]+netrevenue[!,11]+netrevenue[!,11] netrevenue[!,12]+netrevenue[!,14] netrevenue[!,13] netrevenue[!,15] netrevenue[!,16] netrevenue[!,17]] + +groupedbar(xnames,netrev, bar_position = :stack, bar_width=0.9,size=(850,800), + labels=names1,title="Cost Allocation per Node with Revenue",xlabel="Node",ylabel="Cost (Dollars)", + titlefontsize=10,legend=:outerright,ylims=[0,maximum(netrevenue[!,"Revenue"])+1e8],xrotation = 90) +StatsPlots.scatter!(xnames,netrevenue[!,"Revenue"],label="Revenue",color="black") + +``` + +![svg](./files/t8_cost.svg) + + + +### Emissions + +The file `emmissions.csv` gives the total CO2 emmissions per zone for each hour GenX runs. The first three rows give the marginal CO2 abatement cost in $/ton CO2. + + +```julia +emm1 = CSV.read(joinpath(case,"results/emissions.csv"),DataFrame) +``` + + + +``` @raw html +
1852×5 DataFrame
1827 rows omitted
RowZone123Total
String15Float64Float64Float64Float64
1CO2_Price_1444.9210.00.00.0
2CO2_Price_20.0468.6680.00.0
3CO2_Price_30.00.0240.860.0
4AnnualSum4.14279e61.30236e66.03017e56.04816e6
5t10.00.00.00.0
6t20.00.00.00.0
7t30.00.00.00.0
8t40.00.00.00.0
9t50.00.00.00.0
10t60.00.00.00.0
11t70.00.00.00.0
12t80.00.00.00.0
13t90.00.00.00.0
1841t18370.00.00.00.0
1842t18380.00.00.00.0
1843t18390.00.00.00.0
1844t18400.00.00.00.0
1845t18410.00.00.00.0
1846t18420.00.00.00.0
1847t18430.00.00.00.0
1848t18440.00.00.00.0
1849t18450.00.00.00.0
1850t18460.00.00.00.0
1851t18470.00.00.00.0
1852t18480.00.00.00.0
+``` + + + +```julia +# Pre-processing +tstart = 470 +tend = 1500 +names_emm = ["Zone 1","Zone 2","Zone 3"] + +emm_tot = DataFrame([emm1[3:end,2] emm1[3:end,3] emm1[3:end,4]], + ["Zone 1","Zone 2","Zone 3"]) + + +emm_plot = DataFrame([collect((tstart-3):(tend-3)) emm_tot[tstart:tend,1] repeat([names_emm[1]],(tend-tstart+1))], + ["Hour","MW","Zone"]); + +for i in range(2,3) + emm_plot_temp = DataFrame([collect((tstart-3):(tend-3)) emm_tot[tstart:tend,i] repeat([names_emm[i]],(tend-tstart+1))],["Hour","MW","Zone"]) + emm_plot = [emm_plot; emm_plot_temp] +end + +``` + + +```julia +emm_plot |> +@vlplot(mark={:line}, + x={:Hour,title="Time Step (hours)",labels="Zone:n",axis={values=tstart:24:tend}}, y={:MW,title="Emmissions (Tons)",type="quantitative"}, + color={"Zone:n"},width=845,height=400,title="Emmissions per Time Step by Zone") +``` + +![svg](./files/t8_emm1.svg) + + + + +Let's try changing the CO2 cap, as in Tutorial 7, and plotting the resulting emmissions. + + +```julia +genx_settings_TZ = YAML.load(open((joinpath(case,"settings/genx_settings.yml")))) +genx_settings_TZ["CO2Cap"] = 0 +YAML.write_file((joinpath(case,"settings/genx_settings.yml")), genx_settings_TZ) + +include("example_systems/1_three_zones/Run.jl") + +# run outside of notebook +``` + + Configuring Settings + Time Series Data Already Clustered. + Configuring Solver + Loading Inputs + Reading Input CSV Files + Network.csv Successfully Read! + Demand (load) data Successfully Read! + Fuels_data.csv Successfully Read! + + Summary of resources loaded into the model: + ------------------------------------------------------- + Resource type Number of resources + ======================================================= + Thermal 3 + VRE 4 + Storage 3 + ======================================================= + Total number of resources: 10 + ------------------------------------------------------- + + + Thermal.csv Successfully Read. + Vre.csv Successfully Read. + Storage.csv Successfully Read. + Resource_energy_share_requirement.csv Successfully Read. + Resource_capacity_reserve_margin.csv Successfully Read. + Resource_minimum_capacity_requirement.csv Successfully Read. + + + Generators_variability.csv Successfully Read! + Validating time basis + Minimum_capacity_requirement.csv Successfully Read! + CSV Files Successfully Read In From /Users/mayamutic/Desktop/GenX-Tutorials/Tutorials/example_systems/1_three_zones + Generating the Optimization Model + Discharge Module + Non-served Energy Module + Investment Discharge Module + Unit Commitment Module + Fuel Module + CO2 Module + Investment Transmission Module + Transmission Module + Dispatchable Resources Module + Storage Resources Module + Storage Investment Module + Storage Core Resources Module + Storage Resources with Symmetric Charge/Discharge Capacity Module + Thermal (Unit Commitment) Resources Module + Minimum Capacity Requirement Module + Time elapsed for model building is + 0.531860834 + Solving Model + Running HiGHS 1.6.0: Copyright (c) 2023 HiGHS under MIT licence terms + Presolving model + 118035 rows, 81083 cols, 422475 nonzeros + 110878 rows, 73926 cols, 422989 nonzeros + Presolve : Reductions: rows 110878(-42517); columns 73926(-46210); elements 422989(-48026) + Solving the presolved LP + IPX model has 110878 rows, 73926 columns and 422989 nonzeros + Input + Number of variables: 73926 + Number of free variables: 3696 + Number of constraints: 110878 + Number of equality constraints: 16867 + Number of matrix entries: 422989 + Matrix range: [4e-07, 1e+01] + RHS range: [8e-01, 2e+01] + Objective range: [1e-04, 7e+02] + Bounds range: [2e-03, 2e+01] + Preprocessing + Dualized model: no + Number of dense columns: 15 + Range of scaling factors: [5.00e-01, 1.00e+00] + IPX version 1.0 + Interior Point Solve + Iter P.res D.res P.obj D.obj mu Time + 0 2.34e+01 6.62e+02 3.28242911e+06 -1.30284671e+07 1.55e+04 0s + 1 1.39e+01 1.95e+02 -2.79051574e+06 -1.70869614e+07 8.32e+03 0s + 2 1.34e+01 1.41e+02 -2.86489620e+06 -3.99200815e+07 8.76e+03 0s + 3 4.75e+00 7.73e+01 -3.58904115e+06 -4.55608455e+07 4.46e+03 1s + Constructing starting basis... + 4 2.62e+00 2.77e+01 -1.46128616e+06 -3.92821768e+07 2.06e+03 3s + 5 2.29e+00 2.23e+01 -1.07522739e+06 -3.64123392e+07 1.79e+03 4s + 6 1.30e+00 6.60e+00 5.76572112e+04 -2.35071885e+07 8.03e+02 6s + 7 5.52e-02 1.21e+00 9.07716904e+05 -1.09217119e+07 1.39e+02 7s + 8 3.19e-03 1.35e-01 4.98206547e+05 -1.86042062e+06 2.08e+01 7s + 9 1.88e-04 3.20e-02 1.94049580e+05 -4.73698668e+05 5.30e+00 8s + 10 5.02e-05 7.56e-03 1.21122260e+05 -1.44306243e+05 1.78e+00 9s + 11 1.41e-05 1.14e-03 4.93526445e+04 -2.41004370e+04 4.23e-01 9s + 12 5.61e-06 1.68e-04 3.67745870e+04 -1.32012445e+04 2.72e-01 10s + 13 1.95e-06 1.01e-05 2.77016719e+04 -6.88123837e+03 1.86e-01 11s + 14 9.38e-07 4.53e-06 1.71337276e+04 -1.48902435e+03 1.00e-01 13s + 15 4.55e-07 2.12e-06 1.18334304e+04 1.03786061e+03 5.79e-02 14s + 16 2.04e-07 1.21e-06 9.18918668e+03 2.04003217e+03 3.84e-02 15s + 17 1.10e-07 6.34e-07 7.84163830e+03 3.03187846e+03 2.58e-02 17s + 18 5.85e-08 3.55e-07 7.07336591e+03 3.60947669e+03 1.86e-02 19s + 19 4.19e-08 1.93e-07 6.81537596e+03 4.04962353e+03 1.48e-02 22s + 20 2.17e-08 1.22e-07 6.38250114e+03 4.36184309e+03 1.08e-02 26s + 21 1.46e-08 8.65e-08 6.15373845e+03 4.59489784e+03 8.36e-03 28s + 22 1.45e-08 8.60e-08 6.21987475e+03 4.64840404e+03 8.43e-03 31s + 23 1.10e-08 6.52e-08 6.17121693e+03 4.72787295e+03 7.74e-03 33s + 24 8.82e-09 4.21e-08 6.08867860e+03 4.94663843e+03 6.13e-03 35s + 25 7.42e-09 1.59e-08 6.06378830e+03 5.01156108e+03 5.64e-03 37s + 26 7.08e-09 2.46e-09 6.05642307e+03 5.09371090e+03 5.16e-03 38s + 27 3.57e-09 1.59e-09 5.87880189e+03 5.21058424e+03 3.58e-03 40s + 28 1.95e-09 1.11e-09 5.81293790e+03 5.25218415e+03 3.01e-03 41s + 29 1.42e-09 7.21e-10 5.77482634e+03 5.32239130e+03 2.43e-03 43s + 30 1.35e-09 6.49e-10 5.77061907e+03 5.32860331e+03 2.37e-03 45s + 31 1.26e-09 5.90e-10 5.76739631e+03 5.33020034e+03 2.35e-03 46s + 32 1.08e-09 4.91e-10 5.75363003e+03 5.35203400e+03 2.15e-03 47s + 33 2.49e-14 4.26e-10 5.68794026e+03 5.36071156e+03 1.76e-03 47s + 34 2.13e-14 2.53e-10 5.66831172e+03 5.41753142e+03 1.35e-03 48s + 35 2.13e-14 1.06e-10 5.63886596e+03 5.49300645e+03 7.82e-04 49s + 36 2.13e-14 5.55e-11 5.61729546e+03 5.52199336e+03 5.11e-04 51s + 37 2.13e-14 2.59e-11 5.60778510e+03 5.54931828e+03 3.14e-04 52s + 38 2.13e-14 1.75e-11 5.60173021e+03 5.55566214e+03 2.47e-04 53s + 39 2.13e-14 1.18e-11 5.59813889e+03 5.56260835e+03 1.91e-04 54s + 40 2.13e-14 1.01e-11 5.59718690e+03 5.56442962e+03 1.76e-04 55s + 41 2.13e-14 1.00e-11 5.59698222e+03 5.56447950e+03 1.74e-04 55s + 42 2.13e-14 4.04e-12 5.59428165e+03 5.57215354e+03 1.19e-04 56s + 43 2.13e-14 2.50e-12 5.59133373e+03 5.57571709e+03 8.38e-05 56s + 44 2.13e-14 1.48e-12 5.59035970e+03 5.57874298e+03 6.23e-05 56s + 45 2.13e-14 1.22e-12 5.58936152e+03 5.57965257e+03 5.21e-05 57s + 46 2.13e-14 1.25e-12 5.58736745e+03 5.58061357e+03 3.62e-05 57s + 47 2.13e-14 5.68e-13 5.58697892e+03 5.58214126e+03 2.60e-05 57s + 48 2.13e-14 5.36e-13 5.58691900e+03 5.58233212e+03 2.46e-05 58s + 49 2.13e-14 3.73e-13 5.58656054e+03 5.58365417e+03 1.56e-05 58s + 50 2.13e-14 3.55e-13 5.58656104e+03 5.58367145e+03 1.55e-05 58s + 51 2.13e-14 2.31e-13 5.58641950e+03 5.58394090e+03 1.33e-05 59s + 52 2.13e-14 2.56e-13 5.58608647e+03 5.58430397e+03 9.56e-06 59s + 53 2.13e-14 1.43e-13 5.58604712e+03 5.58455329e+03 8.01e-06 59s + 54 2.13e-14 3.13e-13 5.58604145e+03 5.58455679e+03 7.96e-06 59s + 55 2.13e-14 1.99e-13 5.58598248e+03 5.58506295e+03 4.93e-06 60s + 56 2.13e-14 2.56e-13 5.58593821e+03 5.58507236e+03 4.64e-06 60s + 57 2.13e-14 1.99e-13 5.58578478e+03 5.58540690e+03 2.03e-06 60s + 58 2.84e-14 2.91e-13 5.58578450e+03 5.58540754e+03 2.02e-06 61s + 59 2.13e-14 2.56e-13 5.58572083e+03 5.58541744e+03 1.63e-06 61s + 60 2.84e-14 2.56e-13 5.58571491e+03 5.58541894e+03 1.59e-06 61s + 61 2.13e-14 1.63e-13 5.58565078e+03 5.58546281e+03 1.01e-06 61s + 62 2.13e-14 3.41e-13 5.58557843e+03 5.58548803e+03 4.85e-07 62s + 63 2.13e-14 3.98e-13 5.58557613e+03 5.58548563e+03 4.85e-07 62s + 64 2.13e-14 3.69e-13 5.58556537e+03 5.58552541e+03 2.14e-07 62s + 65 2.13e-14 3.13e-13 5.58556537e+03 5.58552559e+03 2.13e-07 62s + 66 2.13e-14 1.42e-13 5.58555314e+03 5.58553125e+03 1.17e-07 63s + 67 2.13e-14 1.74e-13 5.58555081e+03 5.58553284e+03 9.64e-08 63s + 68 2.13e-14 2.13e-13 5.58554989e+03 5.58553484e+03 8.07e-08 63s + 69 2.13e-14 5.68e-13 5.58554752e+03 5.58553671e+03 5.80e-08 63s + 70 2.13e-14 4.83e-13 5.58554607e+03 5.58553831e+03 4.16e-08 64s + 71 2.13e-14 2.13e-13 5.58554582e+03 5.58554198e+03 2.06e-08 64s + 72 2.13e-14 8.92e-13 5.58554574e+03 5.58554196e+03 2.03e-08 64s + 73 2.13e-14 1.09e-12 5.58554539e+03 5.58554200e+03 1.82e-08 64s + 74 2.13e-14 3.23e-12 5.58554405e+03 5.58554312e+03 4.99e-09 65s + 75 2.13e-14 5.31e-12 5.58554382e+03 5.58554334e+03 2.58e-09 65s + 76 2.13e-14 7.04e-12 5.58554366e+03 5.58554353e+03 7.22e-10 65s + 77* 2.84e-14 1.57e-12 5.58554362e+03 5.58554357e+03 2.79e-10 65s + 78* 3.55e-14 4.18e-12 5.58554360e+03 5.58554358e+03 1.16e-10 65s + 79* 3.55e-14 5.29e-12 5.58554360e+03 5.58554360e+03 2.36e-11 66s + 80* 3.55e-14 5.19e-12 5.58554360e+03 5.58554360e+03 3.40e-12 66s + 81* 3.55e-14 1.16e-11 5.58554360e+03 5.58554360e+03 3.27e-13 66s + 82* 3.55e-14 9.05e-12 5.58554360e+03 5.58554360e+03 2.97e-14 66s + Running crossover as requested + Primal residual before push phase: 9.82e-08 + Dual residual before push phase: 1.24e-07 + Number of dual pushes required: 18968 + Number of primal pushes required: 2204 + Summary + Runtime: 66.29s + Status interior point solve: optimal + Status crossover: optimal + objective value: 5.58554360e+03 + interior solution primal residual (abs/rel): 1.51e-10 / 8.54e-12 + interior solution dual residual (abs/rel): 8.46e-10 / 1.20e-12 + interior solution objective gap (abs/rel): 2.29e-09 / 4.10e-13 + basic solution primal infeasibility: 1.43e-14 + basic solution dual infeasibility: 6.89e-16 + Ipx: IPM optimal + Ipx: Crossover optimal + Solving the original LP from the solution after postsolve + Model status : Optimal + IPM iterations: 82 + Crossover iterations: 1447 + Objective value : 5.5855435982e+03 + HiGHS run time : 66.51 + LP solved for primal + Writing Output + Time elapsed for writing costs is + 0.099885792 + Time elapsed for writing capacity is + 0.000646583 + Time elapsed for writing power is + 0.021790625 + Time elapsed for writing charge is + 0.0167645 + Time elapsed for writing capacity factor is + 0.021259458 + Time elapsed for writing storage is + 0.009532667 + Time elapsed for writing curtailment is + 0.019054083 + Time elapsed for writing nse is + 0.0452305 + Time elapsed for writing power balance is + 0.053504209 + Time elapsed for writing transmission flows is + 0.004709417 + Time elapsed for writing transmission losses is + 0.013975458 + Time elapsed for writing network expansion is + 0.000157 + Time elapsed for writing emissions is + 0.050411042 + Time elapsed for writing reliability is + 0.005842667 + Time elapsed for writing storage duals is + 0.024307708 + Time elapsed for writing commitment is + 0.006124458 + Time elapsed for writing startup is + 0.012590917 + Time elapsed for writing shutdown is + 0.012514292 + Time elapsed for writing fuel consumption is + 0.054159667 + Time elapsed for writing co2 is + 0.019371417 + Time elapsed for writing price is + 0.005712875 + Time elapsed for writing energy revenue is + 0.010585041 + Time elapsed for writing charging cost is + 0.005354792 + Time elapsed for writing subsidy is + 0.000396208 + Time elapsed for writing time weights is + 0.000497875 + Time elapsed for writing minimum capacity requirement is + 0.000146875 + Time elapsed for writing net revenue is + 0.011134208 + Wrote outputs to /Users/mayamutic/Desktop/GenX-Tutorials/Tutorials/example_systems/1_three_zones/results_1 + Time elapsed for writing is + 0.530491792 + + + +```julia +emm2 = CSV.read(joinpath(case,"results_1/emissions.csv"),DataFrame) +``` + + + +``` @raw html +
1849×5 DataFrame
1824 rows omitted
RowZone123Total
String15Float64Float64Float64Float64
1AnnualSum1.68155e71.41088e74310.213.09286e7
2t1997.1690.00.0997.169
3t2997.1690.00.0997.169
4t3997.1690.00.0997.169
5t4997.1690.00.0997.169
6t5997.1690.00.0997.169
7t6997.1690.00.0997.169
8t7997.1690.00.0997.169
9t8997.1690.00.0997.169
10t9997.1690.00.0997.169
11t101471.460.00.01471.46
12t11997.1690.00.0997.169
13t121115.810.00.01115.81
1838t18372789.351012.990.03802.34
1839t18382835.211012.990.03848.2
1840t18392520.571012.990.03533.56
1841t18401496.47445.850.01942.32
1842t18412571.261012.990.03584.25
1843t18422835.211012.990.03848.2
1844t18432835.211012.990.03848.2
1845t18442625.42960.1840.03585.6
1846t18452506.32342.3910.02848.71
1847t18462277.59342.3910.02619.98
1848t18471960.08524.5260.02484.6
1849t18481566.77342.3910.01909.16
+``` + + + +```julia +# Pre-processing +tstart = 470 +tend = 1500 +names_emm = ["Zone 1","Zone 2","Zone 3"] + +emm_tot2 = DataFrame([emm2[3:end,2] emm2[3:end,3] emm2[3:end,4]], + ["Zone 1","Zone 2","Zone 3"]) + + +emm_plot2 = DataFrame([collect((tstart-3):(tend-3)) emm_tot2[tstart:tend,1] repeat([names_emm[1]],(tend-tstart+1))], + ["Hour","MW","Zone"]); + +for i in range(2,3) + emm_plot_temp = DataFrame([collect((tstart-3):(tend-3)) emm_tot2[tstart:tend,i] repeat([names_emm[i]],(tend-tstart+1))],["Hour","MW","Zone"]) + emm_plot2 = [emm_plot2; emm_plot_temp] +end +``` + + +```julia +emm_plot2 |> +@vlplot(mark={:line}, + x={:Hour,title="Time Step (hours)",labels="Zone:n",axis={values=tstart:24:tend}}, y={:MW,title="Emmissions (Tons)",type="quantitative"}, + color={"Zone:n"},width=845,height=400,title="Emmissions per Time Step by Zone") +``` + +![svg](./files/t8_emm2.svg) + + + + +We can see how the emmissions, summed over all zones, compare in the following plot: + + +```julia +emm1sum = sum(eachcol(emm_tot)); +emm2sum = sum(eachcol(emm_tot2)); + +Plots.plot(collect((tstart-3):(tend-3)),emm1sum[tstart:tend],size=(800,400),label="Load Based CO2 Cap", + xlabel="Time Step (Hours)",ylabel="Emmissions (Tons)",thickness_scaling = 1.1,linewidth = 1.5, + title="Emmisions per Time Step",xticks=tstart:72:tend) +Plots.plot!(collect((tstart-3):(tend-3)),emm2sum[tstart:tend],label="No CO2 Cap",linewidth = 1.5) +``` +![svg](./files/t8_emm_comp.svg) + + + +Finally, set the CO2 Cap back to 2: + + +```julia +genx_settings_TZ["CO2Cap"] = 2 +YAML.write_file((joinpath(case,"settings/genx_settings.yml")), genx_settings_TZ) +``` + + +```julia + +``` diff --git a/docs/src/Tutorials/Tutorials_intro.md b/docs/src/Tutorials/Tutorials_intro.md index 014e215b39..62e59083c9 100644 --- a/docs/src/Tutorials/Tutorials_intro.md +++ b/docs/src/Tutorials/Tutorials_intro.md @@ -10,5 +10,7 @@ Here is a list of the tutorials: 4. [Tutorial 4: Model Generation](@ref) 5. [Tutorial 5: Solving the Model](@ref) 6. [Tutorial 6: Solver Settings](@ref) +7. [Tutorial 7: Policy Constraints](@ref) +8. [Tutorial 8: Outputs](@ref) diff --git a/docs/src/Tutorials/files/Julia.png b/docs/src/Tutorials/files/Julia.png new file mode 100644 index 0000000000000000000000000000000000000000..e881b2fa4b2206b3975d0ddaf8fe630e2e3e87f0 GIT binary patch literal 80822 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 F{|{+s1xNq@ literal 0 HcmV?d00001 diff --git a/docs/src/Tutorials/files/LatexHierarchy.png b/docs/src/Tutorials/files/LatexHierarchy.png index 05981f99c061b62c12eec392a9e3a766270c351a..9e7c7295f993181f35044c56b9104978ac9ad89b 100644 GIT binary patch literal 43427 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 literal 93747 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