diff --git a/.gitignore b/.gitignore
index d760600..0d2614f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,13 +1,13 @@
-*.csv
-*.jld2
-*.json
-*.pyc
-*.log
-.DS_Store
-.vscode
-*.txt
-*.ipynb_checkpoints
-*.sh
-*.ipynb
-*.json
+*.csv
+*.jld2
+*.json
+*.pyc
+*.log
+.DS_Store
+.vscode
+*.txt
+*.ipynb_checkpoints
+*.sh
+*.ipynb
+*.json
Manifest.toml
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index 37f5fcd..52a1b7d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,29 +1,29 @@
-BSD 3-Clause License
-
-Copyright (c) 2019, Alliance for Sustainable Energy, LLC
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright notice, this
- list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright notice,
- this list of conditions and the following disclaimer in the documentation
- and/or other materials provided with the distribution.
-
-* Neither the name of the copyright holder nor the names of its
- contributors may be used to endorse or promote products derived from
- this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+BSD 3-Clause License
+
+Copyright (c) 2019, Alliance for Sustainable Energy, LLC
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Project.toml b/Project.toml
index 7200ac7..6d89ef7 100644
--- a/Project.toml
+++ b/Project.toml
@@ -12,6 +12,7 @@ DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
+EMISExtensions = "ea396fd1-c9da-422a-af29-096b7307fcdf"
FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
InfrastructureSystems = "2cd47ed4-ca9b-11e9-27f2-ab636a7671f1"
InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
@@ -21,11 +22,15 @@ LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
MinCostFlows = "62286e6e-1779-56f1-888a-1c0056788ce0"
PRAS = "05348d26-1c52-11e9-35e3-9d51842d34b9"
+PooledArrays = "2dfb63ee-cc39-5dd5-95bd-886bf059d720"
PowerSimulations = "e690365d-45e2-57bb-ac84-44ba829e73c4"
PowerSystems = "bcd98974-b02a-5e2f-9ee0-a103f5c450dd"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
+ReliablePowerSimulations = "52e8e31b-eef8-4797-aa92-cb78ffe3b27e"
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
TimeSeries = "9e3dc215-6440-5c97-bce1-76c03772f85e"
+TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
+Xpress = "9e70acf3-d6c9-5be6-b5bd-4e2c73e3e054"
diff --git a/README.md b/README.md
index e9e0869..6f8cea2 100644
--- a/README.md
+++ b/README.md
@@ -1,66 +1,66 @@
-# EMIS AgentSimulation Model
-
-The Electricity Markets Investment Suite Agent-based Simulation (EMIS-AS)
-model is an agent-based model developed at NREL for simulating annual investment
-and retirement decisions of heterogeneous investors in the electricity sector.
-EMIS-AS is designed to capture the evolution of the electricity generation portfolio
-resulting from the interactions of heterogeneous investors under different policy
-and market designs. EMIS-AS not only allows end-users to customize market products
-and rules, but also to capture investors' heterogeneous financing parameters,
-technology preferences, beliefs about the future (forecasts), ability to update
-those forecasts, and risk preferences under uncertainty.
-
-## Model Structure
-
-
-
-EMIS-AS is initialized based on the input data detailing the test system, generation
-units, market design, investors' characteristics and simulation parameters. Additionally,
-the pre-processing step includes calculation of the weighted average cost of capital
-(WACC) and adjusted CAPEX for all existing and option projects for each investor.
-Next, the model selects the specified number of representative days (with hourly
-resolution) to be used by the investors for price prediction. These representative
-days and their corresponding weights are determined based on load and renewable
-generation timeseries using k-medoids clustering.
-
-Subsequently, the simulation commences with successive yearly iterations for the specified
-simulation horizon. At the start of each simulation year, the investors project future
-prices and make revenue predictions. The predicted revenues are then used to calculate the
-expected utility (based on the expected net present value (NPV)) for all projects.
-Subsequently, the revenue prediction outcomes and expected utility are used to make
-investment and retirement decisions and for calculation of market bids.
-
-
-Once all investors have completed their decision-making process, their investment and
-retirement decisions are announced. The model then updates the build-phases of all
-projects, which are depicted below.
-
-
-
-The projects in the Option phase which get decided to be invested in are sent to the
-interconnection queue and included in the list of active projects. The projects already
-in the interconnection queue progress one year further in the queue.
-The projects which have completed their queue time then enter the construction phase. It
-is assumed that the investors only have to pay the annual queue costs when the projects are
-in the queue, but would have to pay the full capital cost once construction commences.
-The projects which have completed their construction times are included in the set of existing
-projects. Finally, if any project is to be retired, either due to end-of-life or profitability
-reasons, is removed from the list of active projects.
-
-The life cycle phase updates are followed by the actual clearing of the modeled electricity
-markets. The current version of EMIS-AS includes energy, ancillary services, capacity and
-Renewable Energy Certificates (REC) markets. The investors then update the realized revenues
-for their projects and also update their beliefs (using Kalman Filters) based on the realized
-values of the parameters. Finally, the derating factors of variable renewable generation
-are calculated based on the updated installed capacities using the top-n net-load hour methodology.
-
-## Loading Data and Simulation Creation
-
-While the model's input data for investors and markets can be instantiated directly as
-Julia structs, the model provides a CSV-based file import functionality that may be
-more convenient. Data organized according to the file structure
-[described here](input_file_structure.html) will be automatically populated into a
-corresponding `AgentSimulation`. The worklow for processing the input data for creating
-the `AgentSimulation` is depicted below.
-
+# EMIS AgentSimulation Model
+
+The Electricity Markets Investment Suite Agent-based Simulation (EMIS-AS)
+model is an agent-based model developed at NREL for simulating annual investment
+and retirement decisions of heterogeneous investors in the electricity sector.
+EMIS-AS is designed to capture the evolution of the electricity generation portfolio
+resulting from the interactions of heterogeneous investors under different policy
+and market designs. EMIS-AS not only allows end-users to customize market products
+and rules, but also to capture investors' heterogeneous financing parameters,
+technology preferences, beliefs about the future (forecasts), ability to update
+those forecasts, and risk preferences under uncertainty.
+
+## Model Structure
+
+
+
+EMIS-AS is initialized based on the input data detailing the test system, generation
+units, market design, investors' characteristics and simulation parameters. Additionally,
+the pre-processing step includes calculation of the weighted average cost of capital
+(WACC) and adjusted CAPEX for all existing and option projects for each investor.
+Next, the model selects the specified number of representative days (with hourly
+resolution) to be used by the investors for price prediction. These representative
+days and their corresponding weights are determined based on load and renewable
+generation timeseries using k-medoids clustering.
+
+Subsequently, the simulation commences with successive yearly iterations for the specified
+simulation horizon. At the start of each simulation year, the investors project future
+prices and make revenue predictions. The predicted revenues are then used to calculate the
+expected utility (based on the expected net present value (NPV)) for all projects.
+Subsequently, the revenue prediction outcomes and expected utility are used to make
+investment and retirement decisions and for calculation of market bids.
+
+
+Once all investors have completed their decision-making process, their investment and
+retirement decisions are announced. The model then updates the build-phases of all
+projects, which are depicted below.
+
+
+
+The projects in the Option phase which get decided to be invested in are sent to the
+interconnection queue and included in the list of active projects. The projects already
+in the interconnection queue progress one year further in the queue.
+The projects which have completed their queue time then enter the construction phase. It
+is assumed that the investors only have to pay the annual queue costs when the projects are
+in the queue, but would have to pay the full capital cost once construction commences.
+The projects which have completed their construction times are included in the set of existing
+projects. Finally, if any project is to be retired, either due to end-of-life or profitability
+reasons, is removed from the list of active projects.
+
+The life cycle phase updates are followed by the actual clearing of the modeled electricity
+markets. The current version of EMIS-AS includes energy, ancillary services, capacity and
+Renewable Energy Certificates (REC) markets. The investors then update the realized revenues
+for their projects and also update their beliefs (using Kalman Filters) based on the realized
+values of the parameters. Finally, the derating factors of variable renewable generation
+are calculated based on the updated installed capacities using the top-n net-load hour methodology.
+
+## Loading Data and Simulation Creation
+
+While the model's input data for investors and markets can be instantiated directly as
+Julia structs, the model provides a CSV-based file import functionality that may be
+more convenient. Data organized according to the file structure
+[described here](input_file_structure.html) will be automatically populated into a
+corresponding `AgentSimulation`. The worklow for processing the input data for creating
+the `AgentSimulation` is depicted below.
+
\ No newline at end of file
diff --git a/input_file_structure.html b/input_file_structure.html
index a69a8bf..89b672c 100644
--- a/input_file_structure.html
+++ b/input_file_structure.html
@@ -1,424 +1,424 @@
-
-
-
-
-
-
-
-
-
-- Base Directory/
- -
- Homogeneous/ - File Structure of Homogeneous Runs is the same as that of Heterogeneous, but with the same financial characteristics and technology preferences for all investors
-
-
- -
- Heterogeneous/
-
- -
- markets_data/
-
- -
- markets.csv - Booleans for which markets are to be included
-
- - Energy
- - Reserves
- - Capacity
- - REC
-
-
- -
- energy_mkt_param.csv
-
-
- -
- reserve_up_mkt_param.csv
-
- - zones
- - price_cap
- - ORDC - Boolean for whether an Operating Reserve Demand Curve is modeled
- - ORDC_points - Number of break points of ORDC
- - ORDC_x{} - x-coordinates of ORDC
- - ORDC_y{} - y-coordinates of ORDC
-
-
- -
- reserve_down_mkt_param.csv
-
-
- -
- capacity_mkt_param.csv
-
- - Forecast Pool Req
- - Net CONE per day
- - Gross CONE per day
- - IRM
- - IRM perc points
- - Net CONE perc points
- - Max Clear
- - forward_years
- - num_top_hours - For derating factor calculation
-
-
- -
- REC_mkt_param.csv
-
- - price_cap
- - rec_req
- - annual_increment
-
-
- -
- derating_dict.csv - Contains fixed derating factors for all modeled technologies other than Variable Renewables
-
- - ST - Coal-powered Steam Turbine
- - NU_ST - Nuclear-powered Steam Turbine
- - CC - Natural Gas Combined Cycle
- - CT - Natural Gas Combustion Turbine
- - HY - Hydropower
- - BA_{duration} - Duration-limited Battery
-
-
- -
- annual_growth.csv - Name and data of parameters whose annual growth rates are modeled are included here
-
- - year
- - {parameter_name}
-
-
- -
- symmetric_belief.csv - Parameters to be used if all investors share the same belief about the future
-
- - parameters
- - initial_estimate
- - initial_error_cov
- - process_cov
- - measurement_cov
-
-
- -
- symmetric_scenario_multiplier_data.csv - Parameters to be used to model uncertain scenarios if all investors share the same belief about the future
-
- - scenario
- - probability
- - {parameter_name}
-
-
-
-
- -
- investors/
-
- -
- {investor_name}/ - One folder for each investor
-
- -
- characteristics.csv
-
- - risk_preference
- - uf_constant - *uf = Utility Function
- - uf_multiplier
- - uf_risk_coefficient
- - capital_cost_multiplier
- - max_annual_projects
- - discount_rate_adder
-
-
- -
- finance_params.csv - Financing data obtained from Annual Technology Baseline (ATB)
-
- - Category
- - ITC 2020
- - ITC 2021
- - ITC post 2021
- - GRANTS
- - ITC_RECOVERY_FACTOR
- - PTC_RECOVERY_FACTOR
- - TAX_RATE
- - PTC_RECOVERY_FACTOR
- - DEPRECIABLE_PERCENT
- - MACRS_DEPRECIATION_YEARS
- - PROJECT_SPECIFIC_ADDER
- - DEBT_INTEREST_RATE
- - EQUITY_RATE
- - DEBT_FRACTION
-
-
- -
- MACRS_Schedule.csv - Standard MACRS Depreciation Schedules
-
- - Y5
- - Y7
- - Y10
- - Y15
- - Y20
-
-
- -
- project_capex.csv - Annual Technology Capital Cost Data
-
- - Category
- - pre 2020
- - {year}
-
-
- -
- projectexisting.csv - Parameters of investor's existing projects at the start of the simulation. Other operational data is directly extracted from the test system based on the model requirements.
-
- - GEN_UID
- - Unit Type
- - Fuel
- - Category
- - Size
- - Min Gen pu
- - Ramp Rate pu/Hr
- - Input Power Rating pu
- - Output Power Rating pu
- - Min Storgae pu
- - Duration Hr
- - Round Trip Efficiency pu
- - Fuel Price $/MMBTU
- - HR_avg_0
- - Lagtime
- - Online Year
- - Capex Years
- - Lifetime
- - Fixed OM Cost per MW
- - Zone
- - Bus ID
- - Capacity Eligible
- - REC Eligible
-
-
- -
- projectoptions.csv - Parameters of investor's option projects which can be invested in. Contains additional operational data which would be needed to create SIIP PSY Devices
-
- - GEN_UID
- - Unit Type
- - Fuel
- - Category
- - Size
- - Min Gen pu
- - Min Up Time Hr
- - Min Down Time Hr
- - Shut Down Cost
- - Ramp Rate pu/Hr
- - Input Power Rating pu
- - Output Power Rating pu
- - Min Storgae pu
- - Duration Hr
- - Round Trip Efficiency pu
- - Fuel Price $/MMBTU
- - Start Heat Cold MBTU
- - Output_pct_0
- - Output_pct_1
- - Output_pct_2
- - Output_pct_3
- - Output_pct_4
- - HR_avg_0
- - HR_incr_1
- - HR_incr_2
- - HR_incr_3
- - HR_incr_4
- - Lagtime
- - Capex Years
- - Lifetime
- - Fixed OM Cost per MW
- - Zone
- - Bus ID
- - Capacity Eligible
- - REC Eligible
-
-
- -
- size_dict.csv -Size in MW of Small, Medium and Large units of modeled generation technology types.
-
-
-
- -
- markets_data/ - Market parameters for investors' price prediction.
-
- -
- energy_mkt_param.csv
-
-
- -
- reserve_up_mkt_param.csv
-
- - zones
- - price_cap
- - ORDC - Boolean for whether an Operating Reserve Demand Curve is modeled
- - ORDC_points - Number of break points of ORDC
- - ORDC_x{} - x-coordinates of ORDC
- - ORDC_y{} - y-coordinates of ORDC
-
-
- -
- reserve_down_mkt_param.csv
-
-
- -
- capacity_mkt_param.csv
-
- - Forecast Pool Req
- - Net CONE per day
- - Gross CONE per day
- - IRM
- - IRM perc points
- - Net CONE perc points
- - Max Clear
- - forward_years
-
-
- -
- REC_mkt_param.csv
-
- - price_cap
- - rec_req
- - annual_increment
-
-
- -
- investor_belief.csv - Investor's belief about the future
-
- - parameters
- - initial_estimate
- - initial_error_cov
- - process_cov
- - measurement_cov
-
-
- -
- scenario_multiplier_data.csv - Parameters to be used to model uncertain scenarios
-
- - scenario
- - probability
- - {parameter_name}
-
-
-
-
-
-
-
-
-
-
- -
- queue_cost_data.csv - Queue Cost structure is based on MISO's interconnection queue implementaion (https://www.misoenergy.org/planning/generator-interconnection/)
-
- - Ref
- - less than 6 MW
- - 6 to 20 MW
- - 20 to 50 MW
- - 50 to 100 MW
- - 100 to 200 MW
- - 200 to 500 MW
- - 500 to 1000 MW
- - greater than 1000 MW
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+- Base Directory/
+ -
+ Homogeneous/ - File Structure of Homogeneous Runs is the same as that of Heterogeneous, but with the same financial characteristics and technology preferences for all investors
+
+
+ -
+ Heterogeneous/
+
+ -
+ markets_data/
+
+ -
+ markets.csv - Booleans for which markets are to be included
+
+ - Energy
+ - Reserves
+ - Capacity
+ - REC
+
+
+ -
+ energy_mkt_param.csv
+
+
+ -
+ reserve_up_mkt_param.csv
+
+ - zones
+ - price_cap
+ - ORDC - Boolean for whether an Operating Reserve Demand Curve is modeled
+ - ORDC_points - Number of break points of ORDC
+ - ORDC_x{} - x-coordinates of ORDC
+ - ORDC_y{} - y-coordinates of ORDC
+
+
+ -
+ reserve_down_mkt_param.csv
+
+
+ -
+ capacity_mkt_param.csv
+
+ - Forecast Pool Req
+ - Net CONE per day
+ - Gross CONE per day
+ - IRM
+ - IRM perc points
+ - Net CONE perc points
+ - Max Clear
+ - forward_years
+ - num_top_hours - For derating factor calculation
+
+
+ -
+ REC_mkt_param.csv
+
+ - price_cap
+ - rec_req
+ - annual_increment
+
+
+ -
+ derating_dict.csv - Contains fixed derating factors for all modeled technologies other than Variable Renewables
+
+ - ST - Coal-powered Steam Turbine
+ - NU_ST - Nuclear-powered Steam Turbine
+ - CC - Natural Gas Combined Cycle
+ - CT - Natural Gas Combustion Turbine
+ - HY - Hydropower
+ - BA_{duration} - Duration-limited Battery
+
+
+ -
+ annual_growth.csv - Name and data of parameters whose annual growth rates are modeled are included here
+
+ - year
+ - {parameter_name}
+
+
+ -
+ symmetric_belief.csv - Parameters to be used if all investors share the same belief about the future
+
+ - parameters
+ - initial_estimate
+ - initial_error_cov
+ - process_cov
+ - measurement_cov
+
+
+ -
+ symmetric_scenario_multiplier_data.csv - Parameters to be used to model uncertain scenarios if all investors share the same belief about the future
+
+ - scenario
+ - probability
+ - {parameter_name}
+
+
+
+
+ -
+ investors/
+
+ -
+ {investor_name}/ - One folder for each investor
+
+ -
+ characteristics.csv
+
+ - risk_preference
+ - uf_constant - *uf = Utility Function
+ - uf_multiplier
+ - uf_risk_coefficient
+ - capital_cost_multiplier
+ - max_annual_projects
+ - discount_rate_adder
+
+
+ -
+ finance_params.csv - Financing data obtained from Annual Technology Baseline (ATB)
+
+ - Category
+ - ITC 2020
+ - ITC 2021
+ - ITC post 2021
+ - GRANTS
+ - ITC_RECOVERY_FACTOR
+ - PTC_RECOVERY_FACTOR
+ - TAX_RATE
+ - PTC_RECOVERY_FACTOR
+ - DEPRECIABLE_PERCENT
+ - MACRS_DEPRECIATION_YEARS
+ - PROJECT_SPECIFIC_ADDER
+ - DEBT_INTEREST_RATE
+ - EQUITY_RATE
+ - DEBT_FRACTION
+
+
+ -
+ MACRS_Schedule.csv - Standard MACRS Depreciation Schedules
+
+ - Y5
+ - Y7
+ - Y10
+ - Y15
+ - Y20
+
+
+ -
+ project_capex.csv - Annual Technology Capital Cost Data
+
+ - Category
+ - pre 2020
+ - {year}
+
+
+ -
+ projectexisting.csv - Parameters of investor's existing projects at the start of the simulation. Other operational data is directly extracted from the test system based on the model requirements.
+
+ - GEN_UID
+ - Unit Type
+ - Fuel
+ - Category
+ - Size
+ - Min Gen pu
+ - Ramp Rate pu/Hr
+ - Input Power Rating pu
+ - Output Power Rating pu
+ - Min Storgae pu
+ - Duration Hr
+ - Round Trip Efficiency pu
+ - Fuel Price $/MMBTU
+ - HR_avg_0
+ - Lagtime
+ - Online Year
+ - Capex Years
+ - Lifetime
+ - Fixed OM Cost per MW
+ - Zone
+ - Bus ID
+ - Capacity Eligible
+ - REC Eligible
+
+
+ -
+ projectoptions.csv - Parameters of investor's option projects which can be invested in. Contains additional operational data which would be needed to create SIIP PSY Devices
+
+ - GEN_UID
+ - Unit Type
+ - Fuel
+ - Category
+ - Size
+ - Min Gen pu
+ - Min Up Time Hr
+ - Min Down Time Hr
+ - Shut Down Cost
+ - Ramp Rate pu/Hr
+ - Input Power Rating pu
+ - Output Power Rating pu
+ - Min Storgae pu
+ - Duration Hr
+ - Round Trip Efficiency pu
+ - Fuel Price $/MMBTU
+ - Start Heat Cold MBTU
+ - Output_pct_0
+ - Output_pct_1
+ - Output_pct_2
+ - Output_pct_3
+ - Output_pct_4
+ - HR_avg_0
+ - HR_incr_1
+ - HR_incr_2
+ - HR_incr_3
+ - HR_incr_4
+ - Lagtime
+ - Capex Years
+ - Lifetime
+ - Fixed OM Cost per MW
+ - Zone
+ - Bus ID
+ - Capacity Eligible
+ - REC Eligible
+
+
+ -
+ size_dict.csv -Size in MW of Small, Medium and Large units of modeled generation technology types.
+
+
+
+ -
+ markets_data/ - Market parameters for investors' price prediction.
+
+ -
+ energy_mkt_param.csv
+
+
+ -
+ reserve_up_mkt_param.csv
+
+ - zones
+ - price_cap
+ - ORDC - Boolean for whether an Operating Reserve Demand Curve is modeled
+ - ORDC_points - Number of break points of ORDC
+ - ORDC_x{} - x-coordinates of ORDC
+ - ORDC_y{} - y-coordinates of ORDC
+
+
+ -
+ reserve_down_mkt_param.csv
+
+
+ -
+ capacity_mkt_param.csv
+
+ - Forecast Pool Req
+ - Net CONE per day
+ - Gross CONE per day
+ - IRM
+ - IRM perc points
+ - Net CONE perc points
+ - Max Clear
+ - forward_years
+
+
+ -
+ REC_mkt_param.csv
+
+ - price_cap
+ - rec_req
+ - annual_increment
+
+
+ -
+ investor_belief.csv - Investor's belief about the future
+
+ - parameters
+ - initial_estimate
+ - initial_error_cov
+ - process_cov
+ - measurement_cov
+
+
+ -
+ scenario_multiplier_data.csv - Parameters to be used to model uncertain scenarios
+
+ - scenario
+ - probability
+ - {parameter_name}
+
+
+
+
+
+
+
+
+
+
+ -
+ queue_cost_data.csv - Queue Cost structure is based on MISO's interconnection queue implementaion (https://www.misoenergy.org/planning/generator-interconnection/)
+
+ - Ref
+ - less than 6 MW
+ - 6 to 20 MW
+ - 20 to 50 MW
+ - 50 to 100 MW
+ - 100 to 200 MW
+ - 200 to 500 MW
+ - 500 to 1000 MW
+ - greater than 1000 MW
+
+
+
+
+
+
+
diff --git a/src/EMISAgentSimulation.jl b/src/EMISAgentSimulation.jl
index a91b316..75bd99e 100644
--- a/src/EMISAgentSimulation.jl
+++ b/src/EMISAgentSimulation.jl
@@ -4,6 +4,7 @@ module EMISAgentSimulation
# Exports
# Export Structs
+using Base: Tuple, Float64, project_deps_get
export CaseDefinition
export AgentSimulation
@@ -36,6 +37,8 @@ export Energy
export Capacity
export REC
export CarbonTax
+export Inertia
+export ResourceAdequacy
export BuildPhase
export Existing
@@ -60,6 +63,7 @@ export RenewableGenEMIS
export ThermalTech
export ThermalGenEMIS
+export ThermalFastStartSIIP
export ZonalLine
@@ -70,6 +74,7 @@ export ReserveDownMarket
export ReserveORDCMarket
export CapacityMarket
export RECMarket
+export InertiaMarket
export MarketCollection
export MarketClearingProblem
@@ -118,6 +123,9 @@ export update_installed_cap!
export update_PSY_timeseries!
export update_simulation_derating_data!
export write_data
+export parsebool
+export parseint
+export parsefloat
# Export Getter Functions
export get_accepted_perc
@@ -208,7 +216,7 @@ export get_queue
export get_queue_cost
export get_ramp_limits
export get_realized_profit
-export get_rec_certificates
+export get_expected_rec_certificates
export get_rec_price
export get_rec_bid
export get_rep_hour_weight
@@ -278,22 +286,26 @@ import LinearAlgebra
import PooledArrays
import PowerSystems
import PowerSimulations
-using PowerSimulationExtensions
+using EMISExtensions
+using PRAS
import InfrastructureSystems
+import ReliablePowerSimulations
const PSY = PowerSystems
const PSI = PowerSimulations
-const PSIE = PowerSimulationExtensions
+const EMISEx = EMISExtensions
const IS = InfrastructureSystems
+const RPSI = ReliablePowerSimulations
import Random
import UUIDs
import StatsBase
import Statistics
import TimeSeries
-
const TS = TimeSeries
+import TimeZones
+
import Base.convert
import JuMP.value
import MathOptInterface
@@ -302,6 +314,11 @@ import MathOptInterface: AbstractOptimizer
const MOI = MathOptInterface
import Revise
+
+import PowerSystems:
+ get_value,
+ set_value
+
################################################################################
# Includes
@@ -313,6 +330,8 @@ include("structs/products/OperatingReserve.jl")
include("structs/products/Capacity.jl")
include("structs/products/REC.jl")
include("structs/products/CarbonTax.jl")
+include("structs/products/Inertia.jl")
+include("structs/ResourceAdequacy.jl")
include("structs/Finance.jl")
@@ -322,6 +341,7 @@ include("structs/devices/BatteryEMIS.jl")
include("structs/devices/HydroGenEMIS.jl")
include("structs/devices/RenewableGenEMIS.jl")
include("structs/devices/ThermalGenEMIS.jl")
+include("structs/devices/ThermalFastStartSIIP.jl")
include("structs/CaseDefinition.jl")
include("structs/MarketPrices.jl")
@@ -344,6 +364,7 @@ include("structs/market_structs/ReserveDownMarket.jl")
include("structs/market_structs/ReserveORDCMarket.jl")
include("structs/market_structs/CapacityMarket.jl")
include("structs/market_structs/RECMarket.jl")
+include("structs/market_structs/InertiaMarket.jl")
include("structs/market_structs/MarketCollection.jl")
include("structs/market_structs/MarketClearingProblem.jl")
@@ -377,7 +398,11 @@ include("markets_simulation/ordc_construction/ordc_construction.jl")
include("markets_simulation/ordc_construction/ordc_market_creator.jl")
#Include PRAS Resource adequacy functions
-include("PRAS_functions/conv.jl")
+include("resource_adequacy/conv.jl")
+include("resource_adequacy/PSY2PRAS.jl")
+include("resource_adequacy/parsers/power_system_table_data.jl")
+include("resource_adequacy/ra_utils.jl")
+include("resource_adequacy/generator_unavailability.jl")
#Include Test System Parsers
include("test_system_parsers/test_system_reader.jl")
diff --git a/src/PRAS_functions/conv.jl b/src/PRAS_functions/conv.jl
index 47306cb..c0c66a9 100644
--- a/src/PRAS_functions/conv.jl
+++ b/src/PRAS_functions/conv.jl
@@ -1,121 +1,121 @@
-CapacityDistribution = Distributions.DiscreteNonParametric{Int,Float64,Vector{Int},Vector{Float64}}
-
-
-
-function assess(distr::CapacityDistribution)
-
- xs = support(distr)
- ps = probs(distr)
-
- i = 1
- lolp = 0.
- eul = 0.
-
- while i <= length(xs)
-
- xs[i] >= 0 && break
- lolp += ps[i]
- eul -= ps[i] * xs[i]
- i += 1
-
- end
-
- return lolp, eul
-
-end
-
-function spconv(hvsraw::AbstractVector{Int}, hpsraw::AbstractVector{Float64})
-
- zeroidxs = hvsraw .!= 0
- hvs = hvsraw[zeroidxs]
- hps = hpsraw[zeroidxs]
-
- length(hvs) == 0 &&
- return Distributions.DiscreteNonParametric([0], [1.], check_args=false)
-
- max_n = sum(hvs) + 1
- current_probs = Vector{Float64}(undef, max_n)
- prev_probs = Vector{Float64}(undef, max_n)
- current_values = Vector{Int}(undef, max_n)
- prev_values = Vector{Int}(undef, max_n)
-
- current_n = 2
- current_values[1:current_n] = [0, hvs[1]]
- current_probs[1:current_n] = [1 - hps[1], hps[1]]
-
- for (hv, hp) in zip(hvs[2:end], hps[2:end])
- current_values, current_probs, current_n, prev_values, prev_probs =
- spconv!(prev_values, prev_probs, hv, hp,
- current_values, current_probs, current_n)
- end
-
- resize!(current_values, current_n)
- resize!(current_probs, current_n)
- nonzeroprob_idxs = findall(x -> x>0, current_probs)
-
- return Distributions.DiscreteNonParametric(
- current_values[nonzeroprob_idxs],
- current_probs[nonzeroprob_idxs],
- check_args=false)
-
-end
-
-function spconv!(y_values::Vector{Int}, y_probs::Vector{Float64},
- h_value::Int, h_prob::Float64,
- x_values::Vector{Int}, x_probs::Vector{Float64}, nx::Int)
-
- h_q = 1 - h_prob
-
- ix = ixsh = 1
- iy = 0
- lastval = -1
-
- @inbounds while ix <= nx
-
- x = x_values[ix]
- xsh = x_values[ixsh] + h_value
-
- if lastval == x
- @fastmath y_probs[iy] += h_q * x_probs[ix]
- ix += 1
-
- elseif lastval == xsh
- @fastmath y_probs[iy] += h_prob * x_probs[ixsh]
- ixsh += 1
-
- elseif x == xsh
- iy += 1
- y_values[iy] = x
- @fastmath y_probs[iy] = h_q * x_probs[ix] + h_prob * x_probs[ixsh]
- lastval = x
- ix += 1
- ixsh += 1
-
- elseif x < xsh
- iy += 1
- y_values[iy] = x
- @fastmath y_probs[iy] = h_q * x_probs[ix]
- lastval = x
- ix += 1
-
- elseif xsh < x
- iy += 1
- y_values[iy] = xsh
- @fastmath y_probs[iy] = h_prob * x_probs[ixsh]
- lastval = xsh
- ixsh += 1
-
- end
-
- end
-
- @inbounds while ixsh <= nx
- iy += 1
- y_values[iy] = x_values[ixsh] + h_value
- @fastmath y_probs[iy] = h_prob * x_probs[ixsh]
- ixsh += 1
- end
-
- return y_values, y_probs, iy, x_values, x_probs
-
-end
+CapacityDistribution = Distributions.DiscreteNonParametric{Int,Float64,Vector{Int},Vector{Float64}}
+
+
+
+function assess(distr::CapacityDistribution)
+
+ xs = support(distr)
+ ps = probs(distr)
+
+ i = 1
+ lolp = 0.
+ eul = 0.
+
+ while i <= length(xs)
+
+ xs[i] >= 0 && break
+ lolp += ps[i]
+ eul -= ps[i] * xs[i]
+ i += 1
+
+ end
+
+ return lolp, eul
+
+end
+
+function spconv(hvsraw::AbstractVector{Int}, hpsraw::AbstractVector{Float64})
+
+ zeroidxs = hvsraw .!= 0
+ hvs = hvsraw[zeroidxs]
+ hps = hpsraw[zeroidxs]
+
+ length(hvs) == 0 &&
+ return Distributions.DiscreteNonParametric([0], [1.], check_args=false)
+
+ max_n = sum(hvs) + 1
+ current_probs = Vector{Float64}(undef, max_n)
+ prev_probs = Vector{Float64}(undef, max_n)
+ current_values = Vector{Int}(undef, max_n)
+ prev_values = Vector{Int}(undef, max_n)
+
+ current_n = 2
+ current_values[1:current_n] = [0, hvs[1]]
+ current_probs[1:current_n] = [1 - hps[1], hps[1]]
+
+ for (hv, hp) in zip(hvs[2:end], hps[2:end])
+ current_values, current_probs, current_n, prev_values, prev_probs =
+ spconv!(prev_values, prev_probs, hv, hp,
+ current_values, current_probs, current_n)
+ end
+
+ resize!(current_values, current_n)
+ resize!(current_probs, current_n)
+ nonzeroprob_idxs = findall(x -> x>0, current_probs)
+
+ return Distributions.DiscreteNonParametric(
+ current_values[nonzeroprob_idxs],
+ current_probs[nonzeroprob_idxs],
+ check_args=false)
+
+end
+
+function spconv!(y_values::Vector{Int}, y_probs::Vector{Float64},
+ h_value::Int, h_prob::Float64,
+ x_values::Vector{Int}, x_probs::Vector{Float64}, nx::Int)
+
+ h_q = 1 - h_prob
+
+ ix = ixsh = 1
+ iy = 0
+ lastval = -1
+
+ @inbounds while ix <= nx
+
+ x = x_values[ix]
+ xsh = x_values[ixsh] + h_value
+
+ if lastval == x
+ @fastmath y_probs[iy] += h_q * x_probs[ix]
+ ix += 1
+
+ elseif lastval == xsh
+ @fastmath y_probs[iy] += h_prob * x_probs[ixsh]
+ ixsh += 1
+
+ elseif x == xsh
+ iy += 1
+ y_values[iy] = x
+ @fastmath y_probs[iy] = h_q * x_probs[ix] + h_prob * x_probs[ixsh]
+ lastval = x
+ ix += 1
+ ixsh += 1
+
+ elseif x < xsh
+ iy += 1
+ y_values[iy] = x
+ @fastmath y_probs[iy] = h_q * x_probs[ix]
+ lastval = x
+ ix += 1
+
+ elseif xsh < x
+ iy += 1
+ y_values[iy] = xsh
+ @fastmath y_probs[iy] = h_prob * x_probs[ixsh]
+ lastval = xsh
+ ixsh += 1
+
+ end
+
+ end
+
+ @inbounds while ixsh <= nx
+ iy += 1
+ y_values[iy] = x_values[ixsh] + h_value
+ @fastmath y_probs[iy] = h_prob * x_probs[ixsh]
+ ixsh += 1
+ end
+
+ return y_values, y_probs, iy, x_values, x_probs
+
+end
diff --git a/src/derating_factor_updates/derating_factor_calculator.jl b/src/derating_factor_updates/derating_factor_calculator.jl
index 456bc2d..2cb19ad 100644
--- a/src/derating_factor_updates/derating_factor_calculator.jl
+++ b/src/derating_factor_updates/derating_factor_calculator.jl
@@ -1,185 +1,257 @@
-"""
-This function calculates the derating data for existing and new renewable generation
-based on top 100 net-load hour methodology.
-"""
-function calculate_derating_data(simulation_dir::String,
- active_projects::Vector{Project})
-
- cap_mkt_params = read_data(joinpath(simulation_dir, "markets_data", "Capacity.csv"))
-
- renewable_existing = filter(p -> typeof(p) == RenewableGenEMIS{Existing}, active_projects)
- renewable_options = filter(p -> typeof(p) == RenewableGenEMIS{Option}, active_projects)
-
- zones = unique(get_zone.(get_tech.(renewable_existing)))
- types = unique(get_type.(get_tech.(renewable_existing)))
-
- load_n_vg_data = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data.csv"))
- availability_data = read_data(joinpath(simulation_dir, "timeseries_data_files", "Availability", "DAY_AHEAD_availability.csv"))
-
- num_hours = DataFrames.nrow(load_n_vg_data)
- num_top_hours = cap_mkt_params.num_top_hours[1]
-
- existing_vg_power = zeros(num_hours)
-
- load = vec(sum(Matrix(load_n_vg_data[:, r"load"]), dims=2))
-
- for g in renewable_existing
- existing_vg_power += load_n_vg_data[!,get_name(g)]
- end
-
- net_load_df = load_n_vg_data[:, 1:4]
- net_load_df[:, "net_load"] = load - existing_vg_power
-
- net_load_sorted_df = deepcopy(DataFrames.sort(net_load_df, "net_load", rev = true))
-
- derating_factors = read_data(joinpath(simulation_dir, "markets_data", "derating_dict.csv"))
-
- type_zone_max_cap = Dict{String, Float64}()
- for zone in zones
- for type in types
- type_zone_id = "$(type)_$(zone)"
- type_zone_max_cap[type_zone_id] = 0.0
- net_load_df[:, "net_load_w/o_existing_$(type_zone_id)"] = deepcopy(net_load_df[:, "net_load"])
- for g in renewable_existing
- gen_name = get_name(g)
- tech = get_tech(g)
- if "$(get_type(tech))_$(get_zone(tech))" == type_zone_id
- net_load_df[:, "net_load_w/o_existing_$(type_zone_id)"] += load_n_vg_data[:, gen_name]
- type_zone_max_cap[type_zone_id] += get_maxcap(g)
- end
- end
- end
- end
-
- for zone in zones
- for type in types
- type_zone_id = "$(type)_$(zone)"
- gen_sorted_df = deepcopy(DataFrames.sort(net_load_df, "net_load_w/o_existing_$(type_zone_id)", rev = true))
-
- load_reduction = gen_sorted_df[1:num_top_hours, "net_load_w/o_existing_$(type_zone_id)"] - gen_sorted_df[1:num_top_hours, "net_load"]
- derating_factors[:, "existing_$(type_zone_id)"] .= sum(load_reduction) / type_zone_max_cap[type_zone_id] / num_top_hours
- end
- end
-
- for g in renewable_options
- gen_name = get_name(g)
- tech = get_tech(g)
- type_zone_id = "$(get_type(tech))_$(get_zone(tech))"
- gen_cap = get_maxcap(g)
-
- net_load_df[:, "net_load_with_$(gen_name)"] = deepcopy(net_load_df[:, "net_load"] - availability_data[:, "$(type_zone_id)"] * 100)
- gen_sorted_df = deepcopy(DataFrames.sort(net_load_df, "net_load_with_$(gen_name)", rev = true))
-
- load_reduction = net_load_sorted_df[1:num_top_hours, "net_load"] - gen_sorted_df[1:num_top_hours, "net_load_with_$(gen_name)"]
-
- derating_factors[:, "new_$(type_zone_id)"] .= sum(load_reduction) / 100 / num_top_hours
- end
- write_data(joinpath(simulation_dir, "markets_data"), "derating_dict.csv", derating_factors)
- return
-end
-
-"""
-This function does nothing is project is not of ThermalGenEMIS, HydroGenEMIS, RenewableGenEMIS or BatteryEMIS type.
-"""
-function update_derating_factor!(project::P,
- simulation_dir::String,
- ) where P <: Project{<:BuildPhase}
- return
-end
-
-"""
-This function updates the derating factors of ThermalGenEMIS and HydroGenEMIS projects.
-"""
-function update_derating_factor!(project::Union{ThermalGenEMIS{<:BuildPhase}, HydroGenEMIS{<:BuildPhase}},
- simulation_dir::String,
- )
-
- derating_data = read_data(joinpath(simulation_dir, "markets_data", "derating_dict.csv"))
- derating_factor = derating_data[1, get_type(get_tech(project))]
- for product in get_products(project)
- set_derating!(product, derating_factor)
- end
- return
-end
-
-"""
-This function updates the derating factors of existing RenewableGenEMIS projects.
-"""
-function update_derating_factor!(project::RenewableGenEMIS{Existing},
- simulation_dir::String,
- )
-
- derating_data = read_data(joinpath(simulation_dir, "markets_data", "derating_dict.csv"))
- name = get_name(project)
- tech = get_tech(project)
- type_zone_id = "$(get_type(tech))_$(get_zone(tech))"
-
- if in("existing_$(type_zone_id)", names(derating_data))
- derating_factor = derating_data[1, "existing_$(type_zone_id)"]
- else
- error("Derating data not found")
- end
-
- for product in get_products(project)
- set_derating!(product, derating_factor)
- end
-
- return
-end
-
-"""
-This function updates the derating factors of new RenewableGenEMIS projects.
-"""
-function update_derating_factor!(project::RenewableGenEMIS{<:BuildPhase},
- simulation_dir::String,
- )
-
- derating_data = read_data(joinpath(simulation_dir, "markets_data", "derating_dict.csv"))
- name = get_name(project)
- tech = get_tech(project)
- type_zone_id = "$(get_type(tech))_$(get_zone(tech))"
-
- if in("new_$(type_zone_id)", names(derating_data))
- derating_factor = derating_data[1, "new_$(type_zone_id)"]
- else
- error("Derating data not found")
- end
-
- for product in get_products(project)
- set_derating!(product, derating_factor)
- end
-
- return
-end
-
-"""
-This function updates the derating factors of BatteryEMIS projects.
-"""
-function update_derating_factor!(project::BatteryEMIS{<:BuildPhase},
- simulation_dir::String,
- )
- tech = get_tech(project)
- duration = Int(get_storage_capacity(tech)[:max] / get_maxcap(project))
- project_type = "$(get_type(tech))_$(duration)"
-
- derating_data = read_data(joinpath(simulation_dir, "markets_data", "derating_dict.csv"))
- derating_factor = derating_data[1, project_type]
- for product in get_products(project)
- set_derating!(product, derating_factor)
- end
- return
-end
-
-"""
-This function updates the derating factors of all active projects in the simulation.
-"""
-function update_simulation_derating_data!(simulation::Union{AgentSimulation, AgentSimulationData})
- data_dir = get_data_dir(get_case(simulation))
- active_projects = get_activeprojects(simulation)
-
- calculate_derating_data(data_dir, active_projects)
- for project in active_projects
- update_derating_factor!(project, data_dir)
- end
- return
-end
+"""
+function for ifelse elementwise treatment
+"""
+function elementwise_ifelse(x, y)
+ if x <= 0
+ z = x
+ else
+ z = y
+ end
+ return z
+end
+
+"""
+This function calculates the derating data for existing and new renewable generation
+based on top 100 net-load hour methodology.
+"""
+function calculate_derating_data(simulation_dir::String,
+ active_projects::Vector{Project},
+ derating_scale::Float64)
+
+ cap_mkt_params = read_data(joinpath(simulation_dir, "markets_data", "Capacity.csv"))
+
+ renewable_existing = filter(p -> typeof(p) == RenewableGenEMIS{Existing}, active_projects)
+ renewable_options = filter(p -> typeof(p) == RenewableGenEMIS{Option}, active_projects)
+
+ zones = unique(get_zone.(get_tech.(renewable_existing)))
+ types = unique(get_type.(get_tech.(renewable_existing)))
+
+ load_n_vg_data = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data.csv"))
+ availability_data = read_data(joinpath(simulation_dir, "timeseries_data_files", "Availability", "DAY_AHEAD_availability.csv"))
+
+ num_hours = DataFrames.nrow(load_n_vg_data)
+ num_top_hours = cap_mkt_params.num_top_hours[1]
+
+ existing_vg_power = zeros(num_hours)
+
+ load = vec(sum(Matrix(load_n_vg_data[:, r"load"]), dims=2))
+
+ for g in renewable_existing
+ existing_vg_power += load_n_vg_data[!,get_name(g)]
+ end
+
+ net_load_df = load_n_vg_data[:, 1:4]
+ net_load_df[:, "net_load"] = load - existing_vg_power
+
+ net_load_sorted_df = deepcopy(DataFrames.sort(net_load_df, "net_load", rev = true))
+
+ derating_factors = read_data(joinpath(simulation_dir, "markets_data", "derating_dict.csv"))
+
+ type_zone_max_cap = Dict{String, Float64}()
+ for zone in zones
+ for type in types
+ type_zone_id = "$(type)_$(zone)"
+ type_zone_max_cap[type_zone_id] = 0.0
+ net_load_df[:, "net_load_w/o_existing_$(type_zone_id)"] = deepcopy(net_load_df[:, "net_load"])
+ for g in renewable_existing
+ gen_name = get_name(g)
+ tech = get_tech(g)
+ if "$(get_type(tech))_$(get_zone(tech))" == type_zone_id
+ net_load_df[:, "net_load_w/o_existing_$(type_zone_id)"] += load_n_vg_data[:, gen_name]
+ type_zone_max_cap[type_zone_id] += get_maxcap(g)
+ end
+ end
+ end
+ end
+
+ for zone in zones
+ for type in types
+ type_zone_id = "$(type)_$(zone)"
+ gen_sorted_df = deepcopy(DataFrames.sort(net_load_df, "net_load_w/o_existing_$(type_zone_id)", rev = true))
+
+ load_reduction = gen_sorted_df[1:num_top_hours, "net_load_w/o_existing_$(type_zone_id)"] - gen_sorted_df[1:num_top_hours, "net_load"]
+ derating_factors[:, "existing_$(type_zone_id)"] .= min(sum(load_reduction) * derating_scale / type_zone_max_cap[type_zone_id] / num_top_hours, 1.0)
+ end
+ end
+
+ for g in renewable_options
+ gen_name = get_name(g)
+ tech = get_tech(g)
+ type_zone_id = "$(get_type(tech))_$(get_zone(tech))"
+ gen_cap = get_maxcap(g)
+
+ net_load_df[:, "net_load_with_$(gen_name)"] = deepcopy(net_load_df[:, "net_load"] - availability_data[:, "$(type_zone_id)"] * gen_cap)
+ gen_sorted_df = deepcopy(DataFrames.sort(net_load_df, "net_load_with_$(gen_name)", rev = true))
+
+ load_reduction = net_load_sorted_df[1:num_top_hours, "net_load"] - gen_sorted_df[1:num_top_hours, "net_load_with_$(gen_name)"]
+
+ derating_factors[:, "new_$(type_zone_id)"] .= min(sum(load_reduction) * derating_scale / gen_cap / num_top_hours, 1.0)
+ end
+
+ # Storage CC script
+ battery_existing = filter(p -> typeof(p) == BatteryEMIS{Existing}, active_projects)
+ peak_reductions = [sum(get_maxcap.(battery_existing))]
+ if length(battery_existing) > 0
+ eff_charge = Statistics.mean(get_efficiency(get_tech(battery_existing[1])))
+ stor_duration = Int(round(Statistics.mean(get_storage_capacity(get_tech(battery_existing[1]))[:max] ./ get_maxcap(battery_existing[1]))))
+ else
+ eff_charge = 0.85
+ stor_duration = 4
+ end
+
+ stor_buffer_minutes = cap_mkt_params.stor_buffer_minutes[1]
+
+ inc = 1 #set to 1 for system-wide, but will need to be replaced with zonal level array if/when convert to zonal
+
+ max_demands = repeat((maximum(net_load_df[:,"net_load"]) .- peak_reductions), num_hours)
+
+ batt_powers = repeat(peak_reductions, num_hours)
+
+ poss_charges = min(batt_powers .* eff_charge, (max_demands - net_load_df[:,"net_load"]) .* eff_charge)
+
+ necessary_discharges = (max_demands - net_load_df[:,"net_load"])
+
+ poss_batt_changes = zeros(size(necessary_discharges)[1])
+ for n in collect(1:1:size(necessary_discharges)[1])
+ poss_batt_changes[n] = elementwise_ifelse(necessary_discharges[n], poss_charges[n])
+ end
+
+ batt_e_level = zeros((inc, num_hours))
+ batt_e_level[1] = min(poss_batt_changes[1], 0)
+ for n in collect(2:1:num_hours)
+ batt_e_level[n] = batt_e_level[n - 1] + poss_batt_changes[n]
+ batt_e_level[n] = min(batt_e_level[n], 0.0)
+ end
+
+ required_MWhs = -minimum(batt_e_level)
+
+ # This line of code will implement a buffer on all storage duration
+ # requirements, i.e. if the stor_buffer_minutes is set to 60 minutes
+ # then a 2-hour peak would be served by a 3-hour device, a 3-hour peak
+ # by a 4-hour device, etc.
+ stor_buffer_hrs = stor_buffer_minutes / 60
+ required_MWhs = required_MWhs + (batt_powers[1] * stor_buffer_hrs)[1]
+
+ if length(battery_existing) > 0
+ stor_CC = peak_reductions[1] * stor_duration[1] / required_MWhs
+ stor_CC = min(stor_CC, 1.0)
+ else
+ stor_CC = 1.0
+ end
+
+ derating_factors[:, "BA_$(stor_duration)"] .= stor_CC
+ write_data(joinpath(simulation_dir, "markets_data"), "derating_dict.csv", derating_factors)
+ return
+end
+
+"""
+This function does nothing is project is not of ThermalGenEMIS, HydroGenEMIS, RenewableGenEMIS or BatteryEMIS type.
+"""
+function update_derating_factor!(project::P,
+ simulation_dir::String,
+ derating_scale::Float64
+ ) where P <: Project{<:BuildPhase}
+ return
+end
+
+"""
+This function updates the derating factors of ThermalGenEMIS and HydroGenEMIS projects.
+"""
+function update_derating_factor!(project::Union{ThermalGenEMIS{<:BuildPhase}, HydroGenEMIS{<:BuildPhase}},
+ simulation_dir::String,
+ derating_scale::Float64
+ )
+
+ derating_data = read_data(joinpath(simulation_dir, "markets_data", "derating_dict.csv"))
+ derating_factor = derating_data[1, get_type(get_tech(project))]
+ for product in get_products(project)
+ set_derating!(product, derating_factor)
+ end
+ return
+end
+
+"""
+This function updates the derating factors of existing RenewableGenEMIS projects.
+"""
+function update_derating_factor!(project::RenewableGenEMIS{Existing},
+ simulation_dir::String,
+ derating_scale::Float64
+ )
+
+ derating_data = read_data(joinpath(simulation_dir, "markets_data", "derating_dict.csv"))
+ name = get_name(project)
+ tech = get_tech(project)
+ type_zone_id = "$(get_type(tech))_$(get_zone(tech))"
+
+ if in("existing_$(type_zone_id)", names(derating_data))
+ derating_factor = derating_data[1, "existing_$(type_zone_id)"]
+ else
+ error("Derating data not found")
+ end
+
+ for product in get_products(project)
+ set_derating!(product, derating_factor)
+ end
+
+ return
+end
+
+"""
+This function updates the derating factors of new RenewableGenEMIS projects.
+"""
+function update_derating_factor!(project::RenewableGenEMIS{<:BuildPhase},
+ simulation_dir::String,
+ derating_scale::Float64
+ )
+
+ derating_data = read_data(joinpath(simulation_dir, "markets_data", "derating_dict.csv"))
+ name = get_name(project)
+ tech = get_tech(project)
+ type_zone_id = "$(get_type(tech))_$(get_zone(tech))"
+
+ if in("new_$(type_zone_id)", names(derating_data))
+ derating_factor = derating_data[1, "new_$(type_zone_id)"]
+ else
+ error("Derating data not found")
+ end
+
+ for product in get_products(project)
+ set_derating!(product, derating_factor)
+ end
+
+ return
+end
+
+"""
+This function updates the derating factors of BatteryEMIS projects.
+"""
+function update_derating_factor!(project::BatteryEMIS{<:BuildPhase},
+ simulation_dir::String,
+ derating_scale::Float64
+ )
+ tech = get_tech(project)
+ duration = Int(round(get_storage_capacity(tech)[:max] / get_maxcap(project)))
+ project_type = "$(get_type(tech))_$(duration)"
+
+ derating_data = read_data(joinpath(simulation_dir, "markets_data", "derating_dict.csv"))
+ derating_factor = derating_data[1, project_type]
+ derating_factor = min(derating_factor * derating_scale, 1.0)
+ for product in get_products(project)
+ set_derating!(product, derating_factor)
+ end
+ return
+end
+
+"""
+This function updates the derating factors of all active projects in the simulation.
+"""
+function update_simulation_derating_data!(simulation::Union{AgentSimulation, AgentSimulationData}, derating_scale::Float64)
+ data_dir = get_data_dir(get_case(simulation))
+ active_projects = get_activeprojects(simulation)
+
+ calculate_derating_data(data_dir, active_projects, derating_scale)
+ for project in active_projects
+ update_derating_factor!(project, data_dir, derating_scale)
+ end
+ return
+end
diff --git a/src/investment_simulation.jl b/src/investment_simulation.jl
index 603f570..9819a17 100644
--- a/src/investment_simulation.jl
+++ b/src/investment_simulation.jl
@@ -1,11 +1,9 @@
function run_agent_simulation(simulation::AgentSimulation, simulation_years::Int64)
-
total_horizon = get_total_horizon(get_case(simulation))
rolling_horizon = get_rolling_horizon(get_case(simulation))
installed_capacity = zeros(simulation_years)
-
capacity_forward_years = get_capacity_forward_years(simulation)
# Set initial capacity market profits considering forward capacity auctions
@@ -30,7 +28,7 @@ function run_agent_simulation(simulation::AgentSimulation, simulation_years::Int
average_capital_cost_multiplier = Statistics.mean(get_cap_cost_multiplier.(investors))
- services = get_system_services(sys_UC)
+ clean_energy_percentage_vector = zeros(simulation_years)
for iteration_year = 1:simulation_years
@@ -41,26 +39,47 @@ function run_agent_simulation(simulation::AgentSimulation, simulation_years::Int
active_projects = deepcopy(get_activeprojects(simulation))
- #=
installed_capacity = update_installed_cap!(installed_capacity,
active_projects,
iteration_year,
simulation_years)
+ derating_factors = read_data(joinpath(get_data_dir(get_case(simulation)), "markets_data", "derating_dict.csv"))
+
+ output_file = joinpath(get_results_dir(simulation), "derating_data_year_$(iteration_year).jld2")
+
+ FileIO.save(output_file, "derating_factors", derating_factors)
+
+ update_delta_irm!(sys_UC,
+ active_projects,
+ capacity_forward_years,
+ get_resource_adequacy(simulation),
+ get_peak_load(simulation),
+ get_static_capacity_market(get_case(simulation)),
+ iteration_year,
+ get_annual_growth(simulation)[:, iteration_year],
+ get_data_dir(get_case(simulation)),
+ get_da_resolution(get_case(simulation)))
+
+
create_investor_predictions(investors,
- active_projects,
- iteration_year,
- yearly_horizon,
- get_data_dir(get_case(simulation)),
- get_results_dir(simulation),
- average_capital_cost_multiplier,
- get_zones(simulation),
- get_lines(simulation),
- get_peak_load(simulation),
- get_solver(get_case(simulation)),
- get_parallel_investors(get_case(simulation)),
- get_parallel_scenarios(get_case(simulation))
- )
+ active_projects,
+ iteration_year,
+ yearly_horizon,
+ get_data_dir(get_case(simulation)),
+ get_results_dir(simulation),
+ average_capital_cost_multiplier,
+ get_zones(simulation),
+ get_lines(simulation),
+ get_peak_load(simulation),
+ get_rps_target(get_case(simulation)),
+ get_reserve_penalty(get_case(simulation)),
+ get_resource_adequacy(simulation),
+ get_irm_scalar(get_case(simulation)),
+ get_solver(get_case(simulation)),
+ get_parallel_investors(get_case(simulation)),
+ get_parallel_scenarios(get_case(simulation))
+ )
for investor in investors
run_investor_iteration(investor,
@@ -68,18 +87,15 @@ function run_agent_simulation(simulation::AgentSimulation, simulation_years::Int
iteration_year,
yearly_horizon,
simulation_years,
- get_start_year(get_case(simulation)),
capacity_forward_years,
- get_data_dir(get_case(simulation)),
sys_UC,
- services,
- get_solver(get_case(simulation))
+ sys_ED,
+ get_case(simulation)
)
- end
- update_simulation_derating_data!(simulation)
- =#
+ end
+ update_simulation_derating_data!(simulation, get_derating_scale(get_case(simulation)))
#Get all existing projects to calculate realized profits for energy and REC markets.
all_existing_projects = vcat(get_existing.(get_investors(simulation))...)
@@ -95,12 +111,10 @@ function run_agent_simulation(simulation::AgentSimulation, simulation_years::Int
end
# Update variable operation cost based on annual carbon tax for SIIP market clearing
- #update_operation_cost!(project, sys_UC, get_carbon_tax(simulation), iteration_year)
- #update_operation_cost!(project, sys_ED, get_carbon_tax(simulation), iteration_year)
-
+ update_operation_cost!(project, sys_UC, (get_carbon_tax(simulation)), iteration_year)
+ update_operation_cost!(project, sys_ED, (get_carbon_tax(simulation)), iteration_year)
end
-
installed_capacity = update_installed_cap!(installed_capacity,
all_existing_projects,
iteration_year,
@@ -111,26 +125,60 @@ function run_agent_simulation(simulation::AgentSimulation, simulation_years::Int
#Find which markets to simulate.
markets = union(hcat(get_markets.(get_investors(simulation))...))
+ # for d in PSY.get_components(PSYE.ThermalCleanEnergy,sys_UC)
+ # if "CleanEnergyConstraint" ∉ PSY.get_name.(d.services)
+ # println("$(PSY.get_name(d))")
+ # add_clean_energy_contribution!(sys_UC, d)
+ # end
+ # end
+
#Create realzed market prices for existing projects.
realized_market_prices,
realized_capacity_factors,
realized_reserve_perc,
+ realized_inertia_perc,
capacity_accepted_bids,
- rec_accepted_bids = create_realized_marketdata(simulation,
- sys_UC,
- sys_ED,
- markets,
- all_existing_projects,
- capacity_market_projects,
- capacity_forward_years,
- iteration_year,
- simulation_years,
- get_solver(get_case(simulation)),
- get_results_dir(simulation))
+ rec_accepted_bids,
+ clean_energy_percentage_vector[iteration_year],
+ cet_achieved_ratio = create_realized_marketdata(simulation,
+ sys_UC,
+ sys_ED,
+ markets,
+ get_rps_target(get_case(simulation)),
+ get_reserve_penalty(get_case(simulation)),
+ get_ordc_curved(get_case(simulation)),
+ all_existing_projects,
+ capacity_market_projects,
+ capacity_forward_years,
+ iteration_year,
+ simulation_years,
+ get_solver(get_case(simulation)),
+ get_results_dir(simulation))
+
existing_project_types = unique(get_type.(get_tech.(all_existing_projects)))
rt_products = String.(split(read_data(joinpath(get_data_dir(get_case(simulation)), "markets_data", "reserve_products.csv"))[1,"rt_products"], "; "))
+ if iteration_year < simulation_years
+
+ update_rec_correction_factors!(get_activeprojects(simulation),
+ realized_capacity_factors,
+ get_rt_resolution(get_case(simulation)),
+ iteration_year)
+
+ if get_markets(simulation)[:CarbonTax]
+ max_carbon_tax_increment = get_max_carbon_tax_increase(get_case(simulation))
+ if cet_achieved_ratio == 0.0
+ delta_carbon_tax = max_carbon_tax_increment
+ else
+ delta_carbon_tax = max_carbon_tax_increment * max(0.0, (1 - cet_achieved_ratio))
+ end
+ new_carbon_tax = max((get_carbon_tax(simulation)[iteration_year] + delta_carbon_tax), get_carbon_tax(simulation)[iteration_year + 1])
+ simulation.carbon_tax[iteration_year + 1] = new_carbon_tax
+ end
+ end
+
+
#Update forecasts and realized profits of all existing projects for each investor.
for investor in get_investors(simulation)
@@ -145,6 +193,7 @@ function run_agent_simulation(simulation::AgentSimulation, simulation_years::Int
realized_market_prices,
realized_capacity_factors,
realized_reserve_perc,
+ realized_inertia_perc,
capacity_accepted_bids,
rec_accepted_bids,
get_hour_weight(simulation),
@@ -157,7 +206,6 @@ function run_agent_simulation(simulation::AgentSimulation, simulation_years::Int
update_annual_cashflow!(project, iteration_year)
- #=
retire_old!(projects,
i,
project,
@@ -165,11 +213,17 @@ function run_agent_simulation(simulation::AgentSimulation, simulation_years::Int
sys_ED,
get_data_dir(get_case(simulation)),
iteration_year)
- =#
end
+
+ update_portfolio_preference_multipliers!(investor, iteration_year)
+
end
+ ra_metrics = calculate_RA_metrics(deepcopy(sys_ED))
+ println(ra_metrics)
+ set_metrics!(get_resource_adequacy(simulation), iteration_year, ra_metrics)
+
println("COMPLETED YEAR $(iteration_year)")
end
@@ -179,9 +233,8 @@ function run_agent_simulation(simulation::AgentSimulation, simulation_years::Int
extrapolate_profits!(project, simulation_years)
end
+ FileIO.save(joinpath(get_results_dir(simulation), "clean_energy_percentage.jld2"), "clean_energy_percentage", clean_energy_percentage_vector)
FileIO.save(joinpath(get_results_dir(simulation), "simulation_data.jld2"), "simulation_data", simulation)
return
end
-
-
diff --git a/src/investor_functions/annual_updates.jl b/src/investor_functions/annual_updates.jl
index 71e9bd3..9783f92 100644
--- a/src/investor_functions/annual_updates.jl
+++ b/src/investor_functions/annual_updates.jl
@@ -33,10 +33,12 @@ end
"""
This function does nothing if the project is retired.
"""
+#=
function update_realized_profits!(project::P,
market_prices::MarketPrices,
capacity_factors::Dict{String, Array{Float64, 2}},
reserve_perc::Dict{String, Dict{String, Array{Float64, 2}}},
+ inertia_perc::Dict{String, Array{Float64, 2}},
capacity_accepted_bids::Dict{String, Float64},
rec_accepted_bids::Dict{String, Float64},
rep_hour_weight::Vector{Float64},
@@ -46,9 +48,9 @@ function update_realized_profits!(project::P,
da_resolution::Int64,
rt_resolution::Int64,
rt_products::Vector{String}) where P <: Project{Retired}
-
+
end
-
+=#
"""
This function updates the annual realized profit for active projects.
Returns nothing.
@@ -57,6 +59,7 @@ function update_realized_profits!(project::P,
market_prices::MarketPrices,
capacity_factors::Dict{String, Array{Float64, 2}},
reserve_perc::Dict{String, Dict{String, Array{Float64, 2}}},
+ inertia_perc::Dict{String, Array{Float64, 2}},
capacity_accepted_bids::Dict{String, Float64},
rec_accepted_bids::Dict{String, Float64},
realized_hour_weight::Vector{Float64},
@@ -73,6 +76,7 @@ function update_realized_profits!(project::P,
market_prices,
capacity_factors,
reserve_perc,
+ inertia_perc,
capacity_accepted_bids,
rec_accepted_bids,
realized_hour_weight,
@@ -86,6 +90,7 @@ function update_realized_profits!(project::P,
finance_data = get_finance_data(project)
profit_array_length = size(get_realized_profit(finance_data), 2)
if !isnothing(profit) && update_year <= profit_array_length && update_year > 0
+
set_realized_profit!(finance_data,
get_name(product),
update_year,
@@ -104,10 +109,13 @@ end
"""
THis function updates the annual cash flow of Existing projects.
"""
-function update_annual_cashflow!(project::Project{Existing}, iteration_year::Int64)
+function update_annual_cashflow!(project::Union{Project{Retired}, Project{Existing}}, iteration_year::Int64)
finance_data = get_finance_data(project)
annual_revenue = sum(get_realized_profit(finance_data)[:, iteration_year])
+ if isnan(annual_revenue)
+ println(get_name(project))
+ end
annual_cashflow = annual_revenue - get_fixed_OM_cost(finance_data)
set_annual_cashflow!(finance_data, iteration_year, get_annual_cashflow(finance_data)[iteration_year] + annual_cashflow)
return
@@ -148,7 +156,7 @@ end
"""
This function does nothing if project is in Option or Retired phase.
"""
-function update_annual_cashflow!(project::Union{Project{Retired}, Project{Option}}, iteration_year::Int64)
+function update_annual_cashflow!(project:: Project{Option}, iteration_year::Int64)
return
end
diff --git a/src/investor_functions/decision_metrics/npv_functions.jl b/src/investor_functions/decision_metrics/npv_functions.jl
index 7af2b0d..384bab3 100644
--- a/src/investor_functions/decision_metrics/npv_functions.jl
+++ b/src/investor_functions/decision_metrics/npv_functions.jl
@@ -1,175 +1,175 @@
-"""
-This function calculates the annualized capital, queue and fixed O&M costs
-"""
-function calculate_annualized_costs(project::P,
- sz::Int64,
- queue_cost::Vector{Float64}) where P <: Project{<: BuildPhase}
-
- finance = get_finance_data(project)
- r = get_discount_rate(finance)
-
- adjusted_investment_cost = get_effective_investment_cost(finance) * (1 + r) ^ (get_lag_time(finance) - 1)
-
- adjusted_queue_cost = 0
-
- for q in 1:length(queue_cost)
- adjusted_queue_cost += queue_cost[q] * (1 + r) ^ (get_lag_time(finance) + (length(queue_cost) - q))
- end
-
- α = r / (1 - (1 + r) ^ (-get_capex_years(finance)))
-
- annualized_cap_cost = fill(α * (adjusted_investment_cost + adjusted_queue_cost), sz)
-
- annual_OM_cost = fill(get_fixed_OM_cost(finance), sz)
-
- total_annual_cost = annualized_cap_cost + annual_OM_cost
-
- return total_annual_cost
-
-end
-
-"""
-This function returns the net present value of the existing and planned projects:
-NPV for these buildphases doesn't include investment and queue costs.
-"""
-function calculate_npv(project::P,
- scenario_name::String,
- iteration_year::Int64,
- yearly_horizon::Int64,
- queue_time::Int64,
- queue_cost::Vector{Float64},
- update_start_year::Int64) where P <: Union{Project{Existing}, Project{Planned}}
-
- project_life_end = get_end_life_year(project)
- finance = get_finance_data(project)
- npv_years = project_life_end - iteration_year + 1
- discount_rate = get_discount_rate(finance)
-
- OM_cost_array = create_OMcost_array(finance, scenario_name, project_life_end, update_start_year, iteration_year)
-
- npv_cost_array = [OM_cost_array[y]
- for y in iteration_year:project_life_end]
-
- npv_profit_array = [sum(get_scenario_profit(finance)[scenario_name][iteration_year][:, y])
- for y in iteration_year:project_life_end]
-
-
-
- npv = sum(((1 / (1 + discount_rate)) ^ y) * (npv_profit_array[y] - npv_cost_array[y])
- for y in 1:npv_years)
-
- return npv
-end
-
-
-"""
-This function returns the net present value of the queue and option projects:
-NPV for these buildphases considers annualized capital and queue costs.
-"""
-function calculate_npv(project::P,
- scenario_name::String,
- iteration_year::Int64,
- yearly_horizon::Int64,
- queue_time::Int64,
- queue_cost::Vector{Float64},
- update_start_year::Int64) where P <: Union{Project{Option}, Project{Queue}}
-
- project_life_end = get_end_life_year(project)
- finance = get_finance_data(project)
- npv_years = min(project_life_end, yearly_horizon + iteration_year - 1)
- discount_rate = get_discount_rate(finance)
-
- queue_cost_array = create_queue_cost_array(size(get_scenario_profit(finance)[scenario_name][iteration_year], 2),
- get_decision_year(project),
- queue_time,
- queue_cost)
-
- npv_profit_array = [sum(get_scenario_profit(finance)[scenario_name][iteration_year][:, y])
- for y in get_construction_year(project):npv_years]
-
- npv_cost_array = calculate_annualized_costs(project, length(npv_profit_array), queue_cost)
-
- npv = sum(((1 / (1 + discount_rate)) ^ (y + get_construction_year(project) - iteration_year )) * (npv_profit_array[y] - npv_cost_array[y])
- for y in 1:length(npv_profit_array))
-
- return npv
-end
-
-"""
-This function updates the NPV of the project.
-"""
-function update_project_npv!(project::P,
- scenario_data::Vector{Scenario},
- market_prices::MarketPrices,
- carbon_tax_data::Vector{Float64},
- rep_hour_weight::Vector{Float64},
- iteration_year::Int64,
- yearly_horizon::Int64,
- queue_cost::Vector{Float64},
- capacity_forward_years::Int64,
- solver::JuMP.MOI.OptimizerWithAttributes) where P <: Project{<: BuildPhase}
-
- price_years = find_price_years(get_construction_year(project),
- get_end_life_year(project),
- iteration_year,
- yearly_horizon)
-
- update_years = find_update_years(get_construction_year(project),
- get_end_life_year(project),
- iteration_year,
- yearly_horizon)
-
- update_expected_profit!(project,
- scenario_data,
- market_prices,
- carbon_tax_data,
- price_years,
- update_years,
- rep_hour_weight,
- queue_cost,
- capacity_forward_years,
- iteration_year,
- solver)
-
- expected_npv = 0.0
-
- finance_data = get_finance_data(project)
-
- for scenario in scenario_data
- scenario_name = get_name(scenario)
- scenario_npv = calculate_npv(project,
- scenario_name,
- iteration_year,
- yearly_horizon,
- length(queue_cost),
- queue_cost,
- update_years[:start_year])
-
- if -1 <= scenario_npv / get_maxcap(project) < 0 # Converting values from very small negative values to 0 to avoid rounding off issues
- scenario_npv = 0.0
- end
- set_scenario_npv!(finance_data, scenario_name, iteration_year, scenario_npv)
- expected_npv += scenario_npv * get_probability(scenario)
- end
-
- set_expected_npv!(finance_data, iteration_year, expected_npv)
-
- return
-end
-
-"""
-This function does nothing if the project is retired.
-Returns nothing.
-"""
-function update_project_npv!(project::P,
- scenario_data::Vector{Scenario},
- market_prices::MarketPrices,
- carbon_tax_data::Vector{Float64},
- rep_hour_weight::Vector{Float64},
- iteration_year::Int64,
- yearly_horizon::Int64,
- queue_cost::Vector{Float64},
- capacity_forward_years::Int64,
- solver::JuMP.MOI.OptimizerWithAttributes ) where P <: Project{Retired}
- return
-end
+"""
+This function calculates the annualized capital, queue and fixed O&M costs
+"""
+function calculate_annualized_costs(project::P,
+ sz::Int64,
+ queue_cost::Vector{Float64}) where P <: Project{<: BuildPhase}
+
+ finance = get_finance_data(project)
+ r = get_discount_rate(finance)
+
+ adjusted_investment_cost = get_effective_investment_cost(finance) * (1 + r) ^ (get_lag_time(finance) - 1)
+
+ adjusted_queue_cost = 0
+
+ for q in 1:length(queue_cost)
+ adjusted_queue_cost += queue_cost[q] * (1 + r) ^ (get_lag_time(finance) + (length(queue_cost) - q))
+ end
+
+ α = r / (1 - (1 + r) ^ (-get_capex_years(finance)))
+
+ annualized_cap_cost = fill(α * (adjusted_investment_cost + adjusted_queue_cost), sz)
+
+ annual_OM_cost = fill(get_fixed_OM_cost(finance), sz)
+
+ total_annual_cost = annualized_cap_cost + annual_OM_cost
+
+ return total_annual_cost
+
+end
+
+"""
+This function returns the net present value of the existing and planned projects:
+NPV for these buildphases doesn't include investment and queue costs.
+"""
+function calculate_npv(project::P,
+ scenario_name::String,
+ iteration_year::Int64,
+ yearly_horizon::Int64,
+ queue_time::Int64,
+ queue_cost::Vector{Float64},
+ update_start_year::Int64) where P <: Union{Project{Existing}, Project{Planned}}
+
+ project_life_end = get_end_life_year(project)
+ finance = get_finance_data(project)
+ npv_years = project_life_end - iteration_year + 1
+ discount_rate = get_discount_rate(finance)
+
+ OM_cost_array = create_OMcost_array(finance, scenario_name, project_life_end, update_start_year, iteration_year)
+
+ npv_cost_array = [OM_cost_array[y]
+ for y in iteration_year:project_life_end]
+
+ npv_profit_array = [sum(get_scenario_profit(finance)[scenario_name][iteration_year][:, y])
+ for y in iteration_year:project_life_end]
+
+
+
+ npv = sum(((1 / (1 + discount_rate)) ^ y) * (npv_profit_array[y] - npv_cost_array[y])
+ for y in 1:npv_years)
+
+ return npv
+end
+
+
+"""
+This function returns the net present value of the queue and option projects:
+NPV for these buildphases considers annualized capital and queue costs.
+"""
+function calculate_npv(project::P,
+ scenario_name::String,
+ iteration_year::Int64,
+ yearly_horizon::Int64,
+ queue_time::Int64,
+ queue_cost::Vector{Float64},
+ update_start_year::Int64) where P <: Union{Project{Option}, Project{Queue}}
+
+ project_life_end = get_end_life_year(project)
+ finance = get_finance_data(project)
+ npv_years = min(project_life_end, yearly_horizon + iteration_year - 1)
+ discount_rate = get_discount_rate(finance)
+
+ queue_cost_array = create_queue_cost_array(size(get_scenario_profit(finance)[scenario_name][iteration_year], 2),
+ get_decision_year(project),
+ queue_time,
+ queue_cost)
+
+ npv_profit_array = [sum(get_scenario_profit(finance)[scenario_name][iteration_year][:, y])
+ for y in get_construction_year(project):npv_years]
+
+ npv_cost_array = calculate_annualized_costs(project, length(npv_profit_array), queue_cost)
+
+ npv = sum(((1 / (1 + discount_rate)) ^ (y + get_construction_year(project) - iteration_year )) * (npv_profit_array[y] - npv_cost_array[y])
+ for y in 1:length(npv_profit_array))
+
+ return npv
+end
+
+"""
+This function updates the NPV of the project.
+"""
+function update_project_npv!(project::P,
+ scenario_data::Vector{Scenario},
+ market_prices::MarketPrices,
+ carbon_tax_data::Vector{Float64},
+ rep_hour_weight::Vector{Float64},
+ iteration_year::Int64,
+ yearly_horizon::Int64,
+ queue_cost::Vector{Float64},
+ capacity_forward_years::Int64,
+ solver::JuMP.MOI.OptimizerWithAttributes) where P <: Project{<: BuildPhase}
+
+ price_years = find_price_years(get_construction_year(project),
+ get_end_life_year(project),
+ iteration_year,
+ yearly_horizon)
+
+ update_years = find_update_years(get_construction_year(project),
+ get_end_life_year(project),
+ iteration_year,
+ yearly_horizon)
+
+ update_expected_profit!(project,
+ scenario_data,
+ market_prices,
+ carbon_tax_data,
+ price_years,
+ update_years,
+ rep_hour_weight,
+ queue_cost,
+ capacity_forward_years,
+ iteration_year,
+ solver)
+
+ expected_npv = 0.0
+
+ finance_data = get_finance_data(project)
+
+ for scenario in scenario_data
+ scenario_name = get_name(scenario)
+ scenario_npv = calculate_npv(project,
+ scenario_name,
+ iteration_year,
+ yearly_horizon,
+ length(queue_cost),
+ queue_cost,
+ update_years[:start_year])
+
+ if -5 <= scenario_npv / get_maxcap(project) < 0 # Converting values from very small negative values to 0 to avoid rounding off issues
+ scenario_npv = 0.0
+ end
+ set_scenario_npv!(finance_data, scenario_name, iteration_year, scenario_npv)
+ expected_npv += scenario_npv * get_probability(scenario)
+ end
+
+ set_expected_npv!(finance_data, iteration_year, expected_npv)
+
+ return
+end
+
+"""
+This function does nothing if the project is retired.
+Returns nothing.
+"""
+function update_project_npv!(project::P,
+ scenario_data::Vector{Scenario},
+ market_prices::MarketPrices,
+ carbon_tax_data::Vector{Float64},
+ rep_hour_weight::Vector{Float64},
+ iteration_year::Int64,
+ yearly_horizon::Int64,
+ queue_cost::Vector{Float64},
+ capacity_forward_years::Int64,
+ solver::JuMP.MOI.OptimizerWithAttributes ) where P <: Project{Retired}
+ return
+end
diff --git a/src/investor_functions/decisions/buildphase_conversion.jl b/src/investor_functions/decisions/buildphase_conversion.jl
index c9cadf1..89d9286 100644
--- a/src/investor_functions/decisions/buildphase_conversion.jl
+++ b/src/investor_functions/decisions/buildphase_conversion.jl
@@ -1,171 +1,189 @@
-"""
-This function does nothing if project is not in queue.
-Returns nothing.
-"""
-function start_construction!(projects::Vector{<: Project{<: BuildPhase}},
- index::Int64,
- project::P,
- iteration_year::Int64) where P <: Project{<: BuildPhase}
- return
-end
-
-"""
-This function converts a project from queue to planned if
-queue time has been completed.
-Returns nothing.
-"""
-function start_construction!(projects::Vector{<: Project{<: BuildPhase}},
- index::Int64,
- project::P,
- iteration_year::Int64) where P <: Project{Queue}
-
- queue_time = length(get_queue_cost(get_finance_data(project)))
-
- if get_decision_year(project) + queue_time == iteration_year
- projects[index] = convert(Project{Planned}, project)
- end
-end
-
-"""
-This function does nothing if project is not a planned project.
-Returns nothing.
-"""
-function finish_construction!(projects::Vector{<: Project{<: BuildPhase}},
- index::Int64,
- project::P,
- sys_UC::Union{Nothing, PSY.System},
- services::Vector{PSY.Service},
- simulation_dir::String,
- iteration_year::Int64,
- start_year::Int64) where P <: Project{<: BuildPhase}
- return
-end
-
-"""
-This function converts a project from planned to existing if
-construction time has been completed.
-Returns nothing.
-"""
-function finish_construction!(projects::Vector{<: Project{<: BuildPhase}},
- index::Int64,
- project::P,
- sys_UC::Nothing,
- services::Vector{PSY.Service},
- simulation_dir::String,
- iteration_year::Int64,
- start_year::Int64) where P <: Project{Planned}
-
- if get_construction_year(project) == iteration_year
- projects[index] = convert(Project{Existing}, project)
-
- if typeof(project) == RenewableGenEMIS{Planned}
- type = get_type(get_tech(project))
- zone = get_zone(get_tech(project))
-
- availability_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Availability", "DAY_AHEAD_availability.csv"))
- availability_df_rt = read_data(joinpath(simulation_dir, "timeseries_data_files", "Availability", "REAL_TIME_availability.csv"))
-
- if in(get_name(project), names(availability_df))
- availability_raw = availability_df[:, Symbol(get_name(project))]
- availability_raw_rt = availability_df_rt[:, Symbol(get_name(project))]
- elseif in("$(type)_$(zone)", names(availability_df))
- availability_raw = availability_df[:, Symbol("$(type)_$(zone)")]
- availability_raw_rt = availability_df[:, Symbol("$(type)_$(zone)")]
- end
-
- load_n_vg_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data.csv"))
- load_n_vg_df[:, get_name(project)] = availability_raw * get_maxcap(project)
-
- load_n_vg_df_rt = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data_rt.csv"))
- load_n_vg_df_rt[:, get_name(project)] = availability_raw_rt * get_maxcap(project)
-
- write_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data"), "load_n_vg_data.csv", load_n_vg_df)
- write_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data"), "load_n_vg_data_rt.csv", load_n_vg_df_rt)
- end
- end
-
- return
-
-end
-
-"""
-This function converts a project from planned to existing if
-construction time has been completed.
-Returns nothing.
-"""
-function finish_construction!(projects::Vector{<: Project{<: BuildPhase}},
- index::Int64,
- project::P,
- sys_UC:: PSY.System,
- services::Vector{PSY.Service},
- simulation_dir::String,
- iteration_year::Int64,
- start_year::Int64) where P <: Project{Planned}
-
- if get_construction_year(project) == iteration_year
- projects[index] = convert(Project{Existing}, project)
- PSY_project = create_PSY_generator(project, sys_UC)
-
- PSY.add_component!(sys_UC, PSY_project)
-
- for product in get_products(project)
- add_device_services!(services, PSY_project, product)
- end
-
- type = get_type(get_tech(project))
- zone = get_zone(get_tech(project))
-
- availability_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Availability", "DAY_AHEAD_availability.csv"))
- availability_df_rt = read_data(joinpath(simulation_dir, "timeseries_data_files", "Availability", "REAL_TIME_availability.csv"))
-
- if in(get_name(project), names(availability_df))
- availability_raw = availability_df[:, Symbol(get_name(project))]
- availability_raw_rt = availability_df_rt[:, Symbol(get_name(project))]
- elseif in("$(type)_$(zone)", names(availability_df))
- availability_raw = availability_df[:, Symbol("$(type)_$(zone)")]
- availability_raw_rt = availability_df_rt[:, Symbol("$(type)_$(zone)")]
- end
-
- add_device_forecast!(simulation_dir, sys_UC, PSY_project, availability_raw, availability_raw_rt, start_year)
-
- end
-
- return
-end
-
-"""
-This function does nothing if project is not an existing project.
-Returns nothing.
-"""
-function retire_old!(projects::Vector{<: Project{<: BuildPhase}},
- index::Int64,
- project::P,
- sys_UC::Union{Nothing, PSY. System},
- sys_ED::Union{Nothing, PSY. System},
- simulation_dir::String,
- iteration_year::Int64) where P <: Project{<: BuildPhase}
- return false
-end
-
-"""
-This function converts a project from existing to retired if
-lifetime has ended.
-Returns nothing.
-"""
-function retire_old!(projects::Vector{<: Project{<: BuildPhase}},
- index::Int64,
- project::P,
- sys_UC::Union{Nothing, PSY. System},
- sys_ED::Union{Nothing, PSY. System},
- simulation_dir::String,
- iteration_year::Int64) where P <: Project{Existing}
-
- if get_end_life_year(project) == iteration_year
- set_retirement_year!(projects[index], iteration_year + 1)
- projects[index] = convert(Project{Retired}, project)
- remove_system_component!(sys_UC, project)
- remove_system_component!(sys_ED, project)
- remove_renewable_gen_data!(project, simulation_dir)
- end
-end
-
+"""
+This function does nothing if project is not in queue.
+Returns nothing.
+"""
+function start_construction!(projects::Vector{<: Project{<: BuildPhase}},
+ index::Int64,
+ project::P,
+ iteration_year::Int64) where P <: Project{<: BuildPhase}
+ return
+end
+
+"""
+This function converts a project from queue to planned if
+queue time has been completed.
+Returns nothing.
+"""
+function start_construction!(projects::Vector{<: Project{<: BuildPhase}},
+ index::Int64,
+ project::P,
+ iteration_year::Int64) where P <: Project{Queue}
+
+ queue_time = length(get_queue_cost(get_finance_data(project)))
+
+ if get_decision_year(project) + queue_time == iteration_year
+ println("CONSTRUCTING:")
+ println(get_name(project))
+ projects[index] = convert(Project{Planned}, project)
+ end
+end
+
+"""
+This function does nothing if project is not a planned project.
+Returns nothing.
+"""
+function finish_construction!(projects::Vector{<: Project{<: BuildPhase}},
+ index::Int64,
+ project::P,
+ sys_UC::Union{Nothing, PSY.System},
+ sys_ED::Union{Nothing, PSY.System},
+ simulation_dir::String,
+ iteration_year::Int64,
+ da_resolution::Int64,
+ rt_resolution::Int64) where P <: Project{<: BuildPhase}
+ return
+end
+
+"""
+This function converts a project from planned to existing if
+construction time has been completed.
+Returns nothing.
+"""
+function finish_construction!(projects::Vector{<: Project{<: BuildPhase}},
+ index::Int64,
+ project::P,
+ sys_UC::Nothing,
+ sys_ED::Nothing,
+ simulation_dir::String,
+ iteration_year::Int64,
+ da_resolution::Int64,
+ rt_resolution::Int64) where P <: Project{Planned}
+
+ if get_construction_year(project) == iteration_year
+ projects[index] = convert(Project{Existing}, project)
+
+ if typeof(project) == RenewableGenEMIS{Planned}
+ type = get_type(get_tech(project))
+ zone = get_zone(get_tech(project))
+
+ availability_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Availability", "DAY_AHEAD_availability.csv"))
+ availability_df_rt = read_data(joinpath(simulation_dir, "timeseries_data_files", "Availability", "REAL_TIME_availability.csv"))
+
+ if in(get_name(project), names(availability_df))
+ availability_raw = availability_df[:, Symbol(get_name(project))]
+ availability_raw_rt = availability_df_rt[:, Symbol(get_name(project))]
+ elseif in("$(type)_$(zone)", names(availability_df))
+ availability_raw = availability_df[:, Symbol("$(type)_$(zone)")]
+ availability_raw_rt = availability_df[:, Symbol("$(type)_$(zone)")]
+ end
+
+ load_n_vg_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data.csv"))
+ load_n_vg_df[:, get_name(project)] = availability_raw * get_maxcap(project)
+
+ load_n_vg_df_rt = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data_rt.csv"))
+ load_n_vg_df_rt[:, get_name(project)] = availability_raw_rt * get_maxcap(project)
+
+ write_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data"), "load_n_vg_data.csv", load_n_vg_df)
+ write_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data"), "load_n_vg_data_rt.csv", load_n_vg_df_rt)
+ end
+ end
+
+ return
+
+end
+
+"""
+This function converts a project from planned to existing if
+construction time has been completed.
+Returns nothing.
+"""
+function finish_construction!(projects::Vector{<: Project{<: BuildPhase}},
+ index::Int64,
+ project::P,
+ sys_UC:: PSY.System,
+ sys_ED:: PSY.System,
+ simulation_dir::String,
+ iteration_year::Int64,
+ da_resolution::Int64,
+ rt_resolution::Int64) where P <: Project{Planned}
+ if get_construction_year(project) == iteration_year
+ projects[index] = convert(Project{Existing}, project)
+ PSY_project_UC = create_PSY_generator(project, sys_UC)
+ PSY_project_ED = create_PSY_generator(project, sys_ED)
+
+ PSY.add_component!(sys_UC, PSY_project_UC)
+ PSY.add_component!(sys_ED, PSY_project_ED)
+
+ for product in get_products(project)
+ add_device_services!(sys_UC, PSY_project_UC, product)
+ add_device_services!(sys_ED, PSY_project_ED, product)
+ end
+
+ type = get_type(get_tech(project))
+ zone = get_zone(get_tech(project))
+
+ availability_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Availability", "DAY_AHEAD_availability.csv"))
+ availability_df_rt = read_data(joinpath(simulation_dir, "timeseries_data_files", "Availability", "REAL_TIME_availability.csv"))
+
+ if in(get_name(project), names(availability_df))
+ availability_raw = availability_df[:, Symbol(get_name(project))]
+ availability_raw_rt = availability_df_rt[:, Symbol(get_name(project))]
+ elseif in("$(type)_$(zone)", names(availability_df))
+ availability_raw = availability_df[:, Symbol("$(type)_$(zone)")]
+ availability_raw_rt = availability_df_rt[:, Symbol("$(type)_$(zone)")]
+ end
+
+ add_device_forecast!(simulation_dir, sys_UC, sys_ED, PSY_project_UC, PSY_project_ED, availability_raw, availability_raw_rt, da_resolution, rt_resolution)
+
+ if type == "NU_ST" || type == "RE_CT" || typeof(PSY_project_UC) == PSY.RenewableDispatch || typeof(PSY_project_UC) == PSY.HydroEnergyReservoir || typeof(PSY_project_UC) == PSY.HydroDispatch
+ # convert_to_thermal_clean_energy!(PSY_project_UC, sys_UC)
+ # convert_to_thermal_clean_energy!(PSY_project_ED, sys_ED)
+ add_clean_energy_contribution!(sys_UC, PSY_project_UC)
+ elseif type == "CT"
+ convert_to_thermal_fast_start!(PSY_project_UC, sys_UC)
+ convert_to_thermal_fast_start!(PSY_project_ED, sys_ED)
+ end
+
+
+ end
+ println("FINISHED CONSTRUCTING: $(get_name(project))")
+
+ return
+end
+
+"""
+This function does nothing if project is not an existing project.
+Returns nothing.
+"""
+function retire_old!(projects::Vector{<: Project{<: BuildPhase}},
+ index::Int64,
+ project::P,
+ sys_UC::Union{Nothing, PSY. System},
+ sys_ED::Union{Nothing, PSY. System},
+ simulation_dir::String,
+ iteration_year::Int64) where P <: Project{<: BuildPhase}
+ return false
+end
+
+"""
+This function converts a project from existing to retired if
+lifetime has ended.
+Returns nothing.
+"""
+function retire_old!(projects::Vector{<: Project{<: BuildPhase}},
+ index::Int64,
+ project::P,
+ sys_UC::Union{Nothing, PSY. System},
+ sys_ED::Union{Nothing, PSY. System},
+ simulation_dir::String,
+ iteration_year::Int64) where P <: Project{Existing}
+
+ if get_end_life_year(project) == iteration_year
+ set_retirement_year!(projects[index], iteration_year + 1)
+ projects[index] = convert(Project{Retired}, project)
+ remove_system_component!(sys_UC, project)
+ remove_system_component!(sys_ED, project)
+ remove_renewable_gen_data!(project, simulation_dir)
+ end
+end
+
diff --git a/src/investor_functions/decisions/investment_decision.jl b/src/investor_functions/decisions/investment_decision.jl
index bf96331..dba00be 100644
--- a/src/investor_functions/decisions/investment_decision.jl
+++ b/src/investor_functions/decisions/investment_decision.jl
@@ -1,193 +1,254 @@
-"""
-This function is a wrapper for running expected utility and first year profit calculations
-for making investment decisions.
-"""
-function calculate_expected_utility(project::P,
- scenario_data::Vector{Scenario},
- market_prices::MarketPrices,
- carbon_tax_data::Vector{Float64},
- risk_preference::R,
- rep_hour_weight::Vector{Float64},
- iteration_year::Int64,
- yearly_horizon::Int64,
- queue_cost::Vector{Float64},
- capacity_forward_years::Int64,
- solver::JuMP.MOI.OptimizerWithAttributes) where {P <: Project{<: BuildPhase}, R <: RiskPreference}
-
- finance_data = get_finance_data(project)
-
- update_project_utility!(project,
- scenario_data,
- market_prices,
- carbon_tax_data,
- risk_preference,
- rep_hour_weight,
- iteration_year,
- yearly_horizon,
- queue_cost,
- capacity_forward_years,
- solver)
-
- annual_revenue = 0.0
-
- for scenario in scenario_data
- annual_revenue += get_probability(scenario) *
- sum(get_scenario_profit(finance_data)[get_name(scenario)][iteration_year][:, get_construction_year(project)])
- end
-
- annual_profit = annual_revenue - get_fixed_OM_cost(finance_data)
- project_utility = get_expected_utility(finance_data)[iteration_year]
-
- return annual_profit, project_utility
-
-end
-
-"""
-This function adds options of each technology type (also incorporating capital cost multiplier)
-to the list of profitable options which can be sent to the queue.
-Returns the updated list of profitable options.
-"""
-function add_profitable_option(projects::Vector{Project},
- max_new_options::Dict{String, Int64},
- profitable_options::Vector{Project},
- scenario_data::Vector{Scenario},
- market_prices::MarketPrices,
- carbon_tax_data::Vector{Float64},
- risk_preference::R,
- capital_cost_multiplier::Float64,
- investor_name::String,
- iteration_year::Int64,
- yearly_horizon::Int64,
- simulation_years::Int64,
- rep_hour_weight::Vector{Float64},
- capacity_forward_years::Int64,
- solver::JuMP.MOI.OptimizerWithAttributes) where {R <: RiskPreference}
-
- if length(projects) >= 1
- if get_construction_year(projects[1]) <= simulation_years && get_construction_year(projects[1]) <= yearly_horizon
-
- total_counter = 1
- counter_by_zone = AxisArrays.AxisArray(ones(length(projects)), get_zone.(get_tech.(projects)))
- profitable_type_options = Project[]
- base_inv_cost = get_effective_investment_cost(get_finance_data(projects[1]))
- inv_cost_array_length = length(get_investment_cost(get_finance_data(projects[1])))
- queue_cost = get_queue_cost(get_finance_data(projects[1]))
-
- for project in projects
-
- annual_profit, project_utility = calculate_expected_utility(project,
- scenario_data,
- market_prices,
- carbon_tax_data,
- risk_preference,
- rep_hour_weight,
- iteration_year,
- yearly_horizon,
- queue_cost,
- capacity_forward_years,
- solver)
-
- if project_utility >= 0 && annual_profit >= -1 && counter_by_zone[get_zone(get_tech(project))] <= max_new_options[get_name(project)]
- new_option = deepcopy(project)
- push!(profitable_type_options, new_option)
- end
-
- end
-
- while length(profitable_type_options) > 0
- sort!(profitable_type_options, by = x -> -get_expected_utility(get_finance_data(x))[iteration_year])
- most_profitable_option = deepcopy(profitable_type_options[1])
- zone = get_zone(get_tech(most_profitable_option))
- set_name!(most_profitable_option, "$(investor_name)_$(get_type(get_tech(most_profitable_option)))_$(zone)_year_$(iteration_year)_$(Int(counter_by_zone[zone]))")
- set_investment_cost!(most_profitable_option, fill(get_effective_investment_cost(get_finance_data(most_profitable_option)), inv_cost_array_length))
- push!(profitable_options, most_profitable_option)
- counter_by_zone[zone] += 1
-
- for option in profitable_type_options
- set_effective_investment_cost!(option, base_inv_cost * (1 + capital_cost_multiplier * total_counter))
- end
-
- for (idx, option) in enumerate(profitable_type_options)
- annual_profit, project_utility = calculate_expected_utility(option,
- scenario_data,
- market_prices,
- carbon_tax_data,
- risk_preference,
- rep_hour_weight,
- iteration_year,
- yearly_horizon,
- queue_cost,
- capacity_forward_years,
- solver)
- if project_utility < 0 || annual_profit < -1 || counter_by_zone[get_zone(get_tech(option))] > max_new_options[get_name(option)]
- deleteat!(profitable_type_options, idx)
- end
- end
-
- total_counter += 1
-
- end
-
- end
- end
-
- return profitable_options
-end
-"""
-This function makes the investment decisions each year by sending a top N profitable options to the queue,
-where N is the maximum number of new projects an investor can invest in within a year.
-Returns nothing.
-"""
-function make_investments!(investor::Investor,
- max_new_options::Dict{String, Int64},
- iteration_year::Int64,
- yearly_horizon::Int64,
- simulation_years::Int64,
- capacity_forward_years::Int64,
- solver::JuMP.MOI.OptimizerWithAttributes)
-
- scenario_data = get_scenario_data(get_forecast(investor))
- market_prices = get_market_prices(investor)
- carbon_tax_data = get_carbon_tax(investor)
- risk_preference = get_risk_preference(investor)
- projects = get_projects(investor)
-
- option_projects = get_options(investor)
-
- types = unique(get_type.(get_tech.(option_projects)))
- projects_by_type = Dict(type => Project[] for type in types)
-
- for type in types
- for project in option_projects
- if get_type(get_tech(project)) == type
- push!(projects_by_type[type], project)
- end
- end
- end
-
- profitable_options = Project[]
- for type in types
- profitable_options = add_profitable_option(projects_by_type[type],
- max_new_options,
- profitable_options,
- scenario_data,
- market_prices,
- carbon_tax_data,
- risk_preference,
- get_cap_cost_multiplier(investor),
- get_name(investor),
- iteration_year,
- yearly_horizon,
- simulation_years,
- get_rep_hour_weight(investor),
- capacity_forward_years,
- solver)
- end
-
- sort!(profitable_options, by = x -> -get_expected_utility(get_finance_data(x))[iteration_year]) # sort in descending order of project utility
- for i in 1:min(length(profitable_options), get_max_annual_projects(investor))
- push!(projects, convert(Project{Queue}, profitable_options[i]))
- end
-
- return
-end
+"""
+This function is a wrapper for running expected utility and first year profit calculations
+for making investment decisions.
+"""
+function calculate_option_expected_utility(project::P,
+ scenario_data::Vector{Scenario},
+ market_prices::MarketPrices,
+ carbon_tax_data::Vector{Float64},
+ risk_preference::R,
+ rep_hour_weight::Vector{Float64},
+ iteration_year::Int64,
+ yearly_horizon::Int64,
+ queue_cost::Vector{Float64},
+ capacity_forward_years::Int64,
+ portfolio_preference_multipliers::Dict{Tuple{String, String}, Vector{Float64}},
+ solver::JuMP.MOI.OptimizerWithAttributes) where {P <: Project{Option}, R <: RiskPreference}
+
+ finance_data = get_finance_data(project)
+
+ update_project_utility!(project,
+ scenario_data,
+ market_prices,
+ carbon_tax_data,
+ risk_preference,
+ rep_hour_weight,
+ iteration_year,
+ yearly_horizon,
+ queue_cost,
+ capacity_forward_years,
+ solver)
+
+ annual_revenue = 0.0
+
+ for scenario in scenario_data
+ annual_revenue += get_probability(scenario) *
+ sum(get_scenario_profit(finance_data)[get_name(scenario)][iteration_year][:, get_construction_year(project)])
+ end
+
+ annual_profit = annual_revenue - get_fixed_OM_cost(finance_data)
+
+ tech_data = get_tech(project)
+ preference_multiplier = portfolio_preference_multipliers[(get_type(tech_data), get_zone(tech_data))][iteration_year]
+
+ project_utility = get_expected_utility(finance_data)[iteration_year] * preference_multiplier
+ set_preference_multiplier!(project, iteration_year, preference_multiplier)
+
+ return annual_profit, project_utility
+
+end
+
+"""
+This function adds options of each technology type (also incorporating capital cost multiplier)
+to the list of profitable options which can be sent to the queue.
+Returns the updated list of profitable options.
+"""
+function add_profitable_option(projects::Vector{Project},
+ max_new_options::Dict{String, Int64},
+ profitable_options::Vector{Project},
+ scenario_data::Vector{Scenario},
+ market_prices::MarketPrices,
+ carbon_tax_data::Vector{Float64},
+ risk_preference::R,
+ capital_cost_multiplier::Float64,
+ portfolio_preference_multipliers::Dict{Tuple{String, String}, Vector{Float64}},
+ investor_name::String,
+ iteration_year::Int64,
+ yearly_horizon::Int64,
+ simulation_years::Int64,
+ rep_hour_weight::Vector{Float64},
+ capacity_forward_years::Int64,
+ solver::JuMP.MOI.OptimizerWithAttributes) where {R <: RiskPreference}
+
+ if length(projects) >= 1
+ if get_construction_year(projects[1]) <= simulation_years && (get_construction_year(projects[1]) <= iteration_year + yearly_horizon)
+
+ total_counter = 1
+ counter_by_zone = AxisArrays.AxisArray(ones(length(projects)), get_zone.(get_tech.(projects)))
+ profitable_type_options = Project[]
+ base_inv_cost = get_effective_investment_cost(get_finance_data(projects[1]))
+ inv_cost_array_length = length(get_investment_cost(get_finance_data(projects[1])))
+ queue_cost = get_queue_cost(get_finance_data(projects[1]))
+
+ for project in projects
+ if counter_by_zone[get_zone(get_tech(project))] <= max_new_options[get_name(project)]
+ annual_profit, project_utility = calculate_option_expected_utility(project,
+ scenario_data,
+ market_prices,
+ carbon_tax_data,
+ risk_preference,
+ rep_hour_weight,
+ iteration_year,
+ yearly_horizon,
+ queue_cost,
+ capacity_forward_years,
+ portfolio_preference_multipliers,
+ solver)
+ if project_utility >= 0
+ new_option = deepcopy(project)
+ push!(profitable_type_options, new_option)
+ end
+ end
+ end
+
+ while length(profitable_type_options) > 0
+ sort!(profitable_type_options, by = x -> -get_expected_utility(get_finance_data(x))[iteration_year])
+ most_profitable_option = deepcopy(profitable_type_options[1])
+ zone = get_zone(get_tech(most_profitable_option))
+ set_name!(most_profitable_option, "$(investor_name)_$(get_type(get_tech(most_profitable_option)))_$(zone)_year_$(iteration_year)_$(Int(counter_by_zone[zone]))")
+ set_investment_cost!(most_profitable_option, fill(get_effective_investment_cost(get_finance_data(most_profitable_option)), inv_cost_array_length))
+ push!(profitable_options, most_profitable_option)
+ counter_by_zone[zone] += 1
+
+ for option in profitable_type_options
+ set_effective_investment_cost!(option, base_inv_cost * (1 + capital_cost_multiplier * total_counter))
+ end
+
+ for (idx, option) in enumerate(profitable_type_options)
+ annual_profit, project_utility = calculate_option_expected_utility(option,
+ scenario_data,
+ market_prices,
+ carbon_tax_data,
+ risk_preference,
+ rep_hour_weight,
+ iteration_year,
+ yearly_horizon,
+ queue_cost,
+ capacity_forward_years,
+ portfolio_preference_multipliers,
+ solver)
+ if project_utility < 0 || annual_profit < -1 || counter_by_zone[get_zone(get_tech(option))] > max_new_options[get_name(option)]
+ deleteat!(profitable_type_options, idx)
+ end
+ end
+
+ total_counter += 1
+
+ end
+
+ end
+ end
+
+ return profitable_options
+end
+"""
+This function makes the investment decisions each year by sending a top N profitable options to the queue,
+where N is the maximum number of new projects an investor can invest in within a year.
+Returns nothing.
+"""
+function make_investments!(investor::Investor,
+ max_new_options::Dict{String, Int64},
+ iteration_year::Int64,
+ yearly_horizon::Int64,
+ simulation_years::Int64,
+ capacity_forward_years::Int64,
+ solver::JuMP.MOI.OptimizerWithAttributes)
+
+ scenario_data = get_scenario_data(get_forecast(investor))
+ market_prices = get_market_prices(investor)
+ carbon_tax_data = get_carbon_tax(investor)
+ risk_preference = get_risk_preference(investor)
+ projects = get_projects(investor)
+
+ option_projects = get_options(investor)
+
+ types = unique(get_type.(get_tech.(option_projects)))
+ projects_by_type = Dict(type => Project[] for type in types)
+
+ for type in types
+ for project in option_projects
+ if get_type(get_tech(project)) == type
+ push!(projects_by_type[type], project)
+ end
+ end
+ end
+
+ profitable_options = Project[]
+ for type in types
+ profitable_options = add_profitable_option(projects_by_type[type],
+ max_new_options,
+ profitable_options,
+ scenario_data,
+ market_prices,
+ carbon_tax_data,
+ risk_preference,
+ get_cap_cost_multiplier(investor),
+ get_portfolio_preference_multipliers(investor),
+ get_name(investor),
+ iteration_year,
+ yearly_horizon,
+ simulation_years,
+ get_rep_hour_weight(investor),
+ capacity_forward_years,
+ solver)
+ end
+
+ sort!(profitable_options, by = x -> -get_expected_utility(get_finance_data(x))[iteration_year]) # sort in descending order of project utility
+ for i in 1:min(length(profitable_options), get_max_annual_projects(investor))
+ push!(projects, convert(Project{Queue}, profitable_options[i]))
+ end
+
+ return
+end
+
+function update_portfolio_preference_multipliers!(investor::Investor, iteration_year::Int64)
+ existing_projects = get_existing(investor)
+ preference_multipliers = get_portfolio_preference_multipliers(investor)
+ scenario_data = get_scenario_data(get_forecast(investor))
+ lookback = 3
+ range = get_preference_multiplier_range(investor)
+
+ for key in keys(preference_multipliers)
+
+ tech = key[1]
+ zone = key[2]
+
+ all_target_projects = filter(p -> ((get_type(get_tech(p)) == tech) && (get_zone(get_tech(p)) == zone)), existing_projects)
+
+ if !isempty(all_target_projects)
+ latest_year = minimum(p -> get_construction_year(p), all_target_projects)
+ latest_target_projects = filter!(p -> get_construction_year(p) == latest_year, all_target_projects)
+
+ #calculating realized to expected ratio. selecting minimum of available values.
+ preference_ratio = minimum([calculate_realized_to_expected_ratio(p, scenario_data, iteration_year) for p in latest_target_projects])
+
+ #scaling according to investor's multiplier range
+ scale = 1.0 - range[:min]
+ preference_multiplier = min(max(range[:min] + scale * preference_ratio, range[:min]), range[:max])
+ past_multipliers = preference_multipliers[key][max(1, (iteration_year - lookback)):max(1, (iteration_year - 1))]
+ #currently multiplying past multipliers with each other. could also consider average or other operations.
+ adjusted_multiplier = min(max(prod(past_multipliers) * preference_multiplier, range[:min]), range[:max])
+
+ set_preference_multiplier!(investor, tech, zone, iteration_year, adjusted_multiplier)
+ end
+ end
+end
+
+function calculate_realized_to_expected_ratio(project::Project, scenario_data::Vector{Scenario}, iteration_year::Int64)
+ finance_data = get_finance_data(project)
+ expected_revenue = 0.0
+ for scenario in scenario_data
+ expected_revenue += get_probability(scenario) *
+ sum(get_scenario_profit(finance_data)[get_name(scenario)][iteration_year][:, iteration_year])
+ end
+
+ realized_revenue = sum(get_realized_profit(finance_data)[:, iteration_year])
+
+ if iszero(expected_revenue)
+ ratio = 1.0
+ else
+ ratio = realized_revenue/expected_revenue
+ end
+
+ return ratio
+end
diff --git a/src/investor_functions/decisions/retirement_decision.jl b/src/investor_functions/decisions/retirement_decision.jl
index 10dc69f..12efd92 100644
--- a/src/investor_functions/decisions/retirement_decision.jl
+++ b/src/investor_functions/decisions/retirement_decision.jl
@@ -1,240 +1,299 @@
-"""
-This function does nothing if the method is not specified for the project's buildphase.
-"""
-function retire_project!(project::P) where P <: Project{<: BuildPhase}
- return project
-end
-
-"""
-This function converts Existing, Planned and Queued Projects to Retired buildphase.
-"""
-function retire_project!(project::P) where P <: Union{Project{Existing}, Project{Planned}, Project{Queue}}
- return convert(Project{Retired}, project)
-end
-
-"""
-This function does nothing if PSY System is not defined.
-"""
-function remove_system_component!(sys::Nothing,
- project::P,
- ) where P <: Project{<: BuildPhase}
- return
-end
-
-"""
-This function removes Existing projects from the list of Devices in PSY System.
-"""
-function remove_system_component!(sys::PSY.System,
- project::P,
- ) where P <: Project{Existing}
- component = PSY.get_components_by_name(PSY.Device, sys, get_name(project))[1]
- PSY.remove_component!(sys, component)
-end
-
-"""
-This function does nothing if the method is not specified for the project's type and buildphase.
-"""
-function remove_renewable_gen_data!(project::P,
- simulation_dir::String,
- ) where P <: Project{<:BuildPhase}
- return
- end
-
-"""
-This function removes the PSY System timeseries data for Existing RenewableGen projects.
-"""
- function remove_renewable_gen_data!(project::RenewableGenEMIS{Existing},
- simulation_dir::String,
- )
- load_n_vg_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data.csv"))
- DataFrames.select!(load_n_vg_df, DataFrames.Not(get_name(project)))
- write_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data"), "load_n_vg_data.csv", load_n_vg_df)
- return
- end
-
- """
-This function removes the future realized profits of projects which are being retired.
-"""
-function remove_future_profits!(project::Project, iteration_year::Int64)
- products = get_products(project)
- finance_data = get_finance_data(project)
- profit_array_length = size(get_realized_profit(finance_data), 2)
- for year in iteration_year:profit_array_length
- for product in get_name.(products)
- set_realized_profit!(finance_data, product, year, 0.0)
- end
- end
- return
-end
-
-
-"""
-This function does nothing if the method is not specified for the project's buildphase.
-"""
-function take_retirement_decision(project::P,
- index::Int64,
- projects::Vector{<: Project{<: BuildPhase}},
- scenario_data::Vector{Scenario},
- market_prices::MarketPrices,
- carbon_tax_data::Vector{Float64},
- risk_preference::R,
- sys::Union{Nothing, PSY.System},
- simulation_dir::String,
- iteration_year::Int64,
- yearly_horizon::Int64,
- simulation_years::Int64,
- rep_hour_weight::Vector{Float64},
- capacity_forward_years::Int64,
- solver::JuMP.MOI.OptimizerWithAttributes) where {P <: Project{<: BuildPhase}, R <: RiskPreference}
-
- return
-end
-
-
-"""
-This function decides whether or not to retire Existing and Planned projects.
-"""
-function take_retirement_decision(project::P,
- index::Int64,
- projects::Vector{<: Project{<: BuildPhase}},
- scenario_data::Vector{Scenario},
- market_prices::MarketPrices,
- carbon_tax_data::Vector{Float64},
- risk_preference::R,
- sys::Union{Nothing, PSY.System},
- simulation_dir::String,
- iteration_year::Int64,
- yearly_horizon::Int64,
- simulation_years::Int64,
- rep_hour_weight::Vector{Float64},
- capacity_forward_years::Int64,
- solver::JuMP.MOI.OptimizerWithAttributes) where {P <: Union{Project{Existing}, Project{Planned}}, R <: RiskPreference}
-
- finance_data = get_finance_data(project)
- queue_cost = get_queue_cost(finance_data)
-
- if get_construction_year(project) <= simulation_years
-
- update_project_utility!(project,
- scenario_data,
- market_prices,
- carbon_tax_data,
- risk_preference,
- rep_hour_weight,
- iteration_year,
- yearly_horizon,
- queue_cost,
- capacity_forward_years,
- solver)
-
- project_utility = get_expected_utility(finance_data)[iteration_year]
-
- annual_revenue = 0.0
- for scenario in scenario_data
- annual_revenue += get_probability(scenario) *
- sum(get_scenario_profit(finance_data)[get_name(scenario)][iteration_year][:, iteration_year])
- end
-
- annual_profit = annual_revenue - get_fixed_OM_cost(finance_data)
-
- if annual_profit < -1 && project_utility < 0
- set_retirement_year!(projects[index], iteration_year)
- projects[index] = retire_project!(project)
- remove_system_component!(sys, project)
- remove_renewable_gen_data!(project, simulation_dir)
- remove_future_profits!(project, iteration_year)
- end
- end
-
- return
-
-end
-
-"""
-This function decides whether or not to retire Queued projects.
-"""
-function take_retirement_decision(project::P,
- index::Int64,
- projects::Vector{<: Project{<: BuildPhase}},
- scenario_data::Vector{Scenario},
- market_prices::MarketPrices,
- carbon_tax_data::Vector{Float64},
- risk_preference::R,
- sys::Union{Nothing, PSY.System},
- simulation_dir::String,
- iteration_year::Int64,
- yearly_horizon::Int64,
- simulation_years::Int64,
- rep_hour_weight::Vector{Float64},
- capacity_forward_years::Int64,
- solver::JuMP.MOI.OptimizerWithAttributes) where {P <: Project{Queue}, R <: RiskPreference}
-
- finance_data = get_finance_data(project)
- queue_cost = get_queue_cost(finance_data)
-
- if get_construction_year(project) <= simulation_years
-
- update_project_utility!(project,
- scenario_data,
- market_prices,
- carbon_tax_data,
- risk_preference,
- rep_hour_weight,
- iteration_year,
- yearly_horizon,
- queue_cost,
- capacity_forward_years,
- solver)
-
-
- project_utility = get_expected_utility(finance_data)[iteration_year]
-
- if project_utility < 0
- set_retirement_year!(projects[index], iteration_year)
- projects[index] = retire_project!(project)
- remove_future_profits!(project, iteration_year)
- end
- end
-
- return
-
-end
-
-"""
-This function makes the retirement decisions each year.
-Returns nothing.
-"""
-function retire_unprofitable!(investor::Investor,
- sys::Union{Nothing, PSY.System},
- simulation_dir::String,
- iteration_year::Int64,
- yearly_horizon::Int64,
- simulation_years::Int64,
- capacity_forward_years::Int64,
- solver::JuMP.MOI.OptimizerWithAttributes)
-
- scenario_data = get_scenario_data(get_forecast(investor))
- market_prices = get_market_prices(investor)
- carbon_tax_data = get_carbon_tax(investor)
- risk_preference = get_risk_preference(investor)
- projects = get_projects(investor)
- for i in 1:length(projects)
- take_retirement_decision(projects[i],
- i,
- projects,
- scenario_data,
- market_prices,
- carbon_tax_data,
- risk_preference,
- sys,
- simulation_dir,
- iteration_year,
- yearly_horizon,
- simulation_years,
- get_rep_hour_weight(investor),
- capacity_forward_years,
- solver)
-
- end
- return
-end
+"""
+This function does nothing if the method is not specified for the project's buildphase.
+"""
+function retire_project!(project::P) where P <: Project{<: BuildPhase}
+ return project
+end
+
+"""
+This function converts Existing, Planned and Queued Projects to Retired buildphase.
+"""
+function retire_project!(project::P) where P <: Union{Project{Existing}, Project{Planned}, Project{Queue}}
+ return convert(Project{Retired}, project)
+end
+
+"""
+This function does nothing if PSY System is not defined.
+"""
+function remove_system_component!(sys::Nothing,
+ project::P,
+ ) where P <: Project{<: BuildPhase}
+ return
+end
+
+"""
+This function removes Existing projects from the list of Devices in PSY System.
+"""
+function remove_system_component!(sys::PSY.System,
+ project::P,
+ ) where P <: Project{Existing}
+ component = PSY.get_components_by_name(PSY.Device, sys, get_name(project))[1]
+ PSY.remove_component!(sys, component)
+end
+
+"""
+This function does nothing if the method is not specified for the project's type and buildphase.
+"""
+function remove_renewable_gen_data!(project::P,
+ simulation_dir::String,
+ ) where P <: Project{<:BuildPhase}
+ return
+ end
+
+"""
+This function removes the PSY System timeseries data for Existing RenewableGen projects.
+"""
+ function remove_renewable_gen_data!(project::RenewableGenEMIS{Existing},
+ simulation_dir::String,
+ )
+ load_n_vg_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data.csv"))
+ DataFrames.select!(load_n_vg_df, DataFrames.Not(get_name(project)))
+ write_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data"), "load_n_vg_data.csv", load_n_vg_df)
+ return
+ end
+
+ """
+This function removes the future realized profits of projects which are being retired.
+"""
+function remove_future_profits!(project::Project, iteration_year::Int64)
+ products = get_products(project)
+ finance_data = get_finance_data(project)
+ profit_array_length = size(get_realized_profit(finance_data), 2)
+ for year in iteration_year:profit_array_length
+ for product in get_name.(products)
+ set_realized_profit!(finance_data, product, year, 0.0)
+ end
+ end
+ return
+end
+
+
+"""
+This function does nothing if the method is not specified for the project's buildphase.
+"""
+function take_retirement_decision(project::P,
+ index::Int64,
+ projects::Vector{<: Project{<: BuildPhase}},
+ scenario_data::Vector{Scenario},
+ market_prices::MarketPrices,
+ carbon_tax_data::Vector{Float64},
+ risk_preference::R,
+ sys_UC::Union{Nothing, PSY.System},
+ sys_ED::Union{Nothing, PSY.System},
+ simulation_dir::String,
+ iteration_year::Int64,
+ yearly_horizon::Int64,
+ simulation_years::Int64,
+ rep_hour_weight::Vector{Float64},
+ capacity_forward_years::Int64,
+ retirement_lookback::Int64,
+ solver::JuMP.MOI.OptimizerWithAttributes) where {P <: Project{<: BuildPhase}, R <: RiskPreference}
+
+ return
+end
+
+"""
+This function decides whether or not to retire Existing projects.
+"""
+function take_retirement_decision(project::P,
+ index::Int64,
+ projects::Vector{<: Project{<: BuildPhase}},
+ scenario_data::Vector{Scenario},
+ market_prices::MarketPrices,
+ carbon_tax_data::Vector{Float64},
+ risk_preference::R,
+ sys_UC::Union{Nothing, PSY.System},
+ sys_ED::Union{Nothing, PSY.System},
+ simulation_dir::String,
+ iteration_year::Int64,
+ yearly_horizon::Int64,
+ simulation_years::Int64,
+ rep_hour_weight::Vector{Float64},
+ capacity_forward_years::Int64,
+ retirement_lookback::Int64,
+ solver::JuMP.MOI.OptimizerWithAttributes) where {P <: Project{Existing}, R <: RiskPreference}
+
+ finance_data = get_finance_data(project)
+ queue_cost = get_queue_cost(finance_data)
+
+ if (get_construction_year(project) <= simulation_years)
+
+ update_project_utility!(project,
+ scenario_data,
+ market_prices,
+ carbon_tax_data,
+ risk_preference,
+ rep_hour_weight,
+ iteration_year,
+ yearly_horizon,
+ queue_cost,
+ capacity_forward_years,
+ solver)
+
+ project_utility = get_expected_utility(finance_data)[iteration_year]
+
+ annual_revenue = 0.0
+ for scenario in scenario_data
+ annual_revenue += get_probability(scenario) *
+ sum(get_scenario_profit(finance_data)[get_name(scenario)][iteration_year][:, iteration_year])
+ end
+
+ annual_profit = annual_revenue - get_fixed_OM_cost(finance_data)
+
+ cash_flow = get_annual_cashflow(finance_data)
+ if (iteration_year > retirement_lookback)
+ previous_cashflow = cash_flow[(iteration_year - retirement_lookback):(iteration_year - 1)]
+ if (annual_profit < -1) && (project_utility < 0) && (all(previous_cashflow .< -1))
+ println("RETIRING:")
+ println(get_name(project))
+ set_retirement_year!(projects[index], iteration_year)
+ projects[index] = retire_project!(project)
+ remove_system_component!(sys_UC, project)
+ remove_system_component!(sys_ED, project)
+ remove_renewable_gen_data!(project, simulation_dir)
+ remove_future_profits!(project, iteration_year)
+ end
+ end
+
+ end
+
+ return
+
+end
+
+"""
+This function decides whether or not to retire Planned projects.
+"""
+function take_retirement_decision(project::P,
+ index::Int64,
+ projects::Vector{<: Project{<: BuildPhase}},
+ scenario_data::Vector{Scenario},
+ market_prices::MarketPrices,
+ carbon_tax_data::Vector{Float64},
+ risk_preference::R,
+ sys_UC::Union{Nothing, PSY.System},
+ sys_ED::Union{Nothing, PSY.System},
+ simulation_dir::String,
+ iteration_year::Int64,
+ yearly_horizon::Int64,
+ simulation_years::Int64,
+ rep_hour_weight::Vector{Float64},
+ capacity_forward_years::Int64,
+ retirement_lookback::Int64,
+ solver::JuMP.MOI.OptimizerWithAttributes) where {P <: Project{Planned}, R <: RiskPreference}
+
+ finance_data = get_finance_data(project)
+ queue_cost = get_queue_cost(finance_data)
+
+ if (get_construction_year(project) <= simulation_years)
+
+ update_project_utility!(project,
+ scenario_data,
+ market_prices,
+ carbon_tax_data,
+ risk_preference,
+ rep_hour_weight,
+ iteration_year,
+ yearly_horizon,
+ queue_cost,
+ capacity_forward_years,
+ solver)
+ end
+
+ return
+
+end
+
+"""
+This function decides whether or not to retire Queued projects.
+"""
+function take_retirement_decision(project::P,
+ index::Int64,
+ projects::Vector{<: Project{<: BuildPhase}},
+ scenario_data::Vector{Scenario},
+ market_prices::MarketPrices,
+ carbon_tax_data::Vector{Float64},
+ risk_preference::R,
+ sys_UC::Union{Nothing, PSY.System},
+ sys_ED::Union{Nothing, PSY.System},
+ simulation_dir::String,
+ iteration_year::Int64,
+ yearly_horizon::Int64,
+ simulation_years::Int64,
+ rep_hour_weight::Vector{Float64},
+ capacity_forward_years::Int64,
+ retirement_lookback::Int64,
+ solver::JuMP.MOI.OptimizerWithAttributes) where {P <: Project{Queue}, R <: RiskPreference}
+
+ finance_data = get_finance_data(project)
+ queue_cost = get_queue_cost(finance_data)
+
+ if get_construction_year(project) <= simulation_years
+
+ update_project_utility!(project,
+ scenario_data,
+ market_prices,
+ carbon_tax_data,
+ risk_preference,
+ rep_hour_weight,
+ iteration_year,
+ yearly_horizon,
+ queue_cost,
+ capacity_forward_years,
+ solver)
+
+
+ project_utility = get_expected_utility(finance_data)[iteration_year]
+
+ if project_utility < 0
+ set_retirement_year!(projects[index], iteration_year)
+ projects[index] = retire_project!(project)
+ remove_future_profits!(project, iteration_year)
+ end
+ end
+
+ return
+
+end
+
+"""
+This function makes the retirement decisions each year.
+Returns nothing.
+"""
+function retire_unprofitable!(investor::Investor,
+ sys_UC::Union{Nothing, PSY.System},
+ sys_ED::Union{Nothing, PSY.System},
+ simulation_dir::String,
+ iteration_year::Int64,
+ yearly_horizon::Int64,
+ simulation_years::Int64,
+ capacity_forward_years::Int64,
+ solver::JuMP.MOI.OptimizerWithAttributes)
+
+ scenario_data = get_scenario_data(get_forecast(investor))
+ market_prices = get_market_prices(investor)
+ carbon_tax_data = get_carbon_tax(investor)
+ risk_preference = get_risk_preference(investor)
+ projects = get_projects(investor)
+ for i in 1:length(projects)
+ take_retirement_decision(projects[i],
+ i,
+ projects,
+ scenario_data,
+ market_prices,
+ carbon_tax_data,
+ risk_preference,
+ sys_UC,
+ sys_ED,
+ simulation_dir,
+ iteration_year,
+ yearly_horizon,
+ simulation_years,
+ get_rep_hour_weight(investor),
+ capacity_forward_years,
+ get_retirement_lookback(investor),
+ solver)
+
+ end
+ return
+end
diff --git a/src/investor_functions/investor_iteration.jl b/src/investor_functions/investor_iteration.jl
index a4b0a5d..d21e5d6 100644
--- a/src/investor_functions/investor_iteration.jl
+++ b/src/investor_functions/investor_iteration.jl
@@ -3,14 +3,15 @@ function run_investor_iteration(investor::Investor,
iteration_year::Int64,
yearly_horizon::Int64,
simulation_years::Int64,
- start_year::Int64,
capacity_forward_years::Int64,
- sys_data_dir::String,
sys_UC::Union{Nothing, PSY.System},
- services::Vector{PSY.Service},
- solver::JuMP.MOI.OptimizerWithAttributes
+ sys_ED::Union{Nothing, PSY.System},
+ case::CaseDefinition
)
+ sys_data_dir = get_data_dir(case)
+ solver = get_solver(case)
+
investor_dir = get_data_dir(investor)
projects = get_projects(investor)
@@ -43,6 +44,10 @@ function run_investor_iteration(investor::Investor,
set_rec_price!(market_prices, scenario_name, expected_data["rec_price"])
end
+ if in(:Inertia, market_names)
+ set_inertia_price!(market_prices, scenario_name, expected_data["inertia_price"])
+ end
+
for project in projects
if in(get_name(project), get_name.(active_projects_copy))
update_capacity_factors!(project, scenario_name, expected_data["capacity_factors"])
@@ -61,6 +66,7 @@ function run_investor_iteration(investor::Investor,
retire_unprofitable!(investor,
sys_UC,
+ sys_ED,
sys_data_dir,
iteration_year,
yearly_horizon,
@@ -76,7 +82,10 @@ function run_investor_iteration(investor::Investor,
capacity_forward_years,
solver)
+ println(get_name.(get_queue(investor)))
+
for (i, project) in enumerate(projects)
+ # println("current investor is $(get_name(investor)), current project is $(get_name(project))")
start_construction!(projects,
i,
project,
@@ -86,10 +95,11 @@ function run_investor_iteration(investor::Investor,
i,
project,
sys_UC,
- services,
+ sys_ED,
sys_data_dir,
iteration_year,
- start_year)
+ get_da_resolution(case),
+ get_rt_resolution(case))
update_lifecycle!(project,
iteration_year,
@@ -98,5 +108,3 @@ function run_investor_iteration(investor::Investor,
return
end
-
-
diff --git a/src/investor_functions/prediction/REC_profit.jl b/src/investor_functions/prediction/REC_profit.jl
index df21930..026ed00 100644
--- a/src/investor_functions/prediction/REC_profit.jl
+++ b/src/investor_functions/prediction/REC_profit.jl
@@ -1,56 +1,56 @@
-"""
-This function does nothing if the product is not of Capacity type.
-"""
-function update_rec_revenues!(product::T,
- price_years::NamedTuple{(:start_year, :end_year),
- Tuple{Int64, Int64}},
- rec_revenues::Vector{Float64},
- rec_prices::AxisArrays.AxisArray{Float64, 1},
- energy_production::AxisArrays.AxisArray{Float64, 1}) where T <: Product
- return rec_revenues
-end
-
-"""
-This function calculates expected rec revenues for each project.
-Returns REC revenues
-"""
-function update_rec_revenues!(product::REC,
- price_years::NamedTuple{(:start_year, :end_year),
- Tuple{Int64, Int64}},
- rec_revenues::Vector{Float64},
- rec_prices::AxisArrays.AxisArray{Float64, 1},
- energy_production::AxisArrays.AxisArray{Float64, 1})
- rec_revenues += rec_prices[price_years[:start_year]:price_years[:end_year]] .*
- energy_production[1:end]
- return rec_revenues
-end
-
-"""
-This function updates the expected profit array of the project
-if the product is a REC product.
-Returns nothing.
-"""
-function update_profit!(project::P,
- scenario_name::String,
- product::REC,
- operating_profit::AxisArrays.AxisArray{Float64, 2},
- capacity_revenues::Vector{Float64},
- capacity_forward_years::Int64,
- rec_revenues::Vector{Float64},
- price_years::NamedTuple{(:start_year, :end_year),
- Tuple{Int64, Int64}},
- update_years::NamedTuple{(:start_year, :end_year),
- Tuple{Int64, Int64}},
- iteration_year::Int64) where P <: Project{<: BuildPhase}
-
- for i in 1:length(rec_revenues)
- set_scenario_profit!(get_finance_data(project),
- scenario_name,
- get_name(product),
- iteration_year,
- update_years[:start_year] + i - 1,
- rec_revenues[i])
- end
-
- return
-end
+"""
+This function does nothing if the product is not of Capacity type.
+"""
+function update_rec_revenues!(product::T,
+ price_years::NamedTuple{(:start_year, :end_year),
+ Tuple{Int64, Int64}},
+ rec_revenues::Vector{Float64},
+ rec_prices::AxisArrays.AxisArray{Float64, 1},
+ energy_production::AxisArrays.AxisArray{Float64, 1}) where T <: Product
+ return rec_revenues
+end
+
+"""
+This function calculates expected rec revenues for each project.
+Returns REC revenues
+"""
+function update_rec_revenues!(product::REC,
+ price_years::NamedTuple{(:start_year, :end_year),
+ Tuple{Int64, Int64}},
+ rec_revenues::Vector{Float64},
+ rec_prices::AxisArrays.AxisArray{Float64, 1},
+ energy_production::AxisArrays.AxisArray{Float64, 1})
+ rec_revenues += rec_prices[price_years[:start_year]:price_years[:end_year]] .*
+ energy_production[1:end]
+ return rec_revenues
+end
+
+"""
+This function updates the expected profit array of the project
+if the product is a REC product.
+Returns nothing.
+"""
+function update_profit!(project::P,
+ scenario_name::String,
+ product::REC,
+ operating_profit::AxisArrays.AxisArray{Float64, 2},
+ capacity_revenues::Vector{Float64},
+ capacity_forward_years::Int64,
+ rec_revenues::Vector{Float64},
+ price_years::NamedTuple{(:start_year, :end_year),
+ Tuple{Int64, Int64}},
+ update_years::NamedTuple{(:start_year, :end_year),
+ Tuple{Int64, Int64}},
+ iteration_year::Int64) where P <: Project{<: BuildPhase}
+
+ for i in 1:length(rec_revenues)
+ set_scenario_profit!(get_finance_data(project),
+ scenario_name,
+ get_name(product),
+ iteration_year,
+ update_years[:start_year] + i - 1,
+ rec_revenues[i])
+ end
+
+ return
+end
diff --git a/src/investor_functions/prediction/capacity_profit.jl b/src/investor_functions/prediction/capacity_profit.jl
index fcf1840..54f258c 100644
--- a/src/investor_functions/prediction/capacity_profit.jl
+++ b/src/investor_functions/prediction/capacity_profit.jl
@@ -1,62 +1,62 @@
-"""
-This function does nothing if the product is not of Capacity type.
-"""
-function update_capacity_revenues!(product::T,
- scenario_name::String,
- price_years::NamedTuple{(:start_year, :end_year),
- Tuple{Int64, Int64}},
- capacity_revenues::Vector{Float64},
- capacity_prices::Dict{String, AxisArrays.AxisArray{Float64, 1}},
- max_cap::Float64) where T <: Product
- return capacity_revenues
-end
-
-"""
-This function calculates expected capacity revenues for each project.
-Returns capacity revenues.
-"""
-function update_capacity_revenues!(product::Capacity,
- scenario_name::String,
- price_years::NamedTuple{(:start_year, :end_year),
- Tuple{Int64, Int64}},
- capacity_revenues::Vector{Float64},
- capacity_prices::Dict{String, AxisArrays.AxisArray{Float64, 1}},
- max_cap::Float64)
- capacity_revenues += capacity_prices[scenario_name][price_years[:start_year]:price_years[:end_year]] .*
- get_accepted_perc(product)[scenario_name][price_years[:start_year]:price_years[:end_year]] *
- max_cap *
- get_derating(product)
- return capacity_revenues
-end
-
-"""
-This function updates the expected profit array of the project
-if the product is a capacity product.
-Returns nothing.
-"""
-function update_profit!(project::P,
- scenario_name::String,
- product::Capacity,
- operating_profit::AxisArrays.AxisArray{Float64, 2},
- capacity_revenues::Vector{Float64},
- capacity_forward_years::Int64,
- rec_revenues::Vector{Float64},
- price_years::NamedTuple{(:start_year, :end_year),
- Tuple{Int64, Int64}},
- update_years::NamedTuple{(:start_year, :end_year),
- Tuple{Int64, Int64}},
- iteration_year::Int64) where P <: Project{<: BuildPhase}
-
- capacity_revenue_start_year = max(1, capacity_forward_years - price_years[:start_year] + 1)
-
- for i in capacity_revenue_start_year:length(capacity_revenues)
- set_scenario_profit!(get_finance_data(project),
- scenario_name,
- get_name(product),
- iteration_year,
- update_years[:start_year] + i - 1,
- capacity_revenues[i])
- end
-
- return
-end
+"""
+This function does nothing if the product is not of Capacity type.
+"""
+function update_capacity_revenues!(product::T,
+ scenario_name::String,
+ price_years::NamedTuple{(:start_year, :end_year),
+ Tuple{Int64, Int64}},
+ capacity_revenues::Vector{Float64},
+ capacity_prices::Dict{String, AxisArrays.AxisArray{Float64, 1}},
+ max_cap::Float64) where T <: Product
+ return capacity_revenues
+end
+
+"""
+This function calculates expected capacity revenues for each project.
+Returns capacity revenues.
+"""
+function update_capacity_revenues!(product::Capacity,
+ scenario_name::String,
+ price_years::NamedTuple{(:start_year, :end_year),
+ Tuple{Int64, Int64}},
+ capacity_revenues::Vector{Float64},
+ capacity_prices::Dict{String, AxisArrays.AxisArray{Float64, 1}},
+ max_cap::Float64)
+ capacity_revenues += capacity_prices[scenario_name][price_years[:start_year]:price_years[:end_year]] .*
+ get_accepted_perc(product)[scenario_name][price_years[:start_year]:price_years[:end_year]] *
+ max_cap *
+ get_derating(product)
+ return capacity_revenues
+end
+
+"""
+This function updates the expected profit array of the project
+if the product is a capacity product.
+Returns nothing.
+"""
+function update_profit!(project::P,
+ scenario_name::String,
+ product::Capacity,
+ operating_profit::AxisArrays.AxisArray{Float64, 2},
+ capacity_revenues::Vector{Float64},
+ capacity_forward_years::Int64,
+ rec_revenues::Vector{Float64},
+ price_years::NamedTuple{(:start_year, :end_year),
+ Tuple{Int64, Int64}},
+ update_years::NamedTuple{(:start_year, :end_year),
+ Tuple{Int64, Int64}},
+ iteration_year::Int64) where P <: Project{<: BuildPhase}
+
+ capacity_revenue_start_year = max(1, capacity_forward_years - price_years[:start_year] + 1)
+
+ for i in capacity_revenue_start_year:length(capacity_revenues)
+ set_scenario_profit!(get_finance_data(project),
+ scenario_name,
+ get_name(product),
+ iteration_year,
+ update_years[:start_year] + i - 1,
+ capacity_revenues[i])
+ end
+
+ return
+end
diff --git a/src/investor_functions/prediction/operating_profit.jl b/src/investor_functions/prediction/operating_profit.jl
index 5f87d0f..5109caf 100644
--- a/src/investor_functions/prediction/operating_profit.jl
+++ b/src/investor_functions/prediction/operating_profit.jl
@@ -1,435 +1,536 @@
-"""
-This function returns the total cost of generation.
-"""
-function calculate_cost(product::T,
- marginal_cost::Float64,
- carbon_tax::Float64,
- emission_intensity::Float64,
- Q::JuMP.VariableRef) where T <: OperatingProduct
- return marginal_cost * Q
-end
-
-"""
-This function returns the total cost of generation.
-"""
-function calculate_cost(product::Energy,
- marginal_cost::Float64,
- carbon_tax::Float64,
- emission_intensity::Float64,
- Q::JuMP.VariableRef)
- return (marginal_cost + (carbon_tax * emission_intensity)) * Q
-end
-
-"""
-This function returns the product of market price and quantity provided.
-"""
-function calculate_revenue(price::Float64, Q::JuMP.VariableRef)
- return price * Q
-end
-
-"""
-This function returns the product of marginal cost and quantity provided.
-"""
-function calculate_cost(marginal_cost::Float64, Q::JuMP.GenericAffExpr{Float64, JuMP.VariableRef})
- return marginal_cost * Q
-end
-
-"""
-This function returns the product of market price and quantity provided.
-"""
-function calculate_revenue(price::Float64, Q::JuMP.GenericAffExpr{Float64, JuMP.VariableRef})
- return price * Q
-end
-
-
-"""
-This function does nothing if the product is not either energy, reserve up or reserve down.
-"""
-function add_to_generationlimits!(min_gen::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
- max_gen::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
- Q::JuMP.VariableRef,
- product::T) where T <: Product
-return
-end
-
-"""
-This function adds to the maximum generation expression if the
-product is Reserve Up.
-Returns nothing.
-"""
-function add_to_generationlimits!(min_gen::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
- max_gen::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
- Q::JuMP.VariableRef,
- product::OperatingReserve{ReserveUpEMIS})
- JuMP.add_to_expression!(max_gen, Q)
- return
-end
-
-"""
-This function adds to the minimum generation expression if the
-product is Reserve Down.
-Returns nothing.
-"""
-function add_to_generationlimits!(min_gen::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
- max_gen::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
- Q::JuMP.VariableRef,
- product::OperatingReserve{ReserveDownEMIS})
- JuMP.add_to_expression!(min_gen, -Q)
- return
-end
-
-"""
-This function adds to both minimum and maximum generation expressions if the
-product is Energy.
-Returns nothing.
-"""
-function add_to_generationlimits!(min_gen::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
- max_gen::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
- Q::JuMP.VariableRef,
- product::Energy)
- JuMP.add_to_expression!(max_gen, Q)
- JuMP.add_to_expression!(min_gen, Q)
- return
-end
-
-"""
-This function calculates operating market profits for a generator
-based on expected market prices stored in the investor struct.
-Returns an axis array of profits for each operating market product.
-"""
-function calculate_operating_profit(gen::P,
- scenario_name::String,
- all_prices::MarketPrices,
- carbon_tax::Vector{Float64},
- start_year::Int64,
- end_year::Int64,
- rep_hour_weight::Vector{Float64},
- solver::JuMP.MOI.OptimizerWithAttributes) where P <: GeneratorEMIS{<: BuildPhase}
-
- num_hours = length(rep_hour_weight)
-
- all_products = get_products(gen)
-
- emission_intensity = 0.0
-
- for product in all_products
- emission_intensity = calculate_carbon_emissions(emission_intensity, product)
- end
-
- products = find_operating_products(all_products)
-
- market_prices = Dict{Symbol, Union{Array{Float64, 2}, AxisArrays.AxisArray{Float64, 2}}}()
-
- for prod in products
- if get_name(prod) == :Energy
- market_prices[get_name(prod)] = get_prices(all_prices, prod)[scenario_name][get_zone(get_tech(gen)), :, :]
- else
- market_prices[get_name(prod)] = get_prices(all_prices, prod)[scenario_name]
- end
- end
-
- max_perc = Dict(zip(get_name.(products), get_maxperc.(products, scenario_name, end_year, num_hours)))
-
- total_utilization = get_scenario_total_utilization(get_finance_data(gen))[scenario_name]
-
- m = JuMP.Model(solver)
-
- JuMP.@variable(m, Q[i in get_name.(products),
- y in start_year:end_year,
- h in 1:num_hours] >= 0)
-
- JuMP.@variable(m, temp == 0)
-
- JuMP.@expression(m, profit[i in 1:length(products), y in start_year:end_year], 0 + temp)
-
- JuMP.@expression(m, min_gen[y in start_year:end_year, h in 1:num_hours], 0 + temp)
-
- JuMP.@expression(m, max_gen[y in start_year:end_year, h in 1:num_hours], 0 + temp)
-
-
- for y in start_year:end_year
-
- for h in 1:num_hours
- for (i, product) in enumerate(products)
-
- hourly_profit = rep_hour_weight[h] *
- (calculate_revenue(market_prices[get_name(product)][y, h], Q[get_name(product), y, h]) -
- calculate_cost(product, get_marginal_cost(product), carbon_tax[y], emission_intensity, Q[get_name(product), y, h]))
-
- JuMP.add_to_expression!(profit[i, y], hourly_profit)
-
- add_to_generationlimits!(min_gen[y, h],
- max_gen[y, h],
- Q[get_name(product), y, h],
- product)
- JuMP.@constraint(m, Q[get_name(product), y, h] <=
- max_perc[get_name(product)][y, h] * get_maxcap(gen))
- end
- JuMP.@constraint(m, min_gen[y,h] >= get_mincap(gen) * 0)
- JuMP.@constraint(m, max_gen[y,h] <= get_maxcap(gen) * total_utilization[y, h])
- end
- end
-
- JuMP.@objective(m, Max, sum(profit))
-
- JuMP.optimize!(m)
-
- profit_array = AxisArrays.AxisArray(value.(profit),
- AxisArrays.Axis{:prod}(get_name.(products)),
- AxisArrays.Axis{:year}(start_year:end_year))
-
- energy_production = AxisArrays.AxisArray([sum(value(Q[:Energy, y, h]) * rep_hour_weight[h] for h in 1:num_hours) for y in start_year:end_year],
- AxisArrays.Axis{:year}(start_year:end_year))
-
-
- return profit_array, energy_production
-
-end
-
-"""
-This function does nothing if the product is not of Energy, ReserveUp or ReserveDown type
-"""
-function storage_product_constraints(product::T,
- Q::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
- p_out::JuMP.VariableRef,
- p_in::JuMP.VariableRef,
- p_ru::JuMP.VariableRef,
- p_rd::JuMP.VariableRef
- ) where T <: OperatingProduct
- return
-end
-
-"""
-This function calculates the net energy provided by the storage device.
-"""
-function storage_product_constraints(product::Energy,
- Q::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
- p_out::JuMP.VariableRef,
- p_in::JuMP.VariableRef,
- p_ru::JuMP.VariableRef,
- p_rd::JuMP.VariableRef
- )
- JuMP.add_to_expression!(Q, p_out - p_in)
- return
-end
-
-"""
-This function calculates the net reserve up provided by the storage device.
-"""
-function storage_product_constraints(product::ReserveUpEMIS,
- Q::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
- p_out::JuMP.VariableRef,
- p_in::JuMP.VariableRef,
- p_ru::JuMP.VariableRef,
- p_rd::JuMP.VariableRef
- )
- JuMP.add_to_expression!(Q, p_ru)
-
- return
-end
-
-"""
-This function calculates the net reserve down provided by the storage device.
-"""
-function storage_product_constraints(product::ReserveDownEMIS,
- Q::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
- p_out::JuMP.VariableRef,
- p_in::JuMP.VariableRef,
- p_ru::JuMP.VariableRef,
- p_rd::JuMP.VariableRef
- )
- JuMP.add_to_expression!(Q, p_rd)
- return
-end
-
-
-"""
-This function calculates operating market profits for a storage device
-based on expected market prices stored in the investor struct.
-Returns an axis array of profits for each operating market product.
-"""
-function calculate_operating_profit(storage::P,
- scenario_name::String,
- all_prices::MarketPrices,
- carbon_tax::Vector{Float64},
- start_year::Int64,
- end_year::Int64,
- rep_hour_weight::Vector{Float64},
- solver::JuMP.MOI.OptimizerWithAttributes ) where P <: StorageEMIS{<: BuildPhase}
-
- num_hours = length(rep_hour_weight)
- tech = get_tech(storage)
-
- products = find_operating_products(get_products(storage))
- energy_product = find_energy_product(products)
-
- capacity_factors = get_capacity_factors(energy_product[1])[scenario_name]
-
- market_prices = Dict{Symbol, Union{Array{Float64, 2}, AxisArrays.AxisArray{Float64, 2}}}()
-
- for prod in products
- if get_name(prod) == :Energy
- market_prices[get_name(prod)] = get_prices(all_prices, prod)[scenario_name][get_zone(get_tech(storage)), :, :]
- else
- market_prices[get_name(prod)] = get_prices(all_prices, prod)[scenario_name]
- end
- end
-
- max_gen = get_maxcap(storage) # maximum generation limit
- min_gen = get_mincap(storage) # minimum generation limit
-
- total_utilization = get_scenario_total_utilization(get_finance_data(storage))[scenario_name]
-
- input_power_limits = get_input_active_power_limits(tech)
- min_input = input_power_limits[:min] # minimum charging limit
- max_input = input_power_limits[:max] # maximum charging limit
-
- reserve_up_products = filter(p -> typeof(p) == OperatingReserve{ReserveUpEMIS}, products)
- reserve_down_products = filter(p -> typeof(p) == OperatingReserve{ReserveDownEMIS}, products)
-
- storage_capacity = get_storage_capacity(tech)
- min_storage = storage_capacity[:min] # minimum storage capacity
- max_storage = storage_capacity[:max] # maximum storage capacity
-
- efficiency = get_efficiency(tech)
- efficiency_in = efficiency[:in] # charging efficiency
- efficiency_out = efficiency[:out] # discharging efficiency
-
- initstorage = get_soc(tech)
- m = JuMP.Model(solver)
-
- JuMP.@variable(m, p_e[p in start_year:end_year, t in 1:num_hours] >= 0) # Unit energy production [MW]
- JuMP.@variable(m, p_in[p in start_year:end_year, t in 1:num_hours] >= 0) # Storage charging [MW]
-
- JuMP.@variable(m, p_ru[rp in get_name.(reserve_up_products), p in start_year:end_year, t in 1:num_hours] >= 0) # Unit energy production [MW]
- JuMP.@variable(m, p_rd[rp in get_name.(reserve_down_products), p in start_year:end_year, t in 1:num_hours] >= 0) # Storage charging [MW]
-
- JuMP.@variable(m, p_in_ru[rp in get_name.(reserve_up_products), p in start_year:end_year, t in 1:num_hours] >= 0) # reserve up provided by storage charging [MW]
- JuMP.@variable(m, p_in_rd[rp in get_name.(reserve_down_products), p in start_year:end_year, t in 1:num_hours] >= 0) # reserve down provided by storage charging [MW]
-
- JuMP.@variable(m, p_out_ru[rp in get_name.(reserve_up_products), p in start_year:end_year, t in 1:num_hours] >= 0) # reserve up provided by storage discharging [MW]
- JuMP.@variable(m, p_out_rd[rp in get_name.(reserve_down_products), p in start_year:end_year, t in 1:num_hours] >= 0) # reserve down provided by storage discharging [MW]
-
- JuMP.@variable(m, temp == 0)
-
- # Quantity of products provided
- JuMP.@expression(m, Q[i in get_name.(products), p in start_year:end_year, t in 1:num_hours], 0 + temp)
-
- # Profit for each product
- JuMP.@expression(m, profit[i in 1:length(products), p in start_year:end_year], 0 + temp)
-
- # Storage level evolution
- JuMP.@expression(m, storage_level[p in start_year:end_year, t in 1:num_hours],
- p_in[p, t] * efficiency_in - p_e[p, t] / efficiency_out)
-
- for p in start_year:end_year
- for t in 1:num_hours
-
- for (i, product) in enumerate(products)
-
- if product in reserve_up_products || product in reserve_down_products
-
- storage_product_constraints(product,
- Q[get_name(product), p, t],
- p_e[p, t],
- p_in[p, t],
- p_ru[get_name(product), p, t],
- p_rd[get_name(product), p, t])
-
- else
- storage_product_constraints(product,
- Q[get_name(product), p, t],
- p_e[p, t],
- p_in[p, t],
- p_e[p, t],
- p_e[p, t])
- end
-
- hourly_profit = rep_hour_weight[t] *
- (calculate_revenue(market_prices[get_name(product)][p, t],
- Q[get_name(product), p, t]) -
- calculate_cost(get_marginal_cost(product),
- Q[get_name(product), p, t]))
-
- JuMP.add_to_expression!(profit[i, p], hourly_profit)
- #=
- JuMP.@constraint(m, Q[get_name(product), y, h] <=
- max_perc[get_name(product)][y, h] * get_maxcap(gen))
- =#
- end
-
- if t == 1
- JuMP.add_to_expression!(storage_level[p, t], initstorage)
- else
- JuMP.add_to_expression!(storage_level[p, t], storage_level[p, t - 1])
- end
-
- # Storage technical constraints
- JuMP.@constraint(m, p_e[p, t] <= capacity_factors[p, t] * max_gen) # Capacity factor constraint
-
- JuMP.@constraint(m, p_e[p,t] - sum(p_out_rd[rp, p, t] for rp in get_name.(reserve_down_products)) >= min_gen * 0) # Minimum output dispatch
- JuMP.@constraint(m, p_e[p,t] + sum(p_out_ru[rp, p, t] for rp in get_name.(reserve_up_products)) <= max_gen * total_utilization[p, t]) # Maximum output dispatch
-
- JuMP.@constraint(m, p_in[p,t] - sum(p_in_ru[rp, p, t] for rp in get_name.(reserve_up_products)) >= min_input * 0) # Minimum input dispatch
- JuMP.@constraint(m, p_in[p,t] + sum(p_in_rd[rp, p, t] for rp in get_name.(reserve_down_products)) <= max_input) # Maximum input dispatch
-
- for rp in get_name.(reserve_up_products)
- JuMP.@constraint(m, p_ru[rp, p, t] <= p_out_ru[rp, p, t] + p_in_ru[rp, p, t]) # Total storage reserve up
- end
-
- for rp in get_name.(reserve_down_products)
- JuMP.@constraint(m, p_rd[rp, p, t] <= p_out_rd[rp, p, t] + p_in_rd[rp, p, t]) # Total storage reserve down
- end
-
- JuMP.@constraint(m, storage_level[p, t] >= min_storage) # Minimum storage level
- JuMP.@constraint(m, storage_level[p, t] <= max_storage) # Maximum storage level
-
- end
- end
-
- JuMP.@objective(m, Max, sum(profit))
-
- JuMP.optimize!(m)
-
- profit_array = AxisArrays.AxisArray(value.(profit),
- AxisArrays.Axis{:prod}(get_name.(products)),
- AxisArrays.Axis{:year}(start_year:end_year))
-
- energy_production = AxisArrays.AxisArray([sum(value(Q[:Energy, p, t]) * rep_hour_weight[t] for t in 1:num_hours) for p in start_year:end_year],
- AxisArrays.Axis{:year}(start_year:end_year))
-
- return profit_array, energy_production
-
-end
-
-"""
-This function updates the expected profit array of the project
-if the product is an operating market product.
-Returns nothing.
-"""
-function update_profit!(project::P,
- scenario_name::String,
- product::T,
- operating_profit::AxisArrays.AxisArray{Float64, 2},
- capacity_revenues::Vector{Float64},
- capacity_forward_years::Int64,
- rec_revenues::Vector{Float64},
- price_years::NamedTuple{(:start_year, :end_year),
- Tuple{Int64, Int64}},
- update_years::NamedTuple{(:start_year, :end_year),
- Tuple{Int64, Int64}},
- iteration_year::Int64) where {P <: Project{<: BuildPhase}, T <: OperatingProduct}
-
- profit = [operating_profit[get_name(product), y]
- for y in price_years[:start_year]:price_years[:end_year]]
-
- set_scenario_profit!(get_finance_data(project),
- scenario_name,
- get_name(product),
- iteration_year,
- update_years[:start_year],
- update_years[:end_year],
- profit)
-
- return
-end
+"""
+This function returns the total cost of generation.
+"""
+function calculate_cost(product::T,
+ marginal_cost::Float64,
+ carbon_tax::Float64,
+ emission_intensity::Float64,
+ inertia_constant::Float64,
+ Q::JuMP.VariableRef) where T <: OperatingProduct
+ return marginal_cost * Q
+end
+
+"""
+This function returns the total cost of generation.
+"""
+function calculate_cost(product::Energy,
+ marginal_cost::Float64,
+ carbon_tax::Float64,
+ emission_intensity::Float64,
+ inertia_constant::Float64,
+ Q::JuMP.VariableRef)
+ return (marginal_cost + (carbon_tax * emission_intensity)) * Q
+end
+
+function calculate_cost(product::Inertia,
+ marginal_cost::Float64,
+ carbon_tax::Float64,
+ emission_intensity::Float64,
+ inertia_constant::Float64,
+ Q::JuMP.VariableRef)
+ return marginal_cost * Q * inertia_constant
+end
+
+"""
+This function returns the product of market price and quantity provided.
+"""
+function calculate_revenue(product::T, price::Float64, inertia_constant::Float64, Q::JuMP.VariableRef) where T <: OperatingProduct
+ return price * Q
+end
+
+"""
+This function returns the product of market price, quantity and inertia constant for Inertia constant.
+"""
+function calculate_revenue(product::Inertia, price::Float64, inertia_constant::Float64, Q::JuMP.VariableRef)
+ return price * Q * inertia_constant
+end
+
+"""
+This function returns the product of marginal cost and quantity provided.
+"""
+function calculate_cost(product::T, marginal_cost::Float64, inertia_constant::Float64, Q::JuMP.GenericAffExpr{Float64, JuMP.VariableRef}) where T <: OperatingProduct
+ return marginal_cost * Q
+end
+
+"""
+This function returns the product of marginal cost, quantity and inertia constant.
+"""
+function calculate_cost(product::Inertia, marginal_cost::Float64, inertia_constant::Float64, Q::JuMP.GenericAffExpr{Float64, JuMP.VariableRef})
+ return marginal_cost * Q * inertia_constant
+end
+
+"""
+This function returns the product of market price and quantity provided.
+"""
+function calculate_revenue(product::T,
+ price::Float64,
+ inertia_constant::Float64,
+ Q::JuMP.GenericAffExpr{Float64, JuMP.VariableRef}) where T <: OperatingProduct
+ return price * Q
+end
+
+function calculate_revenue(product::Inertia,
+ price::Float64,
+ inertia_constant::Float64,
+ Q::JuMP.GenericAffExpr{Float64, JuMP.VariableRef})
+ return price * Q * inertia_constant
+end
+
+"""
+This function does nothing if the product is not either energy, reserve up or reserve down.
+"""
+function add_to_generationlimits!(min_gen::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
+ max_gen::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
+ Q::JuMP.VariableRef,
+ product::T,
+ synchronous_inertia::Bool) where T <: Product
+return
+end
+
+"""
+This function adds to the maximum generation expression if the
+product is Reserve Up.
+Returns nothing.
+"""
+function add_to_generationlimits!(min_gen::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
+ max_gen::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
+ Q::JuMP.VariableRef,
+ product::OperatingReserve{ReserveUpEMIS},
+ synchronous_inertia::Bool)
+ JuMP.add_to_expression!(max_gen, Q)
+ return
+end
+
+"""
+This function adds to the minimum generation expression if the
+product is Reserve Down.
+Returns nothing.
+"""
+function add_to_generationlimits!(min_gen::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
+ max_gen::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
+ Q::JuMP.VariableRef,
+ product::OperatingReserve{ReserveDownEMIS},
+ synchronous_inertia::Bool)
+ JuMP.add_to_expression!(min_gen, -Q)
+ return
+end
+
+"""
+This function adds to both minimum and maximum generation expressions if the
+product is Energy.
+Returns nothing.
+"""
+function add_to_generationlimits!(min_gen::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
+ max_gen::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
+ Q::JuMP.VariableRef,
+ product::Energy,
+ synchronous_inertia::Bool)
+ JuMP.add_to_expression!(max_gen, Q)
+ JuMP.add_to_expression!(min_gen, Q)
+ return
+end
+
+"""
+This function adds to the maximum generation expressions if the
+product is Inertia and unit doesn't provide synchronous inertia.
+Returns nothing.
+"""
+function add_to_generationlimits!(min_gen::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
+ max_gen::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
+ Q::JuMP.VariableRef,
+ product::Inertia,
+ synchronous_inertia::Bool)
+ if !synchronous_inertia
+ JuMP.add_to_expression!(max_gen, Q)
+ end
+ return
+end
+"""
+This function calculates operating market profits for a generator
+based on expected market prices stored in the investor struct.
+Returns an axis array of profits for each operating market product.
+"""
+function calculate_operating_profit(gen::P,
+ scenario_name::String,
+ all_prices::MarketPrices,
+ carbon_tax::Vector{Float64},
+ start_year::Int64,
+ end_year::Int64,
+ rep_hour_weight::Vector{Float64},
+ solver::JuMP.MOI.OptimizerWithAttributes) where P <: GeneratorEMIS{<: BuildPhase}
+
+ num_hours = length(rep_hour_weight)
+
+ all_products = get_products(gen)
+
+ emission_intensity = 0.0
+
+ for product in all_products
+ emission_intensity = calculate_carbon_emissions(emission_intensity, product)
+ end
+
+ products = find_operating_products(all_products)
+
+ market_prices = Dict{Symbol, Union{Array{Float64, 2}, AxisArrays.AxisArray{Float64, 2}}}()
+
+ for prod in products
+ if get_name(prod) == :Energy
+ market_prices[get_name(prod)] = get_prices(all_prices, prod)[scenario_name][get_zone(get_tech(gen)), :, :]
+ else
+ market_prices[get_name(prod)] = get_prices(all_prices, prod)[scenario_name]
+ end
+ end
+
+ max_perc = Dict(zip(get_name.(products), get_maxperc.(products, scenario_name, end_year, num_hours)))
+
+ inertia_constant = get_inertia_constant(gen)
+ synchronous_inertia = get_synchronous_inertia(gen)
+
+ total_utilization = get_scenario_total_utilization(get_finance_data(gen))[scenario_name]
+
+ m = JuMP.Model(solver)
+
+ JuMP.@variable(m, Q[i in get_name.(products),
+ y in start_year:end_year,
+ h in 1:num_hours] >= 0)
+
+ JuMP.@variable(m, temp == 0)
+
+ JuMP.@expression(m, profit[i in 1:length(products), y in start_year:end_year], 0 + temp)
+
+ JuMP.@expression(m, min_gen[y in start_year:end_year, h in 1:num_hours], 0 + temp)
+
+ JuMP.@expression(m, max_gen[y in start_year:end_year, h in 1:num_hours], 0 + temp)
+
+
+ for y in start_year:end_year
+
+ for h in 1:num_hours
+ for (i, product) in enumerate(products)
+
+ hourly_profit = rep_hour_weight[h] *
+ (calculate_revenue(product,market_prices[get_name(product)][y, h], inertia_constant, Q[get_name(product), y, h]) -
+ calculate_cost(product, get_marginal_cost(product), carbon_tax[y], emission_intensity, inertia_constant, Q[get_name(product), y, h]))
+
+ JuMP.add_to_expression!(profit[i, y], hourly_profit)
+
+ add_to_generationlimits!(min_gen[y, h],
+ max_gen[y, h],
+ Q[get_name(product), y, h],
+ product,
+ synchronous_inertia)
+
+ JuMP.@constraint(m, Q[get_name(product), y, h] <=
+ max_perc[get_name(product)][y, h] * get_maxcap(gen))
+
+ end
+
+ JuMP.@constraint(m, Q[:Energy, y, h] == max_perc[:Energy][y, h] * get_maxcap(gen))
+
+ if in(:Inertia, products) && synchronous_inertia
+ JuMP.@constraint(m, Q[:Inertia, y, h] == Q[:Energy, y, h])
+ end
+
+ JuMP.@constraint(m, min_gen[y,h] >= get_mincap(gen) * 0)
+ JuMP.@constraint(m, max_gen[y,h] <= get_maxcap(gen) * total_utilization[y, h])
+ end
+ end
+
+ JuMP.@objective(m, Max, sum(profit))
+
+ JuMP.optimize!(m)
+
+ profit_array = AxisArrays.AxisArray(value.(profit),
+ AxisArrays.Axis{:prod}(get_name.(products)),
+ AxisArrays.Axis{:year}(start_year:end_year))
+
+ energy_production = AxisArrays.AxisArray([sum(value(Q[:Energy, y, h]) * rep_hour_weight[h] for h in 1:num_hours) for y in start_year:end_year],
+ AxisArrays.Axis{:year}(start_year:end_year))
+
+
+ return profit_array, energy_production
+
+end
+
+"""
+This function does nothing if the product is not of Energy, ReserveUp or ReserveDown type
+"""
+function storage_product_constraints(product::T,
+ Q::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
+ p_out::JuMP.VariableRef,
+ p_in::JuMP.VariableRef,
+ p_r::JuMP.VariableRef,
+ p_inertia::JuMP.VariableRef
+ ) where T <: OperatingProduct
+ return
+end
+
+"""
+This function calculates the net energy provided by the storage device.
+"""
+function storage_product_constraints(product::Energy,
+ Q::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
+ p_out::JuMP.VariableRef,
+ p_in::JuMP.VariableRef,
+ p_r::JuMP.VariableRef,
+ p_inertia::JuMP.VariableRef
+ )
+ JuMP.add_to_expression!(Q, p_out - p_in)
+ return
+end
+
+"""
+This function calculates the net reserve up provided by the storage device.
+"""
+function storage_product_constraints(product::OperatingReserve{ReserveUpEMIS},
+ Q::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
+ p_out::JuMP.VariableRef,
+ p_in::JuMP.VariableRef,
+ p_r::JuMP.VariableRef,
+ p_inertia::JuMP.VariableRef
+ )
+ JuMP.add_to_expression!(Q, p_r)
+
+ return
+end
+
+"""
+This function calculates the net reserve down provided by the storage device.
+"""
+function storage_product_constraints(product::OperatingReserve{ReserveDownEMIS},
+ Q::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
+ p_out::JuMP.VariableRef,
+ p_in::JuMP.VariableRef,
+ p_r::JuMP.VariableRef,
+ p_inertia::JuMP.VariableRef
+ )
+ JuMP.add_to_expression!(Q, p_r)
+ return
+end
+
+"""
+This function calculates the inertia provided by the storage device.
+"""
+function storage_product_constraints(product::Inertia,
+ Q::JuMP.GenericAffExpr{Float64, JuMP.VariableRef},
+ p_out::JuMP.VariableRef,
+ p_in::JuMP.VariableRef,
+ p_r::JuMP.VariableRef,
+ p_inertia::JuMP.VariableRef
+ )
+ JuMP.add_to_expression!(Q, p_inertia)
+ return
+end
+
+
+"""
+This function calculates operating market profits for a storage device
+based on expected market prices stored in the investor struct.
+Returns an axis array of profits for each operating market product.
+"""
+function calculate_operating_profit(storage::P,
+ scenario_name::String,
+ all_prices::MarketPrices,
+ carbon_tax::Vector{Float64},
+ start_year::Int64,
+ end_year::Int64,
+ rep_hour_weight::Vector{Float64},
+ solver::JuMP.MOI.OptimizerWithAttributes ) where P <: StorageEMIS{<: BuildPhase}
+
+ num_hours = length(rep_hour_weight)
+ tech = get_tech(storage)
+
+ products = find_operating_products(get_products(storage))
+ energy_product = find_energy_product(products)
+
+ capacity_factors = get_capacity_factors(energy_product[1])[scenario_name]
+
+ market_prices = Dict{Symbol, Union{Array{Float64, 2}, AxisArrays.AxisArray{Float64, 2}}}()
+
+ for prod in products
+ if get_name(prod) == :Energy
+ market_prices[get_name(prod)] = get_prices(all_prices, prod)[scenario_name][get_zone(get_tech(storage)), :, :]
+ else
+ market_prices[get_name(prod)] = get_prices(all_prices, prod)[scenario_name]
+ end
+ end
+
+ max_gen = get_maxcap(storage) # maximum generation limit
+ min_gen = get_mincap(storage) # minimum generation limit
+
+ total_utilization = get_scenario_total_utilization(get_finance_data(storage))[scenario_name]
+
+ input_power_limits = get_input_active_power_limits(tech)
+ min_input = input_power_limits[:min] # minimum charging limit
+ max_input = input_power_limits[:max] # maximum charging limit
+
+ reserve_up_products = filter(p -> typeof(p) == OperatingReserve{ReserveUpEMIS}, products)
+ reserve_down_products = filter(p -> typeof(p) == OperatingReserve{ReserveDownEMIS}, products)
+
+ storage_capacity = get_storage_capacity(tech)
+ min_storage = storage_capacity[:min] # minimum storage capacity
+ max_storage = storage_capacity[:max] # maximum storage capacity
+
+ efficiency = get_efficiency(tech)
+ efficiency_in = efficiency[:in] # charging efficiency
+ efficiency_out = efficiency[:out] # discharging efficiency
+
+ initstorage = get_soc(tech)
+
+ inertia_constant = get_inertia_constant(storage)
+ synchronous_inertia = get_synchronous_inertia(storage)
+
+ m = JuMP.Model(solver)
+
+ JuMP.@variable(m, p_e[p in start_year:end_year, t in 1:num_hours] >= 0) # Unit energy production [MW]
+ JuMP.@variable(m, p_in[p in start_year:end_year, t in 1:num_hours] >= 0) # Storage charging [MW]
+
+ JuMP.@variable(m, p_ru[rp in get_name.(reserve_up_products), p in start_year:end_year, t in 1:num_hours] >= 0) # Unit energy production [MW]
+ JuMP.@variable(m, p_rd[rp in get_name.(reserve_down_products), p in start_year:end_year, t in 1:num_hours] >= 0) # Storage charging [MW]
+
+ JuMP.@variable(m, p_inertia[p in start_year:end_year, t in 1:num_hours] >= 0) # Unit Inertia Provision [MW-s]
+
+ JuMP.@variable(m, p_in_ru[rp in get_name.(reserve_up_products), p in start_year:end_year, t in 1:num_hours] >= 0) # reserve up provided by storage charging [MW]
+ JuMP.@variable(m, p_in_rd[rp in get_name.(reserve_down_products), p in start_year:end_year, t in 1:num_hours] >= 0) # reserve down provided by storage charging [MW]
+
+ JuMP.@variable(m, p_out_ru[rp in get_name.(reserve_up_products), p in start_year:end_year, t in 1:num_hours] >= 0) # reserve up provided by storage discharging [MW]
+ JuMP.@variable(m, p_out_rd[rp in get_name.(reserve_down_products), p in start_year:end_year, t in 1:num_hours] >= 0) # reserve down provided by storage discharging [MW]
+
+ JuMP.@variable(m, p_out_inertia[p in start_year:end_year, t in 1:num_hours] >= 0) # inertia provided by storage discharging [MW-s]
+ JuMP.@variable(m, p_in_inertia[p in start_year:end_year, t in 1:num_hours] >= 0) # inertia provided by storage charging [MW-s]
+
+ JuMP.@variable(m, temp == 0)
+
+ # Quantity of products provided
+ JuMP.@expression(m, Q[i in get_name.(products), p in start_year:end_year, t in 1:num_hours], 0 + temp)
+
+ # Profit for each product
+ JuMP.@expression(m, profit[i in 1:length(products), p in start_year:end_year], 0 + temp)
+
+ # Storage level evolution
+ JuMP.@expression(m, storage_level[p in start_year:end_year, t in 1:num_hours],
+ p_in[p, t] * efficiency_in - p_e[p, t] / efficiency_out)
+
+ for p in start_year:end_year
+ for t in 1:num_hours
+
+ for (i, product) in enumerate(products)
+
+ if product in reserve_up_products
+
+ storage_product_constraints(product,
+ Q[get_name(product), p, t],
+ p_e[p, t],
+ p_in[p, t],
+ p_ru[get_name(product), p, t],
+ p_inertia[p, t])
+
+ elseif product in reserve_down_products
+
+ storage_product_constraints(product,
+ Q[get_name(product), p, t],
+ p_e[p, t],
+ p_in[p, t],
+ p_rd[get_name(product), p, t],
+ p_inertia[p, t])
+
+ else
+ storage_product_constraints(product,
+ Q[get_name(product), p, t],
+ p_e[p, t],
+ p_in[p, t],
+ p_e[p, t],
+ p_inertia[p, t])
+ end
+
+ hourly_profit = rep_hour_weight[t] *
+ (calculate_revenue(product, market_prices[get_name(product)][p, t], inertia_constant, Q[get_name(product), p, t]))
+
+ JuMP.add_to_expression!(profit[i, p], hourly_profit)
+ #=
+ JuMP.@constraint(m, Q[get_name(product), y, h] <=
+ max_perc[get_name(product)][y, h] * get_maxcap(gen))
+ =#
+ end
+
+ if t == 1
+ JuMP.add_to_expression!(storage_level[p, t], initstorage)
+ else
+ JuMP.add_to_expression!(storage_level[p, t], storage_level[p, t - 1])
+ end
+
+ # Storage technical constraints
+ #JuMP.@constraint(m, p_e[p, t] <= capacity_factors[p, t] * max_gen) # Capacity factor constraint
+
+ JuMP.@constraint(m, p_e[p,t] - sum(p_out_rd[rp, p, t] for rp in get_name.(reserve_down_products)) >= min_gen * 0) # Minimum output dispatch
+ JuMP.@constraint(m, p_e[p,t] + sum(p_out_ru[rp, p, t] for rp in get_name.(reserve_up_products)) + p_out_inertia[p, t] == max_gen * total_utilization[p, t]) # Maximum output dispatch
+
+ JuMP.@constraint(m, p_in[p,t] - sum(p_in_ru[rp, p, t] for rp in get_name.(reserve_up_products)) - p_in_inertia[p, t] >= min_input * 0) # Minimum input dispatch
+ JuMP.@constraint(m, p_in[p,t] + sum(p_in_rd[rp, p, t] for rp in get_name.(reserve_down_products)) <= max_input) # Maximum input dispatch
+
+ for rp in get_name.(reserve_up_products)
+ JuMP.@constraint(m, p_ru[rp, p, t] <= p_out_ru[rp, p, t] + p_in_ru[rp, p, t]) # Total storage reserve up
+ end
+
+ for rp in get_name.(reserve_down_products)
+ JuMP.@constraint(m, p_rd[rp, p, t] <= p_out_rd[rp, p, t] + p_in_rd[rp, p, t]) # Total storage reserve down
+ end
+
+ JuMP.@constraint(m, p_inertia[p, t] <= p_out_inertia[p, t] + p_in_inertia[p, t])
+
+ JuMP.@constraint(m, storage_level[p, t] >= min_storage * 0) # Minimum storage level
+ JuMP.@constraint(m, storage_level[p, t] <= max_storage) # Maximum storage level
+
+ end
+ end
+
+ JuMP.@objective(m, Max, sum(profit))
+
+ JuMP.optimize!(m)
+
+ profit_array = AxisArrays.AxisArray(value.(profit),
+ AxisArrays.Axis{:prod}(get_name.(products)),
+ AxisArrays.Axis{:year}(start_year:end_year))
+
+
+ energy_consumption = AxisArrays.AxisArray(zeros(end_year - start_year + 1), AxisArrays.Axis{:year}(start_year:end_year))
+
+ energy_consumption = AxisArrays.AxisArray([sum(value(p_in[y, h]) * rep_hour_weight[h] for h in 1:num_hours) for y in start_year:end_year],
+ AxisArrays.Axis{:year}(start_year:end_year))
+
+
+ return profit_array, energy_consumption
+
+end
+
+"""
+This function updates the expected profit array of the project
+if the product is an operating market product.
+Returns nothing.
+"""
+function update_profit!(project::P,
+ scenario_name::String,
+ product::T,
+ operating_profit::AxisArrays.AxisArray{Float64, 2},
+ capacity_revenues::Vector{Float64},
+ capacity_forward_years::Int64,
+ rec_revenues::Vector{Float64},
+ price_years::NamedTuple{(:start_year, :end_year),
+ Tuple{Int64, Int64}},
+ update_years::NamedTuple{(:start_year, :end_year),
+ Tuple{Int64, Int64}},
+ iteration_year::Int64) where {P <: Project{<: BuildPhase}, T <: OperatingProduct}
+
+ profit = [operating_profit[get_name(product), y]
+ for y in price_years[:start_year]:price_years[:end_year]]
+
+ set_scenario_profit!(get_finance_data(project),
+ scenario_name,
+ get_name(product),
+ iteration_year,
+ update_years[:start_year],
+ update_years[:end_year],
+ profit)
+
+ return
+end
diff --git a/src/investor_functions/prediction/prediction_methodology.jl b/src/investor_functions/prediction/prediction_methodology.jl
index 2003af1..d774158 100644
--- a/src/investor_functions/prediction/prediction_methodology.jl
+++ b/src/investor_functions/prediction/prediction_methodology.jl
@@ -1,199 +1,227 @@
-""""
-This function gathers data for making price and other market data predictions.
-"""
-function gather_prediction_parameters(investor::Investor,
- sys_data_dir::String,
- iteration_year::Int64)
- investor_name = get_name(investor)
- investor_dir = get_data_dir(investor)
-
- load_dir = joinpath(investor_dir, "timeseries_data_files", "Load")
- dir_exists(load_dir)
- cp(joinpath(sys_data_dir, "timeseries_data_files", "Load", "rep_load_$(iteration_year - 1).csv"),
- joinpath(load_dir, "load_$(iteration_year - 1).csv"), force = true)
-
- reserve_definition = read_data(joinpath(sys_data_dir, "markets_data", "reserve_products.csv"))
-
- reserve_products = String.(split(reserve_definition[1, "all_products"], "; "))
- ordc_products = String.(split(reserve_definition[1, "ordc_products"], "; "))
-
- reserve_dir = joinpath(investor_dir, "timeseries_data_files", "Reserves")
- dir_exists(reserve_dir)
-
- for reserve in reserve_products
- cp(joinpath(sys_data_dir, "timeseries_data_files", "Reserves", "rep_$(reserve)_$(iteration_year - 1).csv"),
- joinpath(reserve_dir, "$(reserve)_$(iteration_year - 1).csv"), force = true)
- end
-
- market_names = get_markets(investor)
-
- carbon_tax = get_carbon_tax(investor)
-
- rep_hour_weight = get_rep_hour_weight(investor)
-
- scenarios = get_scenario_data(get_forecast(investor))
-
- return investor_name, investor_dir, market_names, carbon_tax, reserve_products, ordc_products, rep_hour_weight, scenarios
-end
-
-"""
-This function runs CEM for price predictions based on user-defined parallelization settings.
-"""
-function create_investor_predictions(investors::Vector{Investor},
- active_projects::Vector{Project},
- iteration_year::Int64,
- yearly_horizon::Int64,
- sys_data_dir::String,
- sys_results_dir::String,
- average_capital_cost_multiplier::Float64,
- zones::Vector{String},
- lines::Vector{ZonalLine},
- peak_load::Float64,
- solver::JuMP.MOI.OptimizerWithAttributes,
- parallelize_investors::Bool,
- parallelize_scenarios::Bool)
-
- if parallelize_investors
-
- if parallelize_scenarios
-
- scenarios_pmap = Scenario[]
- investor_name_pmap = String[]
- investor_dir_pmap = String[]
- market_names_pmap = Vector{Symbol}[]
- carbon_tax_pmap = Vector{Float64}[]
- reserve_products_pmap = Vector{String}[]
- ordc_products_pmap = Vector{String}[]
- rep_hour_weight_pmap = Vector{Float64}[]
- expected_portfolio_pmap = Vector{Project}[]
-
- for investor in investors
-
- investor_name,
- investor_dir,
- market_names,
- carbon_tax,
- reserve_products,
- ordc_products,
- rep_hour_weight,
- scenarios = gather_prediction_parameters(investor, sys_data_dir, iteration_year)
-
- for scenario in scenarios
- push!(scenarios_pmap, scenario)
- push!(investor_name_pmap, investor_name)
- push!(investor_dir_pmap, investor_dir)
- push!(market_names_pmap, market_names)
- push!(carbon_tax_pmap, carbon_tax)
- push!(reserve_products_pmap, reserve_products)
- push!(ordc_products_pmap, ordc_products)
- push!(rep_hour_weight_pmap, rep_hour_weight)
- push!(expected_portfolio_pmap, active_projects)
-
- end
-
- end
-
- num_tasks = length(scenarios_pmap)
- Distributed.pmap(create_expected_marketdata,
- investor_dir_pmap,
- market_names_pmap,
- carbon_tax_pmap,
- reserve_products_pmap,
- ordc_products_pmap,
- expected_portfolio_pmap,
- repeat([zones], num_tasks),
- repeat([lines], num_tasks),
- repeat([peak_load], num_tasks),
- rep_hour_weight_pmap,
- repeat([average_capital_cost_multiplier], num_tasks),
- scenarios_pmap,
- repeat([iteration_year], num_tasks),
- repeat([yearly_horizon], num_tasks),
- repeat([solver], num_tasks),
- repeat([sys_results_dir], num_tasks),
- investor_name_pmap)
-
- else
-
- num_tasks = length(investors)
- Distributed.pmap(parallelize_only_investors,
- investors,
- repeat([sys_data_dir], num_tasks),
- repeat([active_projects], num_tasks),
- repeat([zones], num_tasks),
- repeat([lines], num_tasks),
- repeat([peak_load], num_tasks),
- repeat([average_capital_cost_multiplier], num_tasks),
- repeat([iteration_year], num_tasks),
- repeat([yearly_horizon], num_tasks),
- repeat([solver], num_tasks),
- repeat([sys_results_dir], num_tasks),
- get_name.(investors))
- end
-
- else
-
- for investor in investors
-
- investor_name,
- investor_dir,
- market_names,
- carbon_tax,
- reserve_products,
- ordc_products,
- rep_hour_weight,
- scenarios = gather_prediction_parameters(investor, sys_data_dir, iteration_year)
-
- if parallelize_scenarios
-
- num_scenarios = length(scenarios)
-
- Distributed.pmap(create_expected_marketdata,
- repeat([investor_dir], num_scenarios),
- repeat([market_names], num_scenarios),
- repeat([carbon_tax], num_scenarios),
- repeat([reserve_products], num_scenarios),
- repeat([ordc_products], num_scenarios),
- repeat([active_projects], num_scenarios),
- repeat([zones], num_scenarios),
- repeat([lines], num_scenarios),
- repeat([peak_load], num_scenarios),
- repeat([rep_hour_weight], num_scenarios),
- repeat([average_capital_cost_multiplier], num_scenarios),
- scenarios,
- repeat([iteration_year], num_scenarios),
- repeat([yearly_horizon], num_scenarios),
- repeat([solver], num_scenarios),
- repeat([sys_results_dir], num_scenarios),
- repeat([investor_name], num_scenarios))
-
- else
-
- for scenario in scenarios
- create_expected_marketdata(investor_dir,
- market_names,
- carbon_tax,
- reserve_products,
- ordc_products,
- active_projects,
- zones,
- lines,
- peak_load,
- rep_hour_weight,
- average_capital_cost_multiplier,
- scenario,
- iteration_year,
- yearly_horizon,
- solver,
- sys_results_dir,
- investor_name)
- end
-
- end
-
- end
-
- end
-
- return
-end
+""""
+This function gathers data for making price and other market data predictions.
+"""
+function gather_prediction_parameters(investor::Investor,
+ sys_data_dir::String,
+ iteration_year::Int64)
+ investor_name = get_name(investor)
+ investor_dir = get_data_dir(investor)
+
+ load_dir = joinpath(investor_dir, "timeseries_data_files", "Load")
+ dir_exists(load_dir)
+ cp(joinpath(sys_data_dir, "timeseries_data_files", "Load", "rep_load_$(iteration_year - 1).csv"),
+ joinpath(load_dir, "load_$(iteration_year - 1).csv"), force = true)
+
+ reserve_definition = read_data(joinpath(sys_data_dir, "markets_data", "reserve_products.csv"))
+
+ reserve_products = String.(split(reserve_definition[1, "all_products"], "; "))
+ ordc_products = String.(split(reserve_definition[1, "ordc_products"], "; "))
+
+ reserve_dir = joinpath(investor_dir, "timeseries_data_files", "Reserves")
+ dir_exists(reserve_dir)
+
+ for reserve in reserve_products
+ cp(joinpath(sys_data_dir, "timeseries_data_files", "Reserves", "rep_$(reserve)_$(iteration_year - 1).csv"),
+ joinpath(reserve_dir, "$(reserve)_$(iteration_year - 1).csv"), force = true)
+ end
+
+ market_names = get_markets(investor)
+
+ carbon_tax = get_carbon_tax(investor)
+
+ rep_hour_weight = get_rep_hour_weight(investor)
+
+ scenarios = get_scenario_data(get_forecast(investor))
+
+ return investor_name, investor_dir, market_names, carbon_tax, reserve_products, ordc_products, rep_hour_weight, scenarios
+end
+
+"""
+This function runs CEM for price predictions based on user-defined parallelization settings.
+"""
+function create_investor_predictions(investors::Vector{Investor},
+ active_projects::Vector{Project},
+ iteration_year::Int64,
+ yearly_horizon::Int64,
+ sys_data_dir::String,
+ sys_results_dir::String,
+ average_capital_cost_multiplier::Float64,
+ zones::Vector{String},
+ lines::Vector{ZonalLine},
+ peak_load::Float64,
+ rps_target::String,
+ reserve_penalty::String,
+ resource_adequacy::ResourceAdequacy,
+ irm_scalar::Float64,
+ solver::JuMP.MOI.OptimizerWithAttributes,
+ parallelize_investors::Bool,
+ parallelize_scenarios::Bool)
+
+ if parallelize_investors
+
+ if parallelize_scenarios
+
+ scenarios_pmap = Scenario[]
+ investor_name_pmap = String[]
+ investor_dir_pmap = String[]
+ market_names_pmap = Vector{Symbol}[]
+ carbon_tax_pmap = Vector{Float64}[]
+ reserve_products_pmap = Vector{String}[]
+ ordc_products_pmap = Vector{String}[]
+ rps_target_pmap = String[]
+ reserve_penalty_pmap = String[]
+ resource_adequacy_pmap = ResourceAdequacy[]
+ irm_scalar_pmap = Float64[]
+ rep_hour_weight_pmap = Vector{Float64}[]
+ expected_portfolio_pmap = Vector{Project}[]
+
+ for investor in investors
+
+ investor_name,
+ investor_dir,
+ market_names,
+ carbon_tax,
+ reserve_products,
+ ordc_products,
+ rep_hour_weight,
+ scenarios = gather_prediction_parameters(investor, sys_data_dir, iteration_year)
+
+ for scenario in scenarios
+ push!(scenarios_pmap, scenario)
+ push!(investor_name_pmap, investor_name)
+ push!(investor_dir_pmap, investor_dir)
+ push!(market_names_pmap, market_names)
+ push!(carbon_tax_pmap, carbon_tax)
+ push!(reserve_products_pmap, reserve_products)
+ push!(ordc_products_pmap, ordc_products)
+ push!(rps_target_pmap, rps_target)
+ push!(reserve_penalty_pmap, reserve_penalty)
+ push!(resource_adequacy_pmap, resource_adequacy)
+ push!(irm_scalar_pmap, irm_scalar)
+ push!(rep_hour_weight_pmap, rep_hour_weight)
+ push!(expected_portfolio_pmap, active_projects)
+
+ end
+
+ end
+
+ num_tasks = length(scenarios_pmap)
+ Distributed.pmap(create_expected_marketdata,
+ investor_dir_pmap,
+ market_names_pmap,
+ carbon_tax_pmap,
+ reserve_products_pmap,
+ ordc_products_pmap,
+ rps_target_pmap,
+ reserve_penalty_pmap,
+ resource_adequacy_pmap,
+ irm_scalar_pmap,
+ expected_portfolio_pmap,
+ repeat([zones], num_tasks),
+ repeat([lines], num_tasks),
+ repeat([peak_load], num_tasks),
+ rep_hour_weight_pmap,
+ repeat([average_capital_cost_multiplier], num_tasks),
+ scenarios_pmap,
+ repeat([iteration_year], num_tasks),
+ repeat([yearly_horizon], num_tasks),
+ repeat([solver], num_tasks),
+ repeat([sys_results_dir], num_tasks),
+ investor_name_pmap)
+
+ else
+
+ num_tasks = length(investors)
+ Distributed.pmap(parallelize_only_investors,
+ investors,
+ repeat([sys_data_dir], num_tasks),
+ repeat([active_projects], num_tasks),
+ repeat([rps_target], num_tasks),
+ repeat([reserve_penalty], num_tasks),
+ repeat([resource_adequacy], num_tasks),
+ repeat([irm_scalar], num_tasks),
+ repeat([zones], num_tasks),
+ repeat([lines], num_tasks),
+ repeat([peak_load], num_tasks),
+ repeat([average_capital_cost_multiplier], num_tasks),
+ repeat([iteration_year], num_tasks),
+ repeat([yearly_horizon], num_tasks),
+ repeat([solver], num_tasks),
+ repeat([sys_results_dir], num_tasks),
+ get_name.(investors))
+ end
+
+ else
+
+ for investor in investors
+
+ investor_name,
+ investor_dir,
+ market_names,
+ carbon_tax,
+ reserve_products,
+ ordc_products,
+ rep_hour_weight,
+ scenarios = gather_prediction_parameters(investor, sys_data_dir, iteration_year)
+
+ if parallelize_scenarios
+
+ num_scenarios = length(scenarios)
+
+ Distributed.pmap(create_expected_marketdata,
+ repeat([investor_dir], num_scenarios),
+ repeat([market_names], num_scenarios),
+ repeat([carbon_tax], num_scenarios),
+ repeat([reserve_products], num_scenarios),
+ repeat([ordc_products], num_scenarios),
+ repeat([rps_target], num_scenarios),
+ repeat([reserve_penalty], num_scenarios),
+ repeat([resource_adequacy], num_scenarios),
+ repeat([irm_scalar], num_scenarios),
+ repeat([active_projects], num_scenarios),
+ repeat([zones], num_scenarios),
+ repeat([lines], num_scenarios),
+ repeat([peak_load], num_scenarios),
+ repeat([rep_hour_weight], num_scenarios),
+ repeat([average_capital_cost_multiplier], num_scenarios),
+ scenarios,
+ repeat([iteration_year], num_scenarios),
+ repeat([yearly_horizon], num_scenarios),
+ repeat([solver], num_scenarios),
+ repeat([sys_results_dir], num_scenarios),
+ repeat([investor_name], num_scenarios))
+
+ else
+
+ for scenario in scenarios
+ create_expected_marketdata(investor_dir,
+ market_names,
+ carbon_tax,
+ reserve_products,
+ ordc_products,
+ rps_target,
+ reserve_penalty,
+ resource_adequacy,
+ irm_scalar,
+ active_projects,
+ zones,
+ lines,
+ peak_load,
+ rep_hour_weight,
+ average_capital_cost_multiplier,
+ scenario,
+ iteration_year,
+ yearly_horizon,
+ solver,
+ sys_results_dir,
+ investor_name)
+ end
+
+ end
+
+ end
+
+ end
+
+ return
+end
diff --git a/src/investor_functions/prediction/total_profit.jl b/src/investor_functions/prediction/total_profit.jl
index 6967cae..adcf628 100644
--- a/src/investor_functions/prediction/total_profit.jl
+++ b/src/investor_functions/prediction/total_profit.jl
@@ -1,203 +1,216 @@
-"""
-This function updates the expected profit of the project from all products.
-Returns nothing.
-"""
-function update_expected_profit!(project::P,
- scenario_data::Vector{Scenario},
- market_prices::MarketPrices,
- carbon_tax_data::Vector{Float64},
- price_years::NamedTuple{(:start_year, :end_year),
- Tuple{Int64, Int64}},
- update_years::NamedTuple{(:start_year, :end_year),
- Tuple{Int64, Int64}},
- rep_hour_weight::Vector{Float64},
- queue_cost::Vector{Float64},
- capacity_forward_years::Int64,
- iteration_year::Int64,
- solver::JuMP.MOI.OptimizerWithAttributes) where P <: Project{<: BuildPhase}
-
- extend_profit_array(project, get_name.(scenario_data), iteration_year)
-
- carbon_tax_vector = carbon_tax_data[iteration_year:end]
-
- profit_array_length = price_years[:end_year] - price_years[:start_year] + 1
-
- operating_profit_array = AxisArrays.AxisArray{Float64, 2}[] # Operating profit array for each scenario
- capacity_revenue_array = Vector{Float64}[] # Capacity revenue array for each scenario
- rec_revenue_array = Vector{Float64}[] # REC revenue array for each scenario
-
- expected_energy_production = zeros(profit_array_length)
-
- capacity_prices = get_capacity_price(market_prices)
- rec_prices = get_rec_price(market_prices)
-
- sz = price_years[:end_year] - price_years[:start_year] + 1
-
- total_annual_cost = calculate_annualized_costs(project,
- sz,
- queue_cost)
-
- for scenario in scenario_data
- operating_profit,
- energy_production = calculate_operating_profit(project,
- get_name(scenario),
- market_prices,
- carbon_tax_vector,
- price_years[:start_year],
- price_years[:end_year],
- rep_hour_weight,
- solver)
- push!(operating_profit_array, operating_profit)
-
- # Calculate expected energy production
- expected_energy_production += get_probability(scenario) * energy_production
-
- capacity_revenues = zeros(profit_array_length)
- rec_revenues = zeros(profit_array_length)
-
-
- for product in get_products(project)
- if !isnothing(capacity_prices)
- capacity_revenues = update_capacity_revenues!(
- product,
- get_name(scenario),
- price_years,
- capacity_revenues,
- capacity_prices,
- get_maxcap(project)
- )
- end
-
- if !isnothing(rec_prices)
- rec_revenues = update_rec_revenues!(
- product,
- price_years,
- rec_revenues,
- rec_prices[get_name(scenario)],
- energy_production
- )
- end
- end
-
- annual_operating_profit = [sum(operating_profit[:, y])
- for y in price_years[:start_year]:price_years[:end_year]]
-
- capacity_going_forward_cost = total_annual_cost - annual_operating_profit - rec_revenues
-
- for product in get_products(project)
- update_profit!(project,
- get_name(scenario),
- product,
- operating_profit,
- capacity_revenues,
- capacity_forward_years,
- rec_revenues,
- price_years,
- update_years,
- iteration_year)
- end
-
- push!(capacity_revenue_array, capacity_revenues)
- push!(rec_revenue_array, rec_revenues)
- end
-
- profit_axis_values = AxisArrays.axisvalues(operating_profit_array[1])
-
- expected_operating_profit = operating_profit_array[1]
-
- for x in profit_axis_values[1]
- for y in profit_axis_values[2]
- expected_operating_profit[x, y] = sum(get_probability(scenario_data[i]) * operating_profit_array[i][x, y] for i in 1:length(operating_profit_array))
- end
- end
-
- # Calculate expected profits
- expected_annual_operating_profit = [sum(expected_operating_profit[x, y] for x in profit_axis_values[1]) for y in profit_axis_values[2]]
-
- expected_capacity_revenues = [sum(get_probability(scenario_data[i]) * capacity_revenue_array[i][y] for i in 1:length(capacity_revenue_array))
- for y in 1:profit_array_length]
-
- expected_rec_revenues = [sum(get_probability(scenario_data[i]) * rec_revenue_array[i][y] for i in 1:length(rec_revenue_array))
- for y in 1:profit_array_length]
-
- #Calculate capacity market going forward cost.
- expected_capacity_going_forward_cost = total_annual_cost - expected_annual_operating_profit - expected_rec_revenues
- capacity_market_bid = 0.0
-
- capacity_revenue_start_year = max(1, capacity_forward_years - price_years[:start_year] + 1)
-
- if capacity_revenue_start_year <= length(expected_capacity_going_forward_cost)
- capacity_market_bid = round(max(0.0, expected_capacity_going_forward_cost[capacity_revenue_start_year] / get_maxcap(project)), digits = 4)
- end
-
- #Calculate REC market bid.
- rec_market_bid = max(0.0, (total_annual_cost[1] - expected_annual_operating_profit[1] - expected_capacity_revenues[1]) / max(1e-3, expected_energy_production[1]))
-
- for product in get_products(project)
- update_bid!(product,
- capacity_market_bid,
- rec_market_bid,
- expected_energy_production[1]
- )
- end
-
- # Sets the project profit values beyond the horizon equal to the last value.
- if get_end_life_year(project) > update_years[:end_year]
- for scenario_name in get_name.(scenario_data)
- for product_name in get_name.(get_products(project))
- for y in (update_years[:end_year] + 1):get_end_life_year(project)
- profit = get_scenario_profit(get_finance_data(project))[scenario_name][iteration_year][Symbol(product_name),
- update_years[:end_year]]
- set_scenario_profit!(get_finance_data(project), scenario_name, product_name, iteration_year, y, profit)
- end
- end
- end
- end
-
- return
-end
-
-"""
-This function does nothing if the product is neither
-an operating market or capacity or REC product.
-The update_profit functions for each product is defined in files pertaning to those products.
-"""
-function update_profit!(project::P,
- scenario_name::String,
- product::T,
- operating_profit::AxisArrays.AxisArray{Float64, 2},
- capacity_revenues::Vector{Float64},
- capacity_forward_years::Int64,
- rec_revenues::Vector{Float64},
- price_years::NamedTuple{(:start_year, :end_year),
- Tuple{Int64, Int64}},
- update_years::NamedTuple{(:start_year, :end_year),
- Tuple{Int64, Int64}},
- iteration_year::Int64) where {P <: Project{<: BuildPhase}, T <: Product}
- return
-end
-
-"""
-This function does nothing if project is not an option type.
-Returns nothing
-"""
-function extend_profit_array(project::P, scenario_names::Vector{String}, iteration_year::Int64) where P <: Project{<: BuildPhase}
- return
-end
-
-"""
-This function extends the profit array of option projects
-if their end life year exceeds existing length of the profit array.
-"""
-function extend_profit_array(project::P, scenario_names::Vector{String}, iteration_year::Int64) where P <: Project{Option}
-
- if get_end_life_year(project) > size(get_scenario_profit(get_finance_data(project))[scenario_names[1]][iteration_year], 2)
- for scenario_name in scenario_names
- profit = AxisArrays.AxisArray(zeros(length(get_products(project)), get_end_life_year(project)),
- AxisArrays.Axis{:prod}(get_name.(get_products(project))),
- AxisArrays.Axis{:year}(1:1:get_end_life_year(project)))
- set_scenario_profit!(get_finance_data(project), scenario_name, iteration_year, profit)
- end
- end
- return
-end
+"""
+This function updates the expected profit of the project from all products.
+Returns nothing.
+"""
+function update_expected_profit!(project::P,
+ scenario_data::Vector{Scenario},
+ market_prices::MarketPrices,
+ carbon_tax_data::Vector{Float64},
+ price_years::NamedTuple{(:start_year, :end_year),
+ Tuple{Int64, Int64}},
+ update_years::NamedTuple{(:start_year, :end_year),
+ Tuple{Int64, Int64}},
+ rep_hour_weight::Vector{Float64},
+ queue_cost::Vector{Float64},
+ capacity_forward_years::Int64,
+ iteration_year::Int64,
+ solver::JuMP.MOI.OptimizerWithAttributes) where P <: Project{<: BuildPhase}
+
+ extend_profit_array(project, get_name.(scenario_data), iteration_year)
+
+ carbon_tax_vector = carbon_tax_data[iteration_year:end]
+
+ profit_array_length = price_years[:end_year] - price_years[:start_year] + 1
+
+ operating_profit_array = AxisArrays.AxisArray{Float64, 2}[] # Operating profit array for each scenario
+ capacity_revenue_array = Vector{Float64}[] # Capacity revenue array for each scenario
+ rec_revenue_array = Vector{Float64}[] # REC revenue array for each scenario
+
+ expected_energy_production = zeros(profit_array_length)
+
+ capacity_prices = get_capacity_price(market_prices)
+ rec_prices = get_rec_price(market_prices)
+
+ sz = price_years[:end_year] - price_years[:start_year] + 1
+
+ total_annual_cost = calculate_annualized_costs(project,
+ sz,
+ queue_cost)
+
+ for scenario in scenario_data
+ operating_profit,
+ energy_production = calculate_operating_profit(project,
+ get_name(scenario),
+ market_prices,
+ carbon_tax_vector,
+ price_years[:start_year],
+ price_years[:end_year],
+ rep_hour_weight,
+ solver)
+ push!(operating_profit_array, operating_profit)
+
+ # Calculate expected energy production (NOTE: For storage, we save expected energy consumption (instead of production) here, for adding to clean energy (REC) requirement constraint)
+ expected_energy_production += get_probability(scenario) * energy_production
+
+ capacity_revenues = zeros(profit_array_length)
+ rec_revenues = zeros(profit_array_length)
+
+
+ for product in get_products(project)
+ if !isnothing(capacity_prices)
+ capacity_revenues = update_capacity_revenues!(
+ product,
+ get_name(scenario),
+ price_years,
+ capacity_revenues,
+ capacity_prices,
+ get_maxcap(project)
+ )
+ end
+
+ if !isnothing(rec_prices)
+ rec_revenues = update_rec_revenues!(
+ product,
+ price_years,
+ rec_revenues,
+ rec_prices[get_name(scenario)],
+ energy_production
+ )
+ end
+ end
+
+ annual_operating_profit = [sum(operating_profit[:, y])
+ for y in price_years[:start_year]:price_years[:end_year]]
+
+ capacity_going_forward_cost = total_annual_cost - annual_operating_profit - rec_revenues
+
+ for product in get_products(project)
+ update_profit!(project,
+ get_name(scenario),
+ product,
+ operating_profit,
+ capacity_revenues,
+ capacity_forward_years,
+ rec_revenues,
+ price_years,
+ update_years,
+ iteration_year)
+ end
+
+ push!(capacity_revenue_array, capacity_revenues)
+ push!(rec_revenue_array, rec_revenues)
+ end
+
+ profit_axis_values = AxisArrays.axisvalues(operating_profit_array[1])
+
+ expected_operating_profit = operating_profit_array[1]
+
+ for x in profit_axis_values[1]
+ for y in profit_axis_values[2]
+ expected_operating_profit[x, y] = sum(get_probability(scenario_data[i]) * operating_profit_array[i][x, y] for i in 1:length(operating_profit_array))
+ end
+ end
+
+ # Calculate expected profits
+ expected_annual_operating_profit = [sum(expected_operating_profit[x, y] for x in profit_axis_values[1]) for y in profit_axis_values[2]]
+
+ expected_capacity_revenues = [sum(get_probability(scenario_data[i]) * capacity_revenue_array[i][y] for i in 1:length(capacity_revenue_array))
+ for y in 1:profit_array_length]
+
+ expected_rec_revenues = [sum(get_probability(scenario_data[i]) * rec_revenue_array[i][y] for i in 1:length(rec_revenue_array))
+ for y in 1:profit_array_length]
+
+ #Calculate capacity market going forward cost.
+ expected_capacity_going_forward_cost = total_annual_cost - expected_annual_operating_profit - expected_rec_revenues
+ capacity_market_bid = 0.0
+
+ capacity_revenue_start_year = max(1, capacity_forward_years - price_years[:start_year] + 1)
+
+ if capacity_revenue_start_year <= length(expected_capacity_going_forward_cost)
+ capacity_market_bid = round(max(0.0, expected_capacity_going_forward_cost[capacity_revenue_start_year] / get_maxcap(project)), digits = 4)
+ end
+
+ #Calculate REC market bid.
+ if length(expected_annual_operating_profit) >= 1
+ rec_market_bid = max(0.0, (total_annual_cost[1] - expected_annual_operating_profit[1] - expected_capacity_revenues[1]) / max(1e-3, expected_energy_production[1]))
+ for product in get_products(project)
+ update_bid!(product,
+ capacity_market_bid,
+ rec_market_bid,
+ expected_energy_production[1],
+ iteration_year
+ )
+ set_expected_production!(product, expected_energy_production[1])
+ end
+ else
+ rec_market_bid = 0.0
+ for product in get_products(project)
+ update_bid!(product,
+ capacity_market_bid,
+ rec_market_bid,
+ 0.0,
+ iteration_year
+ )
+ end
+ end
+
+ # Sets the project profit values beyond the horizon equal to the last value.
+ if get_end_life_year(project) > update_years[:end_year]
+ for scenario_name in get_name.(scenario_data)
+ for product_name in get_name.(get_products(project))
+ for y in (update_years[:end_year] + 1):get_end_life_year(project)
+ profit = get_scenario_profit(get_finance_data(project))[scenario_name][iteration_year][Symbol(product_name),
+ update_years[:end_year]]
+ set_scenario_profit!(get_finance_data(project), scenario_name, product_name, iteration_year, y, profit)
+ end
+ end
+ end
+ end
+
+ return
+end
+
+"""
+This function does nothing if the product is neither
+an operating market or capacity or REC product.
+The update_profit functions for each product is defined in files pertaning to those products.
+"""
+function update_profit!(project::P,
+ scenario_name::String,
+ product::T,
+ operating_profit::AxisArrays.AxisArray{Float64, 2},
+ capacity_revenues::Vector{Float64},
+ capacity_forward_years::Int64,
+ rec_revenues::Vector{Float64},
+ price_years::NamedTuple{(:start_year, :end_year),
+ Tuple{Int64, Int64}},
+ update_years::NamedTuple{(:start_year, :end_year),
+ Tuple{Int64, Int64}},
+ iteration_year::Int64) where {P <: Project{<: BuildPhase}, T <: Product}
+ return
+end
+
+"""
+This function does nothing if project is not an option type.
+Returns nothing
+"""
+function extend_profit_array(project::P, scenario_names::Vector{String}, iteration_year::Int64) where P <: Project{<: BuildPhase}
+ return
+end
+
+"""
+This function extends the profit array of option projects
+if their end life year exceeds existing length of the profit array.
+"""
+function extend_profit_array(project::P, scenario_names::Vector{String}, iteration_year::Int64) where P <: Project{Option}
+
+ if get_end_life_year(project) > size(get_scenario_profit(get_finance_data(project))[scenario_names[1]][iteration_year], 2)
+ for scenario_name in scenario_names
+ profit = AxisArrays.AxisArray(zeros(length(get_products(project)), get_end_life_year(project)),
+ AxisArrays.Axis{:prod}(get_name.(get_products(project))),
+ AxisArrays.Axis{:year}(1:1:get_end_life_year(project)))
+ set_scenario_profit!(get_finance_data(project), scenario_name, iteration_year, profit)
+ end
+ end
+ return
+end
diff --git a/src/investor_functions/realized_profits_calculator.jl b/src/investor_functions/realized_profits_calculator.jl
index 1da7a4a..9da9dd1 100644
--- a/src/investor_functions/realized_profits_calculator.jl
+++ b/src/investor_functions/realized_profits_calculator.jl
@@ -6,12 +6,14 @@ function calculate_realized_operating_profit(prices::AxisArrays.AxisArray{Float6
output::Array{Float64, 2},
realized_hour_weight::Vector{Float64})
- profit = sum(realized_hour_weight[i] *
- ((prices[1, i] - marginal_cost) * output[1, i])
+ replace!(prices, NaN => 0.0)
+ replace!(output, NaN => 0.0)
+
+ profit = sum(((prices[1, i] - marginal_cost) * output[1, i])
for i in 1:size(output, 2))
- return profit
+ return profit
end
"""
@@ -25,8 +27,11 @@ function calculate_realized_operating_profit(prices::AxisArrays.AxisArray{Float6
realized_hour_weight::Vector{Float64},
resolution::Int64)
- profit = sum(realized_hour_weight[i] *
- ((prices[1, i] - marginal_cost[i]) * output[1, i] - carbon_emissions[i] * carbon_tax)
+ replace!(prices, NaN => 0.0)
+ replace!(output, NaN => 0.0)
+ replace!(marginal_cost, NaN => 0.0)
+
+ profit = sum((max(0.0, (prices[1, i] - marginal_cost[i]) * output[1, i] - carbon_emissions[i] * carbon_tax))
for i in 1:size(output, 2))
@@ -42,8 +47,11 @@ function calculate_realized_operating_profit(prices::Array{Float64, 2},
output::Array{Float64, 2},
realized_hour_weight::Vector{Float64})
- profit = sum(realized_hour_weight[i] *
- ((prices[1, i] - marginal_cost) * output[1, i])
+
+ replace!(prices, NaN => 0.0)
+ replace!(output, NaN => 0.0)
+
+ profit = sum((max(0.0, (prices[1, i] - marginal_cost) * output[1, i]))
for i in 1:size(output, 2))
return profit
@@ -108,6 +116,7 @@ function calculate_realized_profit(project::Project,
market_prices::MarketPrices,
capacity_factors::Dict{String, Array{Float64, 2}},
reserve_perc::Dict{String, Dict{String, Array{Float64, 2}}},
+ inertia_perc::Dict{String, Array{Float64, 2}},
capacity_accepted_bids::Dict{String, Float64},
rec_accepted_bids::Dict{String, Float64},
realized_hour_weight::Vector{Float64},
@@ -129,6 +138,7 @@ function calculate_realized_profit(project::Project,
market_prices::MarketPrices,
capacity_factors::Dict{String, Array{Float64, 2}},
reserve_perc::Dict{String, Dict{String, Array{Float64, 2}}},
+ inertia_perc::Dict{String, Array{Float64, 2}},
capacity_accepted_bids::Dict{String, Float64},
rec_accepted_bids::Dict{String, Float64},
realized_hour_weight::Vector{Float64},
@@ -160,6 +170,9 @@ function calculate_realized_profit(project::Project,
realized_hour_weight,
rt_resolution)
+ for product in get_products(project)
+ set_total_emission!(product, carbon_emissions)
+ end
return profit, update_year
else
return nothing, update_year
@@ -174,6 +187,7 @@ function calculate_realized_profit(project::Project,
market_prices::MarketPrices,
capacity_factors::Dict{String, Array{Float64, 2}},
reserve_perc::Dict{String, Dict{String, Array{Float64, 2}}},
+ inertia_perc::Dict{String, Array{Float64, 2}},
capacity_accepted_bids::Dict{String, Float64},
rec_accepted_bids::Dict{String, Float64},
realized_hour_weight::Vector{Float64},
@@ -217,6 +231,7 @@ function calculate_realized_profit(project::Project,
market_prices::MarketPrices,
capacity_factors::Dict{String, Array{Float64, 2}},
reserve_perc::Dict{String, Dict{String, Array{Float64, 2}}},
+ inertia_perc::Dict{String, Array{Float64, 2}},
capacity_accepted_bids::Dict{String, Float64},
rec_accepted_bids::Dict{String, Float64},
realized_hour_weight::Vector{Float64},
@@ -251,6 +266,7 @@ function calculate_realized_profit(project::Project,
market_prices::MarketPrices,
capacity_factors::Dict{String, Array{Float64, 2}},
reserve_perc::Dict{String, Dict{String, Array{Float64, 2}}},
+ inertia_perc::Dict{String, Array{Float64, 2}},
capacity_accepted_bids::Dict{String, Float64},
rec_accepted_bids::Dict{String, Float64},
realized_hour_weight::Vector{Float64},
@@ -272,3 +288,47 @@ function calculate_realized_profit(project::Project,
return nothing, update_year
end
end
+
+"""
+This function calculates the realized REC market profits.
+"""
+function calculate_realized_profit(project::Project,
+ product::Inertia,
+ market_prices::MarketPrices,
+ capacity_factors::Dict{String, Array{Float64, 2}},
+ reserve_perc::Dict{String, Dict{String, Array{Float64, 2}}},
+ inertia_perc::Dict{String, Array{Float64, 2}},
+ capacity_accepted_bids::Dict{String, Float64},
+ rec_accepted_bids::Dict{String, Float64},
+ realized_hour_weight::Vector{Float64},
+ iteration_year::Int64,
+ capacity_forward_years::Int64,
+ carbon_tax::Float64,
+ da_resolution::Int64,
+ rt_resolution::Int64,
+ rt_products::Vector{String})
+
+ project_name = get_name(project)
+ size = get_maxcap(project)
+
+ if get_name(product) in rt_products
+ scale = rt_resolution / 60
+ else
+ scale = da_resolution / 60
+ end
+
+ update_year = iteration_year
+ if in(project_name, keys(inertia_perc))
+ output = size * inertia_perc[project_name]
+
+ profit = calculate_realized_operating_profit(get_prices(market_prices, product)["realized"],
+ get_marginal_cost(product) * scale,
+ output,
+ realized_hour_weight)
+
+
+ return profit, update_year
+ else
+ return nothing, update_year
+ end
+end
diff --git a/src/markets_simulation/actual_capacity_mkt_clearing.jl b/src/markets_simulation/actual_capacity_mkt_clearing.jl
index 5894c14..b4221cf 100644
--- a/src/markets_simulation/actual_capacity_mkt_clearing.jl
+++ b/src/markets_simulation/actual_capacity_mkt_clearing.jl
@@ -1,150 +1,155 @@
-
-#Capacity Market Clearing Module
-
-"""
-This function does nothing if the product is not Capacity
-"""
-function update_capacity_supply_curve!(capacity_supply_curve::Vector{Vector{Union{String, Float64}}},
- product::T,
- project::P) where {T <: Product, P <: Project{<: BuildPhase}}
-
- return capacity_supply_curve
-end
-
-"""
-This function pushes project bids in the capacity supply curve
-"""
-function update_capacity_supply_curve!(capacity_supply_curve::Vector{Vector{Union{String, Float64}}},
- product::Capacity,
- project::P) where P <: Project{<: BuildPhase}
-
- # Each Element of the Supply Curve:
- # [1] - Project Name
- # [2] - Project Size
- # [3] - Project Capacity Bid
- # [4] - Project Derating Factor
-
- push!(capacity_supply_curve, [get_name(project),
- get_maxcap(project),
- get_project_capacity_market_bid(project),
- get_project_derating(project)])
- return capacity_supply_curve
-end
-
-"""
-This function creates the capacity market demand curve for actual capacity market clearing.
-"""
-function create_capacity_demand_curve(input_file::String,
- system_peak_load::Float64,
- capacity_mkt_bool::Bool)
-
- # Gather parameter data
- capacity_demand_params = read_data(input_file)[1, :]
- fpr = capacity_demand_params["Forecast Pool Req"] #Forecast Pool Req
- rel_req = system_peak_load * fpr * capacity_mkt_bool # Reliability Requirement
- irm = capacity_demand_params["IRM"] # Installed Reserve Margin
- irm_perc_points = parse.(Float64, split(capacity_demand_params["IRM perc points"], ";")) #Installed Reserve Margin percentage points
-
- net_CONE = capacity_demand_params["Net CONE per day"] * 365 * capacity_mkt_bool # Net CONE
- net_CONE_perc_points = parse.(Float64, split(capacity_demand_params["Net CONE perc points"], ";")) #Net CONE percentage points
-
- @assert length(net_CONE_perc_points) == length(irm_perc_points)
-
- gross_CONE_value = capacity_demand_params["Gross CONE per day"] * 365 * capacity_mkt_bool # Gross CONE
- gross_CONE_points = zeros(length(net_CONE_perc_points))
- gross_CONE_points[1] = gross_CONE_value
-
- max_clear = capacity_demand_params["Max Clear"]
-
- # Construct demand curve
- demand_curve_break_points = zeros(length(net_CONE_perc_points) + 2)
- demand_curve_price_points = zeros(length(net_CONE_perc_points) + 2)
-
- demand_curve_break_points[1] = 0.0
- demand_curve_price_points[1] = max(net_CONE * net_CONE_perc_points[1], gross_CONE_points[1])
-
- demand_curve_break_points[end] = rel_req * max_clear
- demand_curve_price_points[end] = 0.0
-
- # Demand curve points based on PJM capacity market
- for i in 1:(length(net_CONE_perc_points))
- demand_curve_break_points[i + 1] = rel_req * ( 1 + irm + irm_perc_points[i]) / ( 1 + irm)
- demand_curve_price_points[i + 1] = max(net_CONE * net_CONE_perc_points[i], gross_CONE_points[i])
- end
-
- capacity_demand_curve = CapacityMarket(demand_curve_break_points, demand_curve_price_points)
- return capacity_demand_curve
-end
-
-"""
-This function models actual capacity market clearing.
-"""
-function capacity_market_clearing(demand_curve::CapacityMarket,
- supply_curve::Vector{Vector{Union{String, Float64}}},
- solver::JuMP.MOI.OptimizerWithAttributes)
-
-
- # Each Element of the Supply Curve:
- # [1] - Project Name
- # [2] - Project Size
- # [3] - Project Capacity Bid
- # [4] - Project Derating Factor
-
-
- demand_segmentsize, demand_segmentgrad, demand_pricepoints = make_capacity_demand(demand_curve)
-
- #Number of segments of capacity supply and demand curves
- n_demand_seg = length(demand_segmentsize);
- n_supply_seg = length(supply_curve);
-
- #----------Capacity Market Clearing Problem---------------------------------------------------------------------------------
- cap_mkt = JuMP.Model(solver);
-
- #Define the variables
- JuMP.@variables(cap_mkt, begin
- Q_supply[s=1:n_supply_seg] >= 0 # Quantity of cleared capacity supply offers
- Q_demand[d=1:n_demand_seg] >= 0 # Quantity of cleared capacity demand from the demand curve
- end)
-
- #Functions----------------------------------------------------------------------------------------
-
- #Cost of procuring capacity supply for each segment
- supply_cost(Q_seg, s) = supply_curve[s][3] * Q_seg;
-
- #Welfare from meeting the capacity resource requirement for each segment
- demand_welfare(Q_seg, d) = demand_pricepoints[d] * Q_seg + 0.5 * demand_segmentgrad[d] * (Q_seg^2);
-
- #Expressions---------------------------------------------------------------------------------------
-
- #Totoal Cost of procuring capacity supply
- JuMP.@expression(cap_mkt, total_supply_cost, sum(supply_cost(Q_supply[s], s) for s = 1:n_supply_seg))
-
- #Total Welfare from meeting the capacity resource requirement
- JuMP.@expression(cap_mkt, total_dem_welfare, sum(demand_welfare(Q_demand[d], d) for d = 1:n_demand_seg))
-
- #Constraints--------------------------------------------------------------------------------------
-
- #Cleared capacity supply limit for each segment
- JuMP.@constraint(cap_mkt, [s=1:n_supply_seg], Q_supply[s] <= supply_curve[s][2])
-
- #Cleared demand limit for each segment
- JuMP.@constraint(cap_mkt, [d=1:n_demand_seg], Q_demand[d] <= demand_segmentsize[d])
-
- #Total cleared capacity supply should meet the total cleared demand
- JuMP.@constraint(cap_mkt, mkt_clear, sum(Q_supply[s] * supply_curve[s][4] for s in 1:n_supply_seg) == sum(Q_demand))
-
- #Define Objective Function - Social Welfare Maxmization
- JuMP.@objective(cap_mkt, Max, total_dem_welfare - total_supply_cost)
-
- println("Actual Capacity Market Clearing:")
- JuMP.optimize!(cap_mkt)
- println(JuMP.termination_status(cap_mkt))
- println(JuMP.objective_value(cap_mkt))
-
- #Capacity Market Clearing Price is the shadow variable of the capacity balance constraint
- cap_price = AxisArrays.AxisArray(reshape([JuMP.dual(mkt_clear)], 1,), [1])
- cap_accepted_bid = Dict(supply_curve[s][1] => value.(Q_supply[s]) / supply_curve[s][2] for s in 1:n_supply_seg)
-#------------------------------------------------------------------------------------------------
- return cap_price, cap_accepted_bid
-
-end
+
+#Capacity Market Clearing Module
+
+"""
+This function does nothing if the product is not Capacity
+"""
+function update_capacity_supply_curve!(capacity_supply_curve::Vector{Vector{Union{String, Float64}}},
+ product::T,
+ project::P) where {T <: Product, P <: Project{<: BuildPhase}}
+
+ return capacity_supply_curve
+end
+
+"""
+This function pushes project bids in the capacity supply curve
+"""
+function update_capacity_supply_curve!(capacity_supply_curve::Vector{Vector{Union{String, Float64}}},
+ product::Capacity,
+ project::P) where P <: Project{<: BuildPhase}
+
+ # Each Element of the Supply Curve:
+ # [1] - Project Name
+ # [2] - Project Size
+ # [3] - Project Capacity Bid
+ # [4] - Project Derating Factor
+
+ push!(capacity_supply_curve, [get_name(project),
+ get_maxcap(project),
+ get_project_capacity_market_bid(project),
+ get_project_derating(project)])
+ return capacity_supply_curve
+end
+
+"""
+This function creates the capacity market demand curve for actual capacity market clearing.
+"""
+function create_capacity_demand_curve(input_file::String,
+ system_peak_load::Float64,
+ irm_scalar::Float64,
+ delta_irm::Float64,
+ capacity_mkt_bool::Bool)
+
+ # Gather parameter data
+ capacity_demand_params = read_data(input_file)[1, :]
+ eford = capacity_demand_params["EFORd"] # Equivalent demand forced outage rate
+ base_irm = capacity_demand_params["IRM"] # Installed Reserve Margin
+ adjusted_irm = (base_irm + delta_irm) * irm_scalar
+ fpr = (1 + adjusted_irm) * (1 - eford) #Forecast Pool Req
+ rel_req = system_peak_load * fpr * capacity_mkt_bool # Reliability Requirement
+ irm_perc_points = parse.(Float64, split(capacity_demand_params["IRM perc points"], ";")) #Installed Reserve Margin percentage points
+ net_CONE = capacity_demand_params["Net CONE per day"] * 365 * capacity_mkt_bool # Net CONE
+ net_CONE_perc_points = parse.(Float64, split(capacity_demand_params["Net CONE perc points"], ";")) #Net CONE percentage points
+
+ @assert length(net_CONE_perc_points) == length(irm_perc_points)
+
+ gross_CONE_value = capacity_demand_params["Gross CONE per day"] * 365 * capacity_mkt_bool # Gross CONE
+ gross_CONE_points = zeros(length(net_CONE_perc_points))
+ gross_CONE_points[1] = gross_CONE_value
+
+ max_clear = capacity_demand_params["Max Clear"]
+
+ # Construct demand curve
+ demand_curve_break_points = zeros(length(net_CONE_perc_points) + 2)
+ demand_curve_price_points = zeros(length(net_CONE_perc_points) + 2)
+
+ demand_curve_break_points[1] = 0.0
+ demand_curve_price_points[1] = max(net_CONE * net_CONE_perc_points[1], gross_CONE_points[1])
+
+ demand_curve_break_points[end] = rel_req * max_clear
+ demand_curve_price_points[end] = 0.0
+
+ # Demand curve points based on PJM capacity market
+ for i in 1:(length(net_CONE_perc_points))
+ demand_curve_break_points[i + 1] = rel_req * ( 1 + adjusted_irm + irm_perc_points[i]) / ( 1 + adjusted_irm)
+ demand_curve_price_points[i + 1] = max(net_CONE * net_CONE_perc_points[i], gross_CONE_points[i])
+ end
+
+ capacity_demand_curve = CapacityMarket(demand_curve_break_points, demand_curve_price_points)
+
+ return capacity_demand_curve
+end
+
+"""
+This function models actual capacity market clearing.
+"""
+function capacity_market_clearing(demand_curve::CapacityMarket,
+ supply_curve::Vector{Vector{Union{String, Float64}}},
+ solver::JuMP.MOI.OptimizerWithAttributes)
+
+
+ # Each Element of the Supply Curve:
+ # [1] - Project Name
+ # [2] - Project Size
+ # [3] - Project Capacity Bid
+ # [4] - Project Derating Factor
+
+
+ demand_segmentsize, demand_segmentgrad, demand_pricepoints = make_capacity_demand(demand_curve)
+
+ #Number of segments of capacity supply and demand curves
+ n_demand_seg = length(demand_segmentsize);
+ n_supply_seg = length(supply_curve);
+
+ #----------Capacity Market Clearing Problem---------------------------------------------------------------------------------
+ cap_mkt = JuMP.Model(solver);
+
+ #Define the variables
+ JuMP.@variables(cap_mkt, begin
+ Q_supply[s=1:n_supply_seg] >= 0 # Quantity of cleared capacity supply offers
+ Q_demand[d=1:n_demand_seg] >= 0 # Quantity of cleared capacity demand from the demand curve
+ end)
+
+ #Functions----------------------------------------------------------------------------------------
+
+ #Cost of procuring capacity supply for each segment
+ supply_cost(Q_seg, s) = supply_curve[s][3] * Q_seg;
+
+ #Welfare from meeting the capacity resource requirement for each segment
+ demand_welfare(Q_seg, d) = demand_pricepoints[d] * Q_seg + 0.5 * demand_segmentgrad[d] * (Q_seg^2);
+
+ #Expressions---------------------------------------------------------------------------------------
+
+ #Totoal Cost of procuring capacity supply
+ JuMP.@expression(cap_mkt, total_supply_cost, sum(supply_cost(Q_supply[s], s) for s = 1:n_supply_seg))
+
+ #Total Welfare from meeting the capacity resource requirement
+ JuMP.@expression(cap_mkt, total_dem_welfare, sum(demand_welfare(Q_demand[d], d) for d = 1:n_demand_seg))
+
+ #Constraints--------------------------------------------------------------------------------------
+
+ #Cleared capacity supply limit for each segment
+ JuMP.@constraint(cap_mkt, [s=1:n_supply_seg], Q_supply[s] <= supply_curve[s][2])
+
+ #Cleared demand limit for each segment
+ JuMP.@constraint(cap_mkt, [d=1:n_demand_seg], Q_demand[d] <= demand_segmentsize[d])
+
+ #Total cleared capacity supply should meet the total cleared demand
+ JuMP.@constraint(cap_mkt, mkt_clear, sum(Q_supply[s] * supply_curve[s][4] for s in 1:n_supply_seg) == sum(Q_demand))
+
+ #Define Objective Function - Social Welfare Maxmization
+ JuMP.@objective(cap_mkt, Max, total_dem_welfare - total_supply_cost)
+
+ println("Actual Capacity Market Clearing:")
+ JuMP.optimize!(cap_mkt)
+ println(JuMP.termination_status(cap_mkt))
+ println(JuMP.objective_value(cap_mkt))
+
+ #Capacity Market Clearing Price is the shadow variable of the capacity balance constraint
+ cap_price = AxisArrays.AxisArray(reshape([JuMP.dual(mkt_clear)], 1,), [1])
+ cap_accepted_bid = Dict(supply_curve[s][1] => value.(Q_supply[s]) / supply_curve[s][2] for s in 1:n_supply_seg)
+ println(cap_price)
+#------------------------------------------------------------------------------------------------
+ return cap_price, cap_accepted_bid
+
+end
diff --git a/src/markets_simulation/actual_energy_mkt_clearing.jl b/src/markets_simulation/actual_energy_mkt_clearing.jl
index 2fee04b..4db9028 100644
--- a/src/markets_simulation/actual_energy_mkt_clearing.jl
+++ b/src/markets_simulation/actual_energy_mkt_clearing.jl
@@ -6,11 +6,16 @@ function energy_mkt_clearing(sys_UC::Nothing,
sys_ED::Nothing,
sys_local_ED::MarketClearingProblem,
load_growth::AxisArrays.AxisArray{Float64, 1},
+ reserve_penalty::String,
+ rec_requirement::Float64,
simulation_dir::String,
zones::Vector{String},
num_days::Int64,
- solver::JuMP.MOI.OptimizerWithAttributes,
- iteration_year::Int64)
+ iteration_year::Int64,
+ da_resolution::Int64,
+ rt_resolution::Int64,
+ case_name::String,
+ solver::JuMP.MOI.OptimizerWithAttributes)
energy_price,
reserve_up_price,
reserve_down_price,
@@ -32,18 +37,30 @@ function energy_mkt_clearing(sys_UC::PSY.System,
sys_local_ED::Union{Nothing, MarketClearingProblem},
simulation_dir::String,
load_growth::AxisArrays.AxisArray{Float64, 1},
+ reserve_penalty::String,
+ rec_requirement::Float64,
zones::Vector{String},
num_days::Int64,
- solver::JuMP.MOI.OptimizerWithAttributes,
- iteration_year::Int64)
+ iteration_year::Int64,
+ da_resolution::Int64,
+ rt_resolution::Int64,
+ case_name::String,
+ solver::JuMP.MOI.OptimizerWithAttributes)
- #update_PSY_timeseries!(sys_UC, load_growth, simulation_dir)
- #update_PSY_timeseries!(sys_ED, load_growth, simulation_dir)
+ update_PSY_timeseries!(sys_UC, load_growth, rec_requirement, simulation_dir, "UC", iteration_year, da_resolution, rt_resolution)
+ update_PSY_timeseries!(sys_ED, load_growth, rec_requirement, simulation_dir, "ED", iteration_year, da_resolution, rt_resolution)
energy_price,
reserve_price,
+ inertia_price,
capacity_factors,
- reserve_perc = create_simulation(sys_UC, sys_ED, simulation_dir, zones, num_days, solver, iteration_year)
+ reserve_perc,
+ inertia_perc,
+ start_up_costs,
+ shut_down_costs,
+ energy_voll,
+ reserve_voll,
+ inertia_voll = create_simulation(sys_UC, sys_ED, simulation_dir, reserve_penalty, zones, num_days, da_resolution, rt_resolution, case_name, solver)
- return energy_price, reserve_price, capacity_factors, reserve_perc;
+ return energy_price, reserve_price, inertia_price, capacity_factors, reserve_perc, inertia_perc, start_up_costs, shut_down_costs, energy_voll, reserve_voll, inertia_voll;
end
diff --git a/src/markets_simulation/actual_market_simulation.jl b/src/markets_simulation/actual_market_simulation.jl
index c6b83a6..750be16 100644
--- a/src/markets_simulation/actual_market_simulation.jl
+++ b/src/markets_simulation/actual_market_simulation.jl
@@ -6,6 +6,9 @@ function create_realized_marketdata(simulation::AgentSimulation,
sys_UC::Union{Nothing, PSY.System},
sys_ED::Union{Nothing, PSY.System},
market_names::Vector{Symbol},
+ rps_target::String,
+ reserve_penalty::String,
+ ordc_curved::Bool,
existing_projects::Vector{<: Project{<: BuildPhase}},
capacity_market_projects::Vector{<: Project{<: BuildPhase}},
capacity_forward_years::Int64,
@@ -47,10 +50,53 @@ function create_realized_marketdata(simulation::AgentSimulation,
num_hours = size(zonal_load)[2]
num_days = Int(num_hours/24)
+ rec_perc_requirement = get_rec_requirement(simulation)[iteration_year]
+
energy_price,
reserve_price,
+ inertia_price,
capacity_factors,
- reserve_perc = energy_mkt_clearing(sys_UC, sys_ED, system, simulation_dir, load_growth, zones, num_days, solver, iteration_year)
+ reserve_perc,
+ inertia_perc,
+ start_up_costs,
+ shut_down_costs,
+ energy_voll,
+ reserve_voll,
+ inertia_voll = energy_mkt_clearing(sys_UC, sys_ED, system, simulation_dir, load_growth, reserve_penalty, rec_perc_requirement, zones, num_days, iteration_year, get_da_resolution(get_case(simulation)), get_rt_resolution(get_case(simulation)), get_name(get_case(simulation)), solver)
+
+ println("Clean energy requirement for this year is $(get_rec_requirement(simulation)[iteration_year] * 100) percent")
+ total_production = 0.0
+ total_cec_production = 0.0
+ day = 0
+ get_rt_resolution(get_case(simulation))
+ for time in 1:Int(24*60/get_rt_resolution(get_case(simulation))):(Int(24*60/get_rt_resolution(get_case(simulation))) * 360)
+ day += 1
+ daily_total_production = 0.0
+ daily_cec_production = 0.0
+ for gen in get_all_techs(sys_ED)
+ name = PSY.get_name(gen)
+ if !(occursin("BA", string(PSY.get_prime_mover(gen)))) #!(occursin("BA", name))
+ energy_production = sum(capacity_factors[name][time:time + Int(24*60/get_rt_resolution(get_case(simulation)))-1]) * get_device_size(gen)
+ total_production += energy_production
+ daily_total_production += energy_production
+ if occursin("WT", string(PSY.get_prime_mover(gen))) || occursin("PVe", string(PSY.get_prime_mover(gen))) || occursin("HY", string(PSY.get_prime_mover(gen))) #occursin("WT", name) || occursin("WIND", name) || occursin("PV", name) || occursin("HY", name) || occursin("NU", name) || occursin("RE", name)
+ total_cec_production += energy_production
+ daily_cec_production += energy_production
+ end
+ if occursin("ST", string(PSY.get_prime_mover(gen)))
+ if occursin("NUCLEAR", string(PSY.get_fuel(gen)))
+ total_cec_production += energy_production
+ daily_cec_production += energy_production
+ end
+ end
+ end
+ end
+ #println("Clean energy contribution for day $(day) is $(round(daily_cec_production * 100.0 / daily_total_production, digits = 2)) percent")
+ end
+
+ println("Total Annual clean energy contribution is $(round(total_cec_production * 100.0 / total_production, digits = 2)) percent")
+
+ cet_achieved_ratio = round(total_cec_production / total_production, digits = 2) / get_rec_requirement(simulation)[iteration_year]
# Replace energy_mkt_clearing(nothing, nothing, system, load_growth, zones, num_days, solver) with
# energy_mkt_clearing(sys_UC, sys_ED, system, load_growth, zones, num_days, solver) to run SIIP production cost model
@@ -62,6 +108,9 @@ function create_realized_marketdata(simulation::AgentSimulation,
set_reserve_price!(market_prices, "realized", reserve_price)
+ if in(:Inertia, market_names)
+ set_inertia_price!(market_prices, "realized", inertia_price)
+ end
######## Capacity market clearing #####################################################################
@@ -78,6 +127,9 @@ function create_realized_marketdata(simulation::AgentSimulation,
capacity_supply_curve = Vector{Union{String, Float64}}[]
+ delta_irm = get_delta_irm(get_resource_adequacy(simulation), iteration_year)
+ irm_scalar = get_irm_scalar(get_case(simulation))
+
for project in capacity_market_projects
for product in get_products(project)
capacity_supply_curve = update_capacity_supply_curve!(capacity_supply_curve, product, project)
@@ -89,7 +141,7 @@ function create_realized_marketdata(simulation::AgentSimulation,
if in(:Capacity, market_names) && iteration_year + capacity_forward_years - 1 <= simulation_years
system_peak_load = (1 + average_load_growth) ^ (capacity_forward_years) * peak_load
- capacity_demand_curve = create_capacity_demand_curve(capacity_mkt_param_file, system_peak_load, capacity_market_bool)
+ capacity_demand_curve = create_capacity_demand_curve(capacity_mkt_param_file, system_peak_load, irm_scalar, delta_irm, capacity_market_bool)
sort!(capacity_supply_curve, by = x -> x[3]) # Sort capacity supply curve by capacity bid
@@ -105,39 +157,62 @@ function create_realized_marketdata(simulation::AgentSimulation,
rec_market_bool = true
end
- REC_mkt_params = read_data(joinpath(simulation_dir, "markets_data", "REC.csv"))
+ REC_mkt_params = read_data(joinpath(simulation_dir, "markets_data", "REC_$(rps_target)_RPS.csv"))
pricecap_rec = REC_mkt_params.price_cap[1]
rec_req = REC_mkt_params.rec_req[1] * rec_market_bool
rec_annual_increment = REC_mkt_params.annual_increment[1] * rec_market_bool
+ rec_non_binding_years = REC_mkt_params.non_binding_years[1] * rec_market_bool
+
+ rec_price = AxisArrays.AxisArray(reshape([pricecap_rec], 1,), [1])
+ rec_accepted_bids = Dict{String, Float64}()
+
+ total_demand = 0.0
+ for z in zones
+ for t in 1:num_hours
+ total_demand += zonal_load[z, t] * (1 + energy_annual_increment[z]) * hour_weight[t]
+ end
+ end
+
+ total_clean_production = 0.0
rec_supply_curve = Vector{Union{String, Float64}}[]
+ total_storage_consumption = 0.0
for project in existing_projects
# Populate REC market supply curves
+ clean_production = 0.0
for product in get_products(project)
rec_supply_curve = update_rec_supply_curve!(rec_supply_curve, product, project)
+ clean_production += find_clean_energy_production(product, project)
+ total_storage_consumption += find_storage_energy_consumption(product, project)
end
+ total_clean_production += clean_production
end
- rec_price = AxisArrays.AxisArray(reshape([pricecap_rec], 1,), [1])
- rec_accepted_bids = Dict{String, Float64}()
+ clean_energy_percentage = min(1.0, (total_clean_production / total_demand))
+ #println(clean_energy_percentage)
if in(:REC, market_names)
if length(rec_supply_curve) >= 1
- rec_energy_requirment = 0.0
-
- for z in zones
- for t in 1:num_hours
- rec_energy_requirment += zonal_load[z, t] * (1 + energy_annual_increment[z]) * hour_weight[t]
- end
- end
-
- rec_energy_requirment = rec_energy_requirment * min(rec_req + (rec_annual_increment * iteration_year), 1)
+ rec_energy_requirment = total_demand * min(rec_req + (rec_annual_increment * iteration_year), 1)
sort!(rec_supply_curve, by = x -> x[3]) # Sort REC supply curve by REC bid
-
- rec_price, rec_accepted_bids = rec_market_clearing(rec_energy_requirment, pricecap_rec, rec_supply_curve, solver)
+ if iteration_year <= rec_non_binding_years
+
+ rec_energy_requirment = min(total_clean_production, rec_energy_requirment)
+ #println(rec_energy_requirment)
+ rec_price, rec_accepted_bids = rec_market_clearing_non_binding(rec_energy_requirment, pricecap_rec, rec_supply_curve, solver)
+ else
+ total = 0
+ for i in rec_supply_curve
+ total += i[2]
+ end
+ #println(total)
+ rec_energy_requirment = min(total_clean_production, rec_energy_requirment)
+ #println(rec_energy_requirment)
+ rec_price, rec_accepted_bids = rec_market_clearing_binding(rec_energy_requirment, pricecap_rec, rec_supply_curve, solver)
+ end
set_rec_price!(market_prices, "realized", rec_price)
end
@@ -152,10 +227,17 @@ function create_realized_marketdata(simulation::AgentSimulation,
"energy_price", energy_price,
"reserve_price", reserve_price,
"rec_price", rec_price,
+ "inertia_price", inertia_price,
"capacity_factors", capacity_factors,
"reserve_perc", reserve_perc,
"capacity_accepted_bids", capacity_accepted_bids,
- "rec_accepted_bids", rec_accepted_bids
+ "rec_accepted_bids", rec_accepted_bids,
+ "inertia_perc", inertia_perc,
+ "start_up_costs", start_up_costs,
+ "shut_down_costs", shut_down_costs,
+ "energy_voll", energy_voll,
+ "reserve_voll", reserve_voll,
+ "inertia_voll", inertia_voll
)
################ Update realized load growth and peak load #########################################
@@ -167,13 +249,13 @@ function create_realized_marketdata(simulation::AgentSimulation,
for (idx, z) in enumerate(zones)
load_data[:, Symbol(idx)] = load_data[:, Symbol(idx)] * (1 + load_growth[idx])
- load_data[:, "Year"] = fill(load_data[1, "Year"] + 1, DataFrames.nrow(load_data))
+ #load_data[:, "Year"] = fill(load_data[1, "Year"] + 1, DataFrames.nrow(load_data))
rep_load_data[:, Symbol(idx)] = rep_load_data[:, Symbol(idx)] * (1 + load_growth[idx])
- rep_load_data[:, "Year"] = fill(rep_load_data[1, "Year"] + 1, DataFrames.nrow(rep_load_data))
+ #rep_load_data[:, "Year"] = fill(rep_load_data[1, "Year"] + 1, DataFrames.nrow(rep_load_data))
load_n_vg_data[:, Symbol("load_zone_$(idx)")] = load_n_vg_data[:, Symbol("load_zone_$(idx)")] * (1 + load_growth[idx])
- load_n_vg_data[:, "Year"] = fill(load_n_vg_data[1, "Year"] + 1, DataFrames.nrow(load_n_vg_data))
+ #load_n_vg_data[:, "Year"] = fill(load_n_vg_data[1, "Year"] + 1, DataFrames.nrow(load_n_vg_data))
end
# Write realized load and reserve demand data in a CSV file
@@ -190,20 +272,21 @@ function create_realized_marketdata(simulation::AgentSimulation,
for product in non_ordc_products
reserve_timeseries_data[product][:, product] = reserve_timeseries_data[product][:, product] * (1 + average_load_growth)
- reserve_timeseries_data[product][:, "Year"] = fill(reserve_timeseries_data[product][1, "Year"] + 1, DataFrames.nrow(reserve_timeseries_data[product]))
+ #reserve_timeseries_data[product][:, "Year"] = fill(reserve_timeseries_data[product][1, "Year"] + 1, DataFrames.nrow(reserve_timeseries_data[product]))
rep_reserve_timeseries_data[product][:, product] = rep_reserve_timeseries_data[product][:, product] * (1 + average_load_growth)
- rep_reserve_timeseries_data[product][:, "Year"] = fill(rep_reserve_timeseries_data[product][1, "Year"] + 1, DataFrames.nrow(rep_reserve_timeseries_data[product]))
+ #rep_reserve_timeseries_data[product][:, "Year"] = fill(rep_reserve_timeseries_data[product][1, "Year"] + 1, DataFrames.nrow(rep_reserve_timeseries_data[product]))
CSV.write(joinpath(simulation_dir, "timeseries_data_files", "Reserves", "$(product)_$(iteration_year).csv"), reserve_timeseries_data[product])
CSV.write(joinpath(simulation_dir, "timeseries_data_files", "Reserves", "rep_$(product)_$(iteration_year).csv"), rep_reserve_timeseries_data[product])
end
- #construct_ordc(simulation_dir, get_investors(simulation), iteration_year, get_rep_days(simulation))
+ ordc_unavailability_method = get_ordc_unavailability_method(get_case(simulation))
+
+ construct_ordc(deepcopy(sys_UC), simulation_dir, get_investors(simulation), iteration_year, get_rep_days(simulation), ordc_curved, ordc_unavailability_method, reserve_penalty)
peak_load_new = (1 + average_load_growth) * peak_load
set_peak_load!(simulation, peak_load_new)
- return market_prices, capacity_factors, reserve_perc, capacity_accepted_bids, rec_accepted_bids
+ return market_prices, capacity_factors, reserve_perc, inertia_perc, capacity_accepted_bids, rec_accepted_bids, clean_energy_percentage, cet_achieved_ratio
end
-
diff --git a/src/markets_simulation/actual_rec_mkt_clearing.jl b/src/markets_simulation/actual_rec_mkt_clearing.jl
index cdc7be0..9866d77 100644
--- a/src/markets_simulation/actual_rec_mkt_clearing.jl
+++ b/src/markets_simulation/actual_rec_mkt_clearing.jl
@@ -1,88 +1,256 @@
-#REC Market Clearing Module
-
-"""
-This function does nothing if the product is not REC
-"""
-function update_rec_supply_curve!(rec_supply_curve::Vector{Vector{Union{String, Float64}}},
- product::T,
- project::P) where {T <: Product, P <: Project{<: BuildPhase}}
-
- return rec_supply_curve
-end
-
-# This function pushes project bids in the REC supply curve
-function update_rec_supply_curve!(rec_supply_curve::Vector{Vector{Union{String, Float64}}},
- product::REC,
- project::P) where P <: Project{<: BuildPhase}
-
- #Each Element of the REC supply Curve:
- #[1] - Project Name (Symbol)
- #[2] - Project REC output (Float64)
- #[3] - Project REC Bid (Float64)
-
-
- push!(rec_supply_curve, [get_name(project),
- get_project_rec_output(project),
- get_project_rec_market_bid(project)])
-
- return rec_supply_curve
-end
-
-"""
-This function models the actual REC market clearing.
-"""
-function rec_market_clearing(rec_requirement::Float64,
- ACP::Float64,
- supply_curve::Vector{Vector{Union{String, Float64}}},
- solver::JuMP.MOI.OptimizerWithAttributes)
-
- #Each Element of the REC supply Curve:
- #[1] - Project Name (Symbol)
- #[2] - Project REC output (Float64)
- #[3] - Project REC Bid (Float64)
-
- n_supply_seg = length(supply_curve);
-
- #----------REC Market Clearing Problem---------------------------------------------------------------------------------
- rec_mkt = JuMP.Model(solver);
-
- #Define the variables
- JuMP.@variables(rec_mkt, begin
- Q_supply[s=1:n_supply_seg] >= 0 # Quantity of cleared REC supply offers
- v_REC >= 0 # RPS compliance violation
- end)
-
- #Functions----------------------------------------------------------------------------------------
-
- #Cost of procuring REC supply for each segment
- supply_cost(Q_seg, s) = supply_curve[s][3] * Q_seg;
-
- #Expressions---------------------------------------------------------------------------------------
-
- #Totoal Cost of procuring REC supply
- JuMP.@expression(rec_mkt, total_supply_cost, sum(supply_cost(Q_supply[s], s) for s = 1:n_supply_seg))
-
- #Constraints--------------------------------------------------------------------------------------
-
- #Cleared REC supply limit for each segment
- JuMP.@constraint(rec_mkt, [s=1:n_supply_seg], Q_supply[s] <= supply_curve[s][2])
-
- #Total cleared REC supply should be greater than or equal to the RPS compliance requirement
- JuMP.@constraint(rec_mkt, mkt_clear, sum(Q_supply[s] for s in 1:n_supply_seg) + v_REC >= rec_requirement)
-
- #Define Objective Function - Social Welfare Maxmization
- JuMP.@objective(rec_mkt, Min, total_supply_cost + v_REC * ACP)
-
- println("Actual REC Market Clearing:")
- JuMP.optimize!(rec_mkt)
-
- println(JuMP.termination_status(rec_mkt))
- println(JuMP.objective_value(rec_mkt))
-
- #REC Market Clearing Price is the shadow variable of the REC balance constraint
- rec_price = AxisArrays.AxisArray(reshape([JuMP.dual(mkt_clear)], 1,), [1])
- rec_accepted_bid = Dict(supply_curve[s][1] => value.(Q_supply[s]) for s in 1:n_supply_seg)
- #------------------------------------------------------------------------------------------------
- return rec_price, rec_accepted_bid
-
-end
+#REC Market Clearing Module
+
+"""
+This function does nothing if the product is not REC
+"""
+function update_rec_supply_curve!(rec_supply_curve::Vector{Vector{Union{String, Float64}}},
+ product::T,
+ project::P) where {T <: Product, P <: Project{<: BuildPhase}}
+
+ return rec_supply_curve
+end
+
+# This function pushes project bids in the REC supply curve
+function update_rec_supply_curve!(rec_supply_curve::Vector{Vector{Union{String, Float64}}},
+ product::REC,
+ project::P) where P <: Project{<: BuildPhase}
+
+ #Each Element of the REC supply Curve:
+ #[1] - Project Name (Symbol)
+ #[2] - Project REC output (Float64)
+ #[3] - Project REC Bid (Float64)
+
+
+ push!(rec_supply_curve, [get_name(project),
+ get_project_expected_rec_output(project),
+ get_project_rec_market_bid(project)])
+
+ return rec_supply_curve
+end
+
+"""
+This function does nothing if the product is not REC
+"""
+function find_clean_energy_production(product::T,
+ project::P) where {T <: Product, P <: Project{<: BuildPhase}}
+
+ return 0.0
+end
+
+# This function pushes project bids in the REC supply curve
+function find_clean_energy_production(product::REC,
+ project::P) where {P <: Project{<: BuildPhase}}
+
+
+ return get_project_expected_rec_output(project)
+end
+
+"""
+This function returns 0 if the project is not of type BatteryEMIS and product is not of type Energy
+"""
+function find_storage_energy_consumption(product::T,
+ project::P) where {T <: Product, P <: Project{<: BuildPhase}}
+
+ return 0.0
+end
+
+function find_storage_energy_consumption(product::T,
+ project::BatteryEMIS{Existing}) where {T <: Product}
+
+ return 0.0
+end
+
+function find_storage_energy_consumption(product::Energy,
+ project::P) where {P <: Project{<: BuildPhase}}
+
+ return 0.0
+end
+
+# This function pushes project bids in the REC supply curve
+function find_storage_energy_consumption(product::Energy,
+ project::BatteryEMIS{Existing})
+
+
+ return get_expected_production(product)
+end
+
+
+function get_expected_rec_certificates(project::Project)
+ certificates = 0.0
+ for product in get_products(project)
+ temp = get_expected_rec_certificates(product)
+ if !(isnothing(temp))
+ certificates = temp
+ end
+ end
+ return certificates
+end
+
+"""
+This function models the actual REC market clearing.
+"""
+function rec_market_clearing_non_binding(rec_requirement::Float64,
+ ACP::Float64,
+ supply_curve::Vector{Vector{Union{String, Float64}}},
+ solver::JuMP.MOI.OptimizerWithAttributes)
+
+ #Each Element of the REC supply Curve:
+ #[1] - Project Name (Symbol)
+ #[2] - Project REC output (Float64)
+ #[3] - Project REC Bid (Float64)
+
+ n_supply_seg = length(supply_curve);
+
+ #----------REC Market Clearing Problem---------------------------------------------------------------------------------
+ rec_mkt = JuMP.Model(solver);
+
+ #Define the variables
+ JuMP.@variables(rec_mkt, begin
+ Q_supply[s=1:n_supply_seg] >= 0 # Quantity of cleared REC supply offers
+ v_REC >= 0 # RPS compliance violation
+ end)
+
+ #Functions----------------------------------------------------------------------------------------
+
+ #Cost of procuring REC supply for each segment
+ supply_cost(Q_seg, s) = supply_curve[s][3] * Q_seg;
+
+ #Expressions---------------------------------------------------------------------------------------
+
+ #Totoal Cost of procuring REC supply
+ JuMP.@expression(rec_mkt, total_supply_cost, sum(supply_cost(Q_supply[s], s) for s = 1:n_supply_seg))
+
+ #Constraints--------------------------------------------------------------------------------------
+
+ #Cleared REC supply limit for each segment
+ JuMP.@constraint(rec_mkt, [s=1:n_supply_seg], Q_supply[s] <= supply_curve[s][2])
+
+ #Total cleared REC supply should be greater than or equal to the RPS compliance requirement
+ JuMP.@constraint(rec_mkt, mkt_clear, sum(Q_supply[s] for s in 1:n_supply_seg) + v_REC >= rec_requirement)
+
+ #Define Objective Function - Social Welfare Maxmization
+ JuMP.@objective(rec_mkt, Min, total_supply_cost + v_REC * ACP)
+
+ println("Actual REC Market Clearing:")
+ JuMP.optimize!(rec_mkt)
+
+ println(JuMP.termination_status(rec_mkt))
+ println(JuMP.objective_value(rec_mkt))
+
+ #REC Market Clearing Price is the shadow variable of the REC balance constraint
+ rec_price = AxisArrays.AxisArray(reshape([JuMP.dual(mkt_clear)], 1,), [1])
+ rec_accepted_bid = Dict(supply_curve[s][1] => value.(Q_supply[s]) for s in 1:n_supply_seg)
+ println(rec_price)
+ #------------------------------------------------------------------------------------------------
+ return rec_price, rec_accepted_bid
+
+end
+
+"""
+This function models the actual REC market clearing.
+"""
+function rec_market_clearing_binding(rec_requirement::Float64,
+ ACP::Float64,
+ supply_curve::Vector{Vector{Union{String, Float64}}},
+ solver::JuMP.MOI.OptimizerWithAttributes)
+
+ #Each Element of the REC supply Curve:
+ #[1] - Project Name (Symbol)
+ #[2] - Project REC output (Float64)
+ #[3] - Project REC Bid (Float64)
+
+ n_supply_seg = length(supply_curve);
+
+ #----------REC Market Clearing Problem---------------------------------------------------------------------------------
+ rec_mkt = JuMP.Model(solver);
+
+ #Define the variables
+ JuMP.@variables(rec_mkt, begin
+ Q_supply[s=1:n_supply_seg] >= 0 # Quantity of cleared REC supply offers
+ end)
+
+ #Functions----------------------------------------------------------------------------------------
+
+ #Cost of procuring REC supply for each segment
+ supply_cost(Q_seg, s) = supply_curve[s][3] * Q_seg;
+
+ #Expressions---------------------------------------------------------------------------------------
+
+ #Totoal Cost of procuring REC supply
+ JuMP.@expression(rec_mkt, total_supply_cost, sum(supply_cost(Q_supply[s], s) for s = 1:n_supply_seg))
+
+ #Constraints--------------------------------------------------------------------------------------
+
+ #Cleared REC supply limit for each segment
+ JuMP.@constraint(rec_mkt, [s=1:n_supply_seg], Q_supply[s] <= supply_curve[s][2])
+
+ #Total cleared REC supply should be greater than or equal to the RPS compliance requirement
+ JuMP.@constraint(rec_mkt, mkt_clear, sum(Q_supply[s] for s in 1:n_supply_seg) >= rec_requirement)
+
+ #Define Objective Function - Social Welfare Maxmization
+ JuMP.@objective(rec_mkt, Min, total_supply_cost)
+
+ println("Actual REC Market Clearing:")
+ JuMP.optimize!(rec_mkt)
+
+ println(JuMP.termination_status(rec_mkt))
+ println(JuMP.objective_value(rec_mkt))
+
+ #REC Market Clearing Price is the shadow variable of the REC balance constraint
+ rec_price = AxisArrays.AxisArray(reshape([JuMP.dual(mkt_clear)], 1,), [1])
+ rec_accepted_bid = Dict(supply_curve[s][1] => value.(Q_supply[s]) for s in 1:n_supply_seg)
+ println(rec_price)
+ #------------------------------------------------------------------------------------------------
+ return rec_price, rec_accepted_bid
+
+end
+
+function update_rec_correction_factors!(active_projects::Vector{Project},
+ realized_capacity_factors::Dict{String, Array{Float64, 2}},
+ rt_resolution::Int64,
+ iteration_year::Int64)
+
+ zones = unique(get_zone.(get_tech.(active_projects)))
+ techs = unique(get_type.(get_tech.(active_projects)))
+
+ zone_tech_expected = Dict(["$(zone)_$(tech)" => 0.0 for zone in zones, tech in techs])
+ zone_tech_actual = Dict(["$(zone)_$(tech)" => 0.0 for zone in zones, tech in techs])
+
+ for project in active_projects
+ project_name = get_name(project)
+ zone = get_zone(get_tech(project))
+ tech = get_type(get_tech(project))
+
+ if project_name in keys(realized_capacity_factors)
+ #println(project_name)
+ expected_rec_production = get_expected_rec_certificates(project)
+ #println(expected_rec_production)
+ actual_rec_production = sum(realized_capacity_factors[project_name]) * get_maxcap(project) * rt_resolution / 60
+ #println(actual_rec_production)
+ zone_tech_expected["$(zone)_$(tech)"] += expected_rec_production
+ zone_tech_actual["$(zone)_$(tech)"] += actual_rec_production
+ end
+
+ end
+
+ for project in active_projects
+ zone = get_zone(get_tech(project))
+ tech = get_type(get_tech(project))
+ expected_rec_production = zone_tech_expected["$(zone)_$(tech)"]
+ actual_rec_production = zone_tech_actual["$(zone)_$(tech)"]
+ previous_correction_factor = get_rec_correction_factor(project, iteration_year)
+ if !(iszero(expected_rec_production))
+ correction_factor = min((actual_rec_production / expected_rec_production), 1.0)
+ else
+ correction_factor = 1.0
+ end
+
+ for product in get_products(project)
+ set_rec_correction_factor!(product, iteration_year + 1, (correction_factor + previous_correction_factor) / 2)
+ end
+
+ end
+
+
+ return
+end
diff --git a/src/markets_simulation/cem.jl b/src/markets_simulation/cem.jl
index 8294561..46e9add 100644
--- a/src/markets_simulation/cem.jl
+++ b/src/markets_simulation/cem.jl
@@ -3,6 +3,7 @@ function cem(system::MarketClearingProblem{Z, T},
solver::JuMP.MOI.OptimizerWithAttributes,
resultfile::String="") where {Z, T}
+
lines = [line.name for line in system.lines] # Lines
projects = [project.name for project in system.projects] # Projects
invperiods = 1:length(system.inv_periods) # Investment periods
@@ -41,6 +42,8 @@ function cem(system::MarketClearingProblem{Z, T},
remaining_life_time, # remaining life_time
capacity_eligible, # project eligible for capacity market
rec_eligible, # project eligible for REC market
+ rec_correction, # project's rec correction factor
+ inertia_constant, synchronous_inertia, # project inertia details
location = # project zone location
make_parameter_vector.(
Ref(system.projects), :name,
@@ -53,7 +56,8 @@ function cem(system::MarketClearingProblem{Z, T},
:ramp_limits, :max_reserve_limits,
:existing_units, :units_in_queue, :build_lead_time, :remaining_build_time,
:max_new_options, :base_cost_units, :capex_years, :life_time, :remaining_life,
- :rec_eligible, :capacity_eligible, :zone])
+ :capacity_eligible, :rec_eligible, :rec_correction,
+ :inertia_constant, :synchronous_inertia, :zone])
life_range = AxisArrays.AxisArray(convert.(Int64, ones(length(projects))), projects)
tech_types = unique(tech_type)
@@ -133,10 +137,19 @@ function cem(system::MarketClearingProblem{Z, T},
price_cap_rec = AxisArrays.AxisArray(zeros(length(invperiods)), invperiods)
rec_requirement = AxisArrays.AxisArray(zeros(length(invperiods)), invperiods)
+ rec_binding = AxisArrays.AxisArray(falses(length(invperiods)), invperiods)
+
+ price_cap_inertia = AxisArrays.AxisArray(zeros(length(invperiods)), invperiods)
+ inertia_requirement = AxisArrays.AxisArray(zeros(length(invperiods)), invperiods)
for p in invperiods
price_cap_rec[p] = getproperty(getproperty(system.inv_periods[p], :rec), :price_cap)
rec_requirement[p] = getproperty(getproperty(system.inv_periods[p], :rec), :rec_req)
+ rec_binding[p] = getproperty(getproperty(system.inv_periods[p], :rec), :binding)
+
+ price_cap_inertia[p] = getproperty(getproperty(system.inv_periods[p], :inertia), :price_cap)
+ inertia_requirement[p] = getproperty(getproperty(system.inv_periods[p], :inertia), :inertia_req)
+
for z in zones
price_cap_e[z, p] = getproperty(getproperty(system.inv_periods[p], :energy), :price_cap)[z]
demand_e[z, p, :] = getproperty(getproperty(system.inv_periods[p], :energy), :demand)[z, :]
@@ -271,6 +284,10 @@ function cem(system::MarketClearingProblem{Z, T},
JuMP.@variable(m, p_out_rd[g in storage_projects, rp in reserve_down_products, p in invperiods, t in opperiods] >= 0) # reserve down provided by storage discharging [MW]
JuMP.@variable(m, p_out_ordc[g in storage_projects, rp in ordc_products, p in invperiods, t in opperiods] >= 0) # synchronous reserve provided by storage discharging [MW]
+ JuMP.@variable(m, p_inertia[g in projects, p in invperiods, t in opperiods] >= 0) # inertia provided by project [MW-s]
+ JuMP.@variable(m, p_out_inertia[g in storage_projects, p in invperiods, t in opperiods] >= 0) # inertia provided by storage project discharging [MW-s]
+ JuMP.@variable(m, p_in_inertia[g in storage_projects, p in invperiods, t in opperiods] >= 0) # inertia provided by project charging[MW-s]
+
JuMP.@variable(m, flow[l in lines, p in invperiods, t in opperiods]) # Line flow
# SHORTFALL VARIABLES
@@ -279,6 +296,7 @@ function cem(system::MarketClearingProblem{Z, T},
JuMP.@variable(m, v_ru[rp in reserve_up_products, p in invperiods, t in opperiods] >= 0) # reserve down shortfall [MW]
JuMP.@variable(m, v_rd[rp in reserve_down_products, p in invperiods, t in opperiods] >= 0) # reserve down shortfall [MW]
JuMP.@variable(m, v_rec[p in invperiods] >= 0) # REC shortfall [MWh]
+ JuMP.@variable(m, v_inertia[p in invperiods, t in opperiods] >= 0) # Inertia shortfall [MWh]
# Populate units dispatchable expression based on remaining build/construction time
JuMP.@expression(m, unitsdispatchable[g in projects, p in invperiods], 0
@@ -397,7 +415,9 @@ function cem(system::MarketClearingProblem{Z, T},
- sum(v_rd[rp, p, t] * price_cap_rd[rp][p] * rep_hour_weight[t] * social_npv_array[p]
for rp in reserve_down_products, p in invperiods, t in opperiods)
- )
+
+ - sum(v_inertia[p, t] * price_cap_inertia[p] * rep_hour_weight[t] * social_npv_array[p]
+ for p in invperiods, t in opperiods))
)
@@ -447,8 +467,15 @@ function cem(system::MarketClearingProblem{Z, T},
# Generator technical constraints
JuMP.@constraint(m, p_e[g, p, t] - sum(p_rd[g, rp, p, t] for rp in reserve_down_products) >= unitsdispatchable[g, p] * 0.0) # Minimum dispatch
- JuMP.@constraint(m, p_e[g, p, t] + sum(p_ru[g, rp, p, t] for rp in reserve_up_products) + sum(p_ordc[g, rp, p, t] for rp in ordc_products) <=
- unitsdispatchable[g, p] * max_gen[g] * availability[g][p, t]) # Maximum dispatch
+ if synchronous_inertia[g]
+ JuMP.@constraint(m, p_e[g, p, t] + sum(p_ru[g, rp, p, t] for rp in reserve_up_products) + sum(p_ordc[g, rp, p, t] for rp in ordc_products) <=
+ unitsdispatchable[g, p] * max_gen[g] * availability[g][p, t]) # Maximum dispatch
+
+ JuMP.@constraint(m, p_inertia[g, p, t] == p_e[g, p, t]) # Inertia provision
+ else
+ JuMP.@constraint(m, p_e[g, p, t] + sum(p_ru[g, rp, p, t] for rp in reserve_up_products) + sum(p_ordc[g, rp, p, t] for rp in ordc_products) + p_inertia[g, p, t] <=
+ unitsdispatchable[g, p] * max_gen[g] * availability[g][p, t]) # Maximum dispatch
+ end
for rp in reserve_up_products
JuMP.@constraint(m, p_ru[g, rp, p, t] <= unitsdispatchable[g, p] * max_reserve_limits[g][rp]) # Maximum reserve up
@@ -464,12 +491,12 @@ function cem(system::MarketClearingProblem{Z, T},
elseif in(g, storage_projects)
# Storage technical constraints
- JuMP.@constraint(m, p_e[g, p, t] - sum(p_out_rd[g, rp, p, t] for rp in reserve_down_products) >= unitsdispatchable[g, p] * 0.0) # Minimum output dispatch
+ JuMP.@constraint(m, p_e[g, p, t] - sum(p_out_rd[g, rp, p, t] for rp in reserve_down_products) >= unitsdispatchable[g, p] * 0.0) # Minimum output dispatch
- JuMP.@constraint(m, p_e[g, p, t] + sum(p_out_ru[g, rp, p, t] for rp in reserve_up_products) + sum(p_out_ordc[g, rp, p, t] for rp in ordc_products) <=
+ JuMP.@constraint(m, p_e[g, p, t] + sum(p_out_ru[g, rp, p, t] for rp in reserve_up_products) + sum(p_out_ordc[g, rp, p, t] for rp in ordc_products) + p_out_inertia[g, p, t] <=
unitsdispatchable[g, p] * max_gen[g]) # Maximum output dispatch
- JuMP.@constraint(m, p_in[g, p, t] - sum(p_in_ru[g, rp, p, t] for rp in reserve_up_products) - sum(p_in_ordc[g, rp, p, t] for rp in ordc_products) >=
+ JuMP.@constraint(m, p_in[g, p, t] - sum(p_in_ru[g, rp, p, t] for rp in reserve_up_products) - sum(p_in_ordc[g, rp, p, t] for rp in ordc_products) - p_in_inertia[g, p, t] >=
unitsdispatchable[g, p] * 0.0) # Minimum input dispatch
JuMP.@constraint(m, p_in[g, p, t] + sum(p_in_rd[g, rp, p, t] for rp in reserve_down_products) <= unitsdispatchable[g, p] * max_input[g]) # Maximum input dispatch
@@ -486,12 +513,16 @@ function cem(system::MarketClearingProblem{Z, T},
JuMP.@constraint(m, p_rd[g, rp, p, t] <= p_out_rd[g, rp, p, t] + p_in_rd[g, rp, p, t]) # Total storage reserve down
end
+ JuMP.@constraint(m, p_inertia[g, p, t] <= p_out_inertia[g, p, t] + p_in_inertia[g, p, t]) # Total storage inertia
+
JuMP.@constraint(m, storage_level[g, p, t] >= unitsdispatchable[g, p] * min_storage[g]) # Minimum storage level
JuMP.@constraint(m, storage_level[g, p, t] <= unitsdispatchable[g, p] * max_storage[g]) # Maximum storage level
- if in(t, end_of_day)
- JuMP.@constraint(m, storage_level[g, p, t] == init_storage[g] * max_storage[g] * unitsdispatchable[g, p])
- end
+ JuMP.@constraint(m, p_out_inertia[g, p, t] * inertia_constant[g] <= storage_level[g, p, t]) # Minimum storage level
+
+ #if in(t, end_of_day)
+ # JuMP.@constraint(m, storage_level[g, p, t] == init_storage[g] * max_storage[g] * unitsdispatchable[g, p])
+ #end
end
end
@@ -507,6 +538,10 @@ function cem(system::MarketClearingProblem{Z, T},
end
end
end
+
+ if rec_binding[p]
+ JuMP.@constraint(m, v_rec[p] == 0)
+ end
end
# Market Clearing Constraints:
@@ -520,27 +555,28 @@ function cem(system::MarketClearingProblem{Z, T},
# reserve up markets
JuMP.@constraint(m, reserve_up_market[rp in reserve_up_products, p in invperiods, t in opperiods], # reserve up balance
- sum(p_ru[g, rp, p, t] for g in reserve_up_projects[rp]) + v_ru[rp, p, t] == demand_ru[rp][p, t])
+ sum(p_ru[g, rp, p, t] for g in reserve_up_projects[rp]) + v_ru[rp, p, t] >= demand_ru[rp][p, t])
# reserve down markets
JuMP.@constraint(m, reserve_down_market[rp in reserve_down_products, p in invperiods, t in opperiods], # reserve down balance
- sum(p_rd[g, rp, p, t] for g in reserve_down_projects[rp]) + v_rd[rp, p, t] == demand_rd[rp][p, t])
+ sum(p_rd[g, rp, p, t] for g in reserve_down_projects[rp]) + v_rd[rp, p, t] >= demand_rd[rp][p, t])
# ordc markets
JuMP.@constraint(m, ordc_market[rp in ordc_products, p in invperiods, t in opperiods], # ordc market balance
- sum(p_ordc[g, rp, p, t] for g in ordc_projects[rp]) == sum(d_ordc[rp, p, t, s] for s in 1:ordc_numsegments[rp][p, t]))
+ sum(p_ordc[g, rp, p, t] for g in ordc_projects[rp]) >= sum(d_ordc[rp, p, t, s] for s in 1:ordc_numsegments[rp][p, t]))
# Capacity market
JuMP.@constraint(m, capacity_market[p in invperiods],
sum(c[g, p] * max_gen[g] * derating_factor[g] for g in capacity_mkt_projects) == sum(d_cap[p, s] for s in 1:cap_numsegments[p]))
-
# REC market
JuMP.@constraint(m, rps_compliance[p in invperiods],
- sum(p_e[g, p, t] * rep_hour_weight[t] for g in rps_compliant_projects, t in opperiods) + v_rec[p]
- >= sum(demand_e[z, p, t] * rep_hour_weight[t] for z in zones, t in opperiods) * rec_requirement[p])
-
+ sum(p_e[g, p, t] * rec_correction[g] * rep_hour_weight[t] for g in rps_compliant_projects, t in opperiods) + v_rec[p]
+ >= (sum(demand_e[z, p, t] * rep_hour_weight[t] for z in zones, t in opperiods)) * rec_requirement[p])
+ # Inertia market
+ JuMP.@constraint(m, inertia_market[p in invperiods, t in opperiods],
+ sum(p_inertia[g, p, t] * inertia_constant[g] for g in projects) + v_inertia[p, t] >= inertia_requirement[p])
println("Price Projection:")
@time JuMP.optimize!(m)
@@ -554,6 +590,7 @@ function cem(system::MarketClearingProblem{Z, T},
reserve_down_price = JuMP.dual.(reserve_down_market)
ordc_price = JuMP.dual.(ordc_market)
REC_price = JuMP.dual.(rps_compliance)
+ inertia_price = JuMP.dual.(inertia_market)
capacity_factor = Dict([g => zeros(length(invperiods), length(opperiods)) for g in projects])
total_utilization = Dict([g => zeros(length(invperiods), length(opperiods)) for g in projects])
@@ -578,7 +615,6 @@ function cem(system::MarketClearingProblem{Z, T},
else
capacity_factor[g][p, t] = value.(p_e[g, p, t]) /(value.(unitsdispatchable[g, p]) * max_gen[g])
total_utilization[g][p, t] = value.(p_e[g, p, t])
-
if length(reserve_up_products) > 0
total_utilization[g][p, t] += sum(value.(p_ru[g, rp, p, t]) for rp in reserve_up_products)
end
@@ -590,8 +626,8 @@ function cem(system::MarketClearingProblem{Z, T},
total_utilization[g][p, t] = total_utilization[g][p, t] / (value.(unitsdispatchable[g, p]) * max_gen[g])
end
end
-
end
+
if in(g, option_projects)
for i in 0:base_cost_units[g]:max_new_options[g] + base_cost_units[g]
if value.(n[g, 1]) > i + ϵ
@@ -601,14 +637,11 @@ function cem(system::MarketClearingProblem{Z, T},
end
end
-
-
nominal_capacity_price = AxisArrays.AxisArray(zeros(length(capacity_price)), invperiods)
nominal_REC_price = AxisArrays.AxisArray(zeros(length(REC_price)), invperiods)
-
nominal_energy_price = AxisArrays.AxisArray(zeros(size(energy_price)), zones, invperiods, opperiods)
-
nominal_reserve_price = Dict(product => zeros(length(invperiods), length(opperiods)) for product in all_reserves)
+ nominal_inertia_price = AxisArrays.AxisArray(zeros(size(inertia_price)), invperiods, opperiods)
for p in invperiods
@@ -627,11 +660,14 @@ function cem(system::MarketClearingProblem{Z, T},
end
end
+ nominal_inertia_price[p, t] = (inertia_price[p, t] / rep_hour_weight[t]) / social_npv_array[p]
+
end
end
-
+ #=
for p in invperiods
+
println(p)
println("Energy")
println(Statistics.mean(nominal_energy_price[:, p, :]))
@@ -641,9 +677,31 @@ function cem(system::MarketClearingProblem{Z, T},
println(Statistics.mean(nominal_reserve_price[product][p, :]))
println(maximum(nominal_reserve_price[product][p, :]))
end
+ println("Inertia")
+ println(Statistics.mean(nominal_inertia_price[p, :]))
+ println(maximum(nominal_inertia_price[p, :]))
+
+
+ println("Total Demand")
+ println(sum(demand_e[z, p, t] * rep_hour_weight[t] for z in zones, t in opperiods))
+
+ println("Total Generation")
+ println(sum(value.(p_e[g, p, t]) * rep_hour_weight[t] for g in generator_projects, t in opperiods))
+
+ println("Total Clean Generation")
+ println(sum(value.(p_e[g, p, t]) * rep_hour_weight[t] for g in rps_compliant_projects, t in opperiods))
+
+ println("RPS Target")
+ println(rec_requirement[p])
+
+ println("RPS Achieved")
+ println(sum(value.(p_e[g, p, t]) * rep_hour_weight[t] for g in rps_compliant_projects, t in opperiods) / (sum(demand_e[z, p, t] * rep_hour_weight[t] for z in zones, t in opperiods)))
+
end
+ =#
println(nominal_capacity_price)
-
+ println(nominal_REC_price)
+ println(new_options)
for z in zones
for p in invperiods
@@ -659,6 +717,7 @@ function cem(system::MarketClearingProblem{Z, T},
nominal_energy_price,
nominal_reserve_price,
nominal_REC_price,
+ nominal_inertia_price,
capacity_factor,
total_utilization,
capacity_accepted_perc,
diff --git a/src/markets_simulation/economicdispatch.jl b/src/markets_simulation/economicdispatch.jl
index 4a8fb84..c48a238 100644
--- a/src/markets_simulation/economicdispatch.jl
+++ b/src/markets_simulation/economicdispatch.jl
@@ -1,301 +1,301 @@
-
-function economicdispatch(system::MarketClearingProblem{Z, T},
- solver::JuMP.MOI.OptimizerWithAttributes,
- resultfile::String="") where {Z, T}
-
- lines = [line.name for line in system.lines] # Lines
- projects = [project.name for project in system.projects] # Projects
- invperiods = 1:length(system.inv_periods) # Investment periods
- opperiods = 1:T
- rep_hour_weight = system.rep_hour_weight
-
- end_of_day = collect(24:24:T)
-
- project_type, # is the project of storage type
- marginal_energy_cost, # $/MW
- marginal_reserveup_cost, # $/MW
- marginal_reservedown_cost, # $/MW
- min_gen, max_gen, # MW/unit
- min_input, max_input, # MW/unit
- efficiency_in, efficiency_out, # percentage
- min_storage, max_storage, # MWh/unit
- init_storage, # MWh/unit
- availability, # hourly availability factor
- ramp_limits, # MW/unit/hour
- max_reserveup, max_reservedown, # MW/unit/hour
- location = # Project zone location
- make_parameter_vector.(
- Ref(system.projects), :name,
- [:project_type, :marginal_energy_cost, :marginal_reserveup_cost, :marginal_reservedown_cost,
- :min_gen, :max_gen, :min_input, :max_input,
- :efficiency_in, :efficiency_out, :min_storage, :max_storage, :init_storage,
- :availability,
- :ramp_limits, :max_reserveup, :max_reservedown, :zone]
- )
-
- zones = system.zones
- price_cap_e = AxisArrays.AxisArray(zeros(length(zones), length(invperiods)), zones, invperiods)
- demand_e = AxisArrays.AxisArray(zeros(length(zones), length(invperiods), T), zones, invperiods, opperiods)
-
- demand_ru = AxisArrays.AxisArray(zeros(length(zones), length(invperiods), T), zones, invperiods, opperiods)
-
- price_cap_rd = AxisArrays.AxisArray(zeros(length(zones), length(invperiods)), zones, invperiods)
- demand_rd = AxisArrays.AxisArray(zeros(length(zones), length(invperiods), T), zones, invperiods, opperiods)
-
- for p in invperiods
- for z in zones
- price_cap_e[z, p] = getproperty(getproperty(system.inv_periods[p], :energy_market), :price_cap)[z]
- demand_e[z, p, :] = getproperty(getproperty(system.inv_periods[p], :energy_market), :demand)[z, :]
-
- demand_ru[z, p, :] = getproperty(getproperty(system.inv_periods[p], :reserveup_market), :demand)[z, :]
-
- price_cap_rd[z, p] = getproperty(getproperty(system.inv_periods[p], :reservedown_market), :price_cap)[z]
- demand_rd[z, p, :] = getproperty(getproperty(system.inv_periods[p], :reservedown_market), :demand)[z, :]
- end
- end
-
- ru_segmentsize, ru_segmentgrad, ru_price_points, ru_numsegments = make_ORDC_vectors(getproperty.(system.inv_periods, :reserveup_market))
-
- zone_projects = Dict(z => Vector{String}() for z in zones)
-
- generator_projects = Vector{String}()
- storage_projects = Vector{String}()
-
- zone_projects = Dict(z => Vector{String}() for z in zones)
- zone_storage = Dict(z => Vector{String}() for z in zones)
-
-
- ramp_lim_projects = Vector{String}()
-
- for project in projects
-
- if ramp_limits[project] !== nothing
- push!(ramp_lim_projects, project)
- end
-
- for z in zones
- if location[project] == z
- push!(zone_projects[z], project)
-
- if project_type[project] == "generator"
- push!(generator_projects, project)
- elseif project_type[project] == "storage"
- push!(storage_projects, project)
- push!(zone_storage[z], project)
- end
-
- end
- end
- end
-
- # Populate line data
- from_zone,
- to_zone,
- linepowerlimit =
- make_parameter_vector.(
- Ref(system.lines), :name,
- [:from_zone, :to_zone, :active_power_limit]
- )
-
- lines_from_zone = Dict(z => Vector{String}() for z in zones)
- lines_to_zone = Dict(z => Vector{String}() for z in zones)
-
- for line in lines
- for zone in zones
- if from_zone[line] == zone
- push!(lines_from_zone[zone], line)
- elseif to_zone[line] == zone
- push!(lines_to_zone[zone], line)
- end
- end
- end
-
- m = JuMP.Model(solver)
-
- # OPERATION DECISION VARIABLES
-
- JuMP.@variable(m, p_e[g in projects, p in invperiods, t in opperiods] >= 0) # Unit energy production [MW]
- JuMP.@variable(m, p_in[g in storage_projects, p in invperiods, t in opperiods] >= 0) # Storage charging [MW]
- JuMP.@variable(m, p_ru[g in projects, p in invperiods, t in opperiods] >= 0) # reserve up provided [MW]
- JuMP.@variable(m, p_rd[g in projects, p in invperiods, t in opperiods] >= 0) # reserve down provided [MW]
-
- JuMP.@variable(m, p_in_ru[g in storage_projects, p in invperiods, t in opperiods] >= 0) # reserve up provided by storage charging [MW]
- JuMP.@variable(m, p_in_rd[g in storage_projects, p in invperiods, t in opperiods] >= 0) # reserve down provided by storage charging [MW]
-
- JuMP.@variable(m, p_out_ru[g in storage_projects, p in invperiods, t in opperiods] >= 0) # reserve up provided by storage discharging [MW]
- JuMP.@variable(m, p_out_rd[g in storage_projects, p in invperiods, t in opperiods] >= 0) # reserve down provided by storage discharging [MW]
-
- JuMP.@variable(m, flow[l in lines, p in invperiods, t in opperiods]) # Line flow
-
- # ORDC DEMAND VARIABLE
- JuMP.@variable(m, d_ru[z in zones, p in invperiods, t in opperiods, s in 1:ru_numsegments[z, p]] >= 0)
-
- JuMP.@variable(m, v_e[z in zones, p in invperiods, t in opperiods] >= 0) # Load shortfall [MW]
- JuMP.@variable(m, v_rd[z in zones, p in invperiods, t in opperiods] >= 0) # Reserve Down shortfall [MW]
-
- JuMP.@expression(m, annual_project_costs[g in projects, p in invperiods], 0
- + sum((p_e[g, p, t] * marginal_energy_cost[g]
- + p_ru[g, p, t] * marginal_reserveup_cost[g]
- + p_rd[g, p, t] * marginal_reservedown_cost[g]) * rep_hour_weight[t] for t in opperiods)) # project operating costs
-
- # Storage level evolution
- JuMP.@expression(m, storage_level[g in storage_projects, p in invperiods, t in opperiods],
- p_in[g, p, t] * efficiency_in[g] - p_e[g, p, t] / efficiency_out[g])
-
-
- for g in storage_projects
- for p in invperiods
- for t in opperiods
- if t == 1 || in(t, end_of_day + ones(length(end_of_day)))
- JuMP.add_to_expression!(storage_level[g, p, t], init_storage[g] * max_storage[g])
- else
- JuMP.add_to_expression!(storage_level[g, p, t], storage_level[g, p, t - 1])
- end
- end
- end
-
- end
-
-
- # OBJECTIVE FUNCTION
-
- JuMP.@objective(m, Max,
-
- # ORDC welfare
- sum(sum(d_ru[z, p, t, s] * demand_ru[z, p, t] * ru_price_points[z, p][s] * rep_hour_weight[t] for z in zones, t in opperiods, s in 1:ru_numsegments[z, p])
- for p in invperiods)
- + 1/2 * sum(sum(ru_segmentgrad[z, p][s] * (d_ru[z, p, t, s] * demand_ru[z, p, t])^2 * rep_hour_weight[t] for z in zones, t in opperiods, s in 1:ru_numsegments[z, p])
- for p in invperiods)
-
- # Annual costs incurred by the projects
- - sum(annual_project_costs[g, p]
- for g in projects, p in invperiods)
-
- # weight operations according to actual duration
- # operations market shortfall penalties
- + ( - sum((v_e[z, p, t] * price_cap_e[z, p] + v_rd[z, p, t] * price_cap_rd[z, p]) * rep_hour_weight[t]
- for z in zones, p in invperiods, t in opperiods)
- )
-
- )
-
- # CONSTRAINTS
-
- # Operations:
-
- # Energy market
- JuMP.@constraint(m, energy_market[z in zones, p in invperiods, t in opperiods], # Power balance
- sum(p_e[g ,p, t] for g in zone_projects[z])
- + sum(flow[l, p, t] for l in lines_to_zone[z])
- - sum(flow[l, p, t] for l in lines_from_zone[z])
- + v_e[z, p, t] == demand_e[z, p, t] + sum(p_in[g, p, t] for g in zone_storage[z]))
-
- # reserve up market
- JuMP.@constraint(m, reserve_up_market[z in zones, p in invperiods, t in opperiods], # reserve up balance
- sum(p_ru[g ,p, t] for g in zone_projects[z]) == sum(d_ru[z, p, t, s] for s in 1:ru_numsegments[z, p]) * demand_ru[z, p, t] )
-
- JuMP.@constraint(m, ru_segment_limit[z in zones, p in invperiods, t in opperiods, s in 1:ru_numsegments[z, p]],
- d_ru[z, p, t, s] <= ru_segmentsize[z, p][s])
-
- # reserve down market
- JuMP.@constraint(m, reserve_down_market[z in zones, p in invperiods, t in opperiods], # reserve down balance
- sum(p_rd[g ,p, t] for g in zone_projects[z]) + v_rd[z, p, t] == demand_rd[z, p, t])
-
- # Line flow constraints
- JuMP.@constraint(m, negative_flow[l in lines, p in invperiods, t in opperiods],
- flow[l, p, t] >= -linepowerlimit[l])
-
- JuMP.@constraint(m, positive_flow[l in lines, p in invperiods, t in opperiods],
- flow[l, p, t] <= linepowerlimit[l])
-
- # Unit operating constraints (dispatch and ramping)
-
- # Generator technical constraints
- JuMP.@constraint(m, min_generation[g in generator_projects, p in invperiods, t in opperiods],
- p_e[g, p, t] - p_rd[g, p, t] >= 0.0) # Minimum dispatch
-
- JuMP.@constraint(m, max_generation[g in generator_projects, p in invperiods, t in opperiods],
- p_e[g, p, t] + p_ru[g, p, t] <= max_gen[g] * availability[g][p, t]) # Maximum dispatch
-
- JuMP.@constraint(m, reserve_down_max[g in generator_projects, p in invperiods, t in opperiods],
- p_rd[g, p, t] <= max_reservedown[g]) # Maximum Reserve Down
-
- JuMP.@constraint(m, reserve_up_max[g in generator_projects, p in invperiods, t in opperiods],
- p_ru[g, p, t] <= max_reserveup[g]) # Maximum Reserve Up
-
- JuMP.@constraint(m, rampdown[g in ramp_lim_projects, p in invperiods, t in opperiods[2:end]], # Down-ramp limit
- p_e[g, p ,t] - p_rd[g, p ,t] >= p_e[g, p, t-1] - ramp_limits[g][:down])
-
- JuMP.@constraint(m, rampup[g in ramp_lim_projects, p in invperiods, t in opperiods[2:end]], # Up-ramp limit
- p_e[g, p, t] + p_ru[g, p, t] <= p_e[g, p, t-1] + ramp_limits[g][:up])
-
- # Storage technical constraints
- JuMP.@constraint(m, minoutput[g in storage_projects, p in invperiods, t in opperiods],
- p_e[g, p, t] - p_out_rd[g, p, t] >= 0.0) # Minimum output dispatch
-
- JuMP.@constraint(m, maxoutput[g in storage_projects, p in invperiods, t in opperiods],
- p_e[g, p, t] + p_out_ru[g, p, t] <= max_gen[g]) # Maximum output dispatch
-
- JuMP.@constraint(m, min_input[g in storage_projects, p in invperiods, t in opperiods],
- p_in[g, p, t] - p_in_ru[g, p, t] >= 0.0) # Minimum input dispatch
-
- JuMP.@constraint(m, max_input[g in storage_projects, p in invperiods, t in opperiods],
- p_in[g, p, t] + p_in_rd[g, p, t] <= max_input[g]) # Maximum input dispatch
-
-
- JuMP.@constraint(m, storage_reserve_up[g in storage_projects, p in invperiods, t in opperiods],
- p_ru[g, p, t] <= p_out_ru[g, p, t] + p_in_ru[g, p, t]) # Maximum storage reserve up
-
- JuMP.@constraint(m, storage_reserve_down[g in storage_projects, p in invperiods, t in opperiods],
- p_rd[g, p, t] <= p_out_rd[g, p, t] + p_in_rd[g, p, t]) # Maximum storage reserve down
-
- JuMP.@constraint(m, min_storage_level[g in storage_projects, p in invperiods, t in opperiods],
- storage_level[g, p, t] >= min_storage[g]) # Minimum storage level
-
- JuMP.@constraint(m, max_storage_level[g in storage_projects, p in invperiods, t in opperiods],
- storage_level[g, p, t] <= max_storage[g]) # Maximum storage level
-
- JuMP.@constraint(m, end_day_storage_level[g in storage_projects, p in invperiods, t in end_of_day],
- storage_level[g, p, t] == init_storage[g] * max_storage[g]) # Maximum storage level after reserve provision
-
-
- println("Actual Energy and A/S Market Clearing:")
- JuMP.optimize!(m)
- println(JuMP.termination_status(m))
- println(JuMP.objective_value(m))
-
- energy_dual = JuMP.dual.(energy_market)
- reserve_up_dual = JuMP.dual.(reserve_up_market)
- reserve_down_dual = JuMP.dual.(reserve_down_market)
-
- energy_price = AxisArrays.AxisArray(zeros(size(energy_dual)), zones, invperiods, opperiods)
- reserve_up_price = AxisArrays.AxisArray(zeros(size(reserve_up_dual)), zones, invperiods, opperiods)
- reserve_down_price = AxisArrays.AxisArray(zeros(size(reserve_down_dual)), zones, invperiods, opperiods)
-
-
- for p in invperiods
- for t in opperiods
- energy_price[:, p, t] = round.((energy_dual[:, p, t] / rep_hour_weight[t]), digits = 5)
- reserve_up_price[:, p, t] = round.((reserve_up_dual[:, p, t] / rep_hour_weight[t]), digits = 5)
- reserve_down_price[:, p, t] = round.((reserve_down_dual[:, p, t] / rep_hour_weight[t]), digits = 5)
- end
- end
-
- capacity_factor = Dict([g => zeros(length(invperiods), length(opperiods)) for g in projects])
- reserve_up_perc = Dict([g => zeros(length(invperiods), length(opperiods)) for g in projects])
- reserve_down_perc = Dict([g => zeros(length(invperiods), length(opperiods)) for g in projects])
-
- for g in projects
- for p in invperiods
- for t in opperiods
- capacity_factor[g][p, t] = value.(p_e[g, p, t]) / max_gen[g]
- reserve_up_perc[g][p, t] = value.(p_ru[g, p, t]) / max_gen[g]
- reserve_down_perc[g][p, t] = value.(p_rd[g, p, t]) / max_gen[g]
- end
-
- end
- end
-
- return energy_price, reserve_up_price, reserve_down_price, capacity_factor, reserve_up_perc, reserve_down_perc;
-
- end
+
+function economicdispatch(system::MarketClearingProblem{Z, T},
+ solver::JuMP.MOI.OptimizerWithAttributes,
+ resultfile::String="") where {Z, T}
+
+ lines = [line.name for line in system.lines] # Lines
+ projects = [project.name for project in system.projects] # Projects
+ invperiods = 1:length(system.inv_periods) # Investment periods
+ opperiods = 1:T
+ rep_hour_weight = system.rep_hour_weight
+
+ end_of_day = collect(24:24:T)
+
+ project_type, # is the project of storage type
+ marginal_energy_cost, # $/MW
+ marginal_reserveup_cost, # $/MW
+ marginal_reservedown_cost, # $/MW
+ min_gen, max_gen, # MW/unit
+ min_input, max_input, # MW/unit
+ efficiency_in, efficiency_out, # percentage
+ min_storage, max_storage, # MWh/unit
+ init_storage, # MWh/unit
+ availability, # hourly availability factor
+ ramp_limits, # MW/unit/hour
+ max_reserveup, max_reservedown, # MW/unit/hour
+ location = # Project zone location
+ make_parameter_vector.(
+ Ref(system.projects), :name,
+ [:project_type, :marginal_energy_cost, :marginal_reserveup_cost, :marginal_reservedown_cost,
+ :min_gen, :max_gen, :min_input, :max_input,
+ :efficiency_in, :efficiency_out, :min_storage, :max_storage, :init_storage,
+ :availability,
+ :ramp_limits, :max_reserveup, :max_reservedown, :zone]
+ )
+
+ zones = system.zones
+ price_cap_e = AxisArrays.AxisArray(zeros(length(zones), length(invperiods)), zones, invperiods)
+ demand_e = AxisArrays.AxisArray(zeros(length(zones), length(invperiods), T), zones, invperiods, opperiods)
+
+ demand_ru = AxisArrays.AxisArray(zeros(length(zones), length(invperiods), T), zones, invperiods, opperiods)
+
+ price_cap_rd = AxisArrays.AxisArray(zeros(length(zones), length(invperiods)), zones, invperiods)
+ demand_rd = AxisArrays.AxisArray(zeros(length(zones), length(invperiods), T), zones, invperiods, opperiods)
+
+ for p in invperiods
+ for z in zones
+ price_cap_e[z, p] = getproperty(getproperty(system.inv_periods[p], :energy_market), :price_cap)[z]
+ demand_e[z, p, :] = getproperty(getproperty(system.inv_periods[p], :energy_market), :demand)[z, :]
+
+ demand_ru[z, p, :] = getproperty(getproperty(system.inv_periods[p], :reserveup_market), :demand)[z, :]
+
+ price_cap_rd[z, p] = getproperty(getproperty(system.inv_periods[p], :reservedown_market), :price_cap)[z]
+ demand_rd[z, p, :] = getproperty(getproperty(system.inv_periods[p], :reservedown_market), :demand)[z, :]
+ end
+ end
+
+ ru_segmentsize, ru_segmentgrad, ru_price_points, ru_numsegments = make_ORDC_vectors(getproperty.(system.inv_periods, :reserveup_market))
+
+ zone_projects = Dict(z => Vector{String}() for z in zones)
+
+ generator_projects = Vector{String}()
+ storage_projects = Vector{String}()
+
+ zone_projects = Dict(z => Vector{String}() for z in zones)
+ zone_storage = Dict(z => Vector{String}() for z in zones)
+
+
+ ramp_lim_projects = Vector{String}()
+
+ for project in projects
+
+ if ramp_limits[project] !== nothing
+ push!(ramp_lim_projects, project)
+ end
+
+ for z in zones
+ if location[project] == z
+ push!(zone_projects[z], project)
+
+ if project_type[project] == "generator"
+ push!(generator_projects, project)
+ elseif project_type[project] == "storage"
+ push!(storage_projects, project)
+ push!(zone_storage[z], project)
+ end
+
+ end
+ end
+ end
+
+ # Populate line data
+ from_zone,
+ to_zone,
+ linepowerlimit =
+ make_parameter_vector.(
+ Ref(system.lines), :name,
+ [:from_zone, :to_zone, :active_power_limit]
+ )
+
+ lines_from_zone = Dict(z => Vector{String}() for z in zones)
+ lines_to_zone = Dict(z => Vector{String}() for z in zones)
+
+ for line in lines
+ for zone in zones
+ if from_zone[line] == zone
+ push!(lines_from_zone[zone], line)
+ elseif to_zone[line] == zone
+ push!(lines_to_zone[zone], line)
+ end
+ end
+ end
+
+ m = JuMP.Model(solver)
+
+ # OPERATION DECISION VARIABLES
+
+ JuMP.@variable(m, p_e[g in projects, p in invperiods, t in opperiods] >= 0) # Unit energy production [MW]
+ JuMP.@variable(m, p_in[g in storage_projects, p in invperiods, t in opperiods] >= 0) # Storage charging [MW]
+ JuMP.@variable(m, p_ru[g in projects, p in invperiods, t in opperiods] >= 0) # reserve up provided [MW]
+ JuMP.@variable(m, p_rd[g in projects, p in invperiods, t in opperiods] >= 0) # reserve down provided [MW]
+
+ JuMP.@variable(m, p_in_ru[g in storage_projects, p in invperiods, t in opperiods] >= 0) # reserve up provided by storage charging [MW]
+ JuMP.@variable(m, p_in_rd[g in storage_projects, p in invperiods, t in opperiods] >= 0) # reserve down provided by storage charging [MW]
+
+ JuMP.@variable(m, p_out_ru[g in storage_projects, p in invperiods, t in opperiods] >= 0) # reserve up provided by storage discharging [MW]
+ JuMP.@variable(m, p_out_rd[g in storage_projects, p in invperiods, t in opperiods] >= 0) # reserve down provided by storage discharging [MW]
+
+ JuMP.@variable(m, flow[l in lines, p in invperiods, t in opperiods]) # Line flow
+
+ # ORDC DEMAND VARIABLE
+ JuMP.@variable(m, d_ru[z in zones, p in invperiods, t in opperiods, s in 1:ru_numsegments[z, p]] >= 0)
+
+ JuMP.@variable(m, v_e[z in zones, p in invperiods, t in opperiods] >= 0) # Load shortfall [MW]
+ JuMP.@variable(m, v_rd[z in zones, p in invperiods, t in opperiods] >= 0) # Reserve Down shortfall [MW]
+
+ JuMP.@expression(m, annual_project_costs[g in projects, p in invperiods], 0
+ + sum((p_e[g, p, t] * marginal_energy_cost[g]
+ + p_ru[g, p, t] * marginal_reserveup_cost[g]
+ + p_rd[g, p, t] * marginal_reservedown_cost[g]) * rep_hour_weight[t] for t in opperiods)) # project operating costs
+
+ # Storage level evolution
+ JuMP.@expression(m, storage_level[g in storage_projects, p in invperiods, t in opperiods],
+ p_in[g, p, t] * efficiency_in[g] - p_e[g, p, t] / efficiency_out[g])
+
+
+ for g in storage_projects
+ for p in invperiods
+ for t in opperiods
+ if t == 1 || in(t, end_of_day + ones(length(end_of_day)))
+ JuMP.add_to_expression!(storage_level[g, p, t], init_storage[g] * max_storage[g])
+ else
+ JuMP.add_to_expression!(storage_level[g, p, t], storage_level[g, p, t - 1])
+ end
+ end
+ end
+
+ end
+
+
+ # OBJECTIVE FUNCTION
+
+ JuMP.@objective(m, Max,
+
+ # ORDC welfare
+ sum(sum(d_ru[z, p, t, s] * demand_ru[z, p, t] * ru_price_points[z, p][s] * rep_hour_weight[t] for z in zones, t in opperiods, s in 1:ru_numsegments[z, p])
+ for p in invperiods)
+ + 1/2 * sum(sum(ru_segmentgrad[z, p][s] * (d_ru[z, p, t, s] * demand_ru[z, p, t])^2 * rep_hour_weight[t] for z in zones, t in opperiods, s in 1:ru_numsegments[z, p])
+ for p in invperiods)
+
+ # Annual costs incurred by the projects
+ - sum(annual_project_costs[g, p]
+ for g in projects, p in invperiods)
+
+ # weight operations according to actual duration
+ # operations market shortfall penalties
+ + ( - sum((v_e[z, p, t] * price_cap_e[z, p] + v_rd[z, p, t] * price_cap_rd[z, p]) * rep_hour_weight[t]
+ for z in zones, p in invperiods, t in opperiods)
+ )
+
+ )
+
+ # CONSTRAINTS
+
+ # Operations:
+
+ # Energy market
+ JuMP.@constraint(m, energy_market[z in zones, p in invperiods, t in opperiods], # Power balance
+ sum(p_e[g ,p, t] for g in zone_projects[z])
+ + sum(flow[l, p, t] for l in lines_to_zone[z])
+ - sum(flow[l, p, t] for l in lines_from_zone[z])
+ + v_e[z, p, t] == demand_e[z, p, t] + sum(p_in[g, p, t] for g in zone_storage[z]))
+
+ # reserve up market
+ JuMP.@constraint(m, reserve_up_market[z in zones, p in invperiods, t in opperiods], # reserve up balance
+ sum(p_ru[g ,p, t] for g in zone_projects[z]) == sum(d_ru[z, p, t, s] for s in 1:ru_numsegments[z, p]) * demand_ru[z, p, t] )
+
+ JuMP.@constraint(m, ru_segment_limit[z in zones, p in invperiods, t in opperiods, s in 1:ru_numsegments[z, p]],
+ d_ru[z, p, t, s] <= ru_segmentsize[z, p][s])
+
+ # reserve down market
+ JuMP.@constraint(m, reserve_down_market[z in zones, p in invperiods, t in opperiods], # reserve down balance
+ sum(p_rd[g ,p, t] for g in zone_projects[z]) + v_rd[z, p, t] == demand_rd[z, p, t])
+
+ # Line flow constraints
+ JuMP.@constraint(m, negative_flow[l in lines, p in invperiods, t in opperiods],
+ flow[l, p, t] >= -linepowerlimit[l])
+
+ JuMP.@constraint(m, positive_flow[l in lines, p in invperiods, t in opperiods],
+ flow[l, p, t] <= linepowerlimit[l])
+
+ # Unit operating constraints (dispatch and ramping)
+
+ # Generator technical constraints
+ JuMP.@constraint(m, min_generation[g in generator_projects, p in invperiods, t in opperiods],
+ p_e[g, p, t] - p_rd[g, p, t] >= 0.0) # Minimum dispatch
+
+ JuMP.@constraint(m, max_generation[g in generator_projects, p in invperiods, t in opperiods],
+ p_e[g, p, t] + p_ru[g, p, t] <= max_gen[g] * availability[g][p, t]) # Maximum dispatch
+
+ JuMP.@constraint(m, reserve_down_max[g in generator_projects, p in invperiods, t in opperiods],
+ p_rd[g, p, t] <= max_reservedown[g]) # Maximum Reserve Down
+
+ JuMP.@constraint(m, reserve_up_max[g in generator_projects, p in invperiods, t in opperiods],
+ p_ru[g, p, t] <= max_reserveup[g]) # Maximum Reserve Up
+
+ JuMP.@constraint(m, rampdown[g in ramp_lim_projects, p in invperiods, t in opperiods[2:end]], # Down-ramp limit
+ p_e[g, p ,t] - p_rd[g, p ,t] >= p_e[g, p, t-1] - ramp_limits[g][:down])
+
+ JuMP.@constraint(m, rampup[g in ramp_lim_projects, p in invperiods, t in opperiods[2:end]], # Up-ramp limit
+ p_e[g, p, t] + p_ru[g, p, t] <= p_e[g, p, t-1] + ramp_limits[g][:up])
+
+ # Storage technical constraints
+ JuMP.@constraint(m, minoutput[g in storage_projects, p in invperiods, t in opperiods],
+ p_e[g, p, t] - p_out_rd[g, p, t] >= 0.0) # Minimum output dispatch
+
+ JuMP.@constraint(m, maxoutput[g in storage_projects, p in invperiods, t in opperiods],
+ p_e[g, p, t] + p_out_ru[g, p, t] <= max_gen[g]) # Maximum output dispatch
+
+ JuMP.@constraint(m, min_input[g in storage_projects, p in invperiods, t in opperiods],
+ p_in[g, p, t] - p_in_ru[g, p, t] >= 0.0) # Minimum input dispatch
+
+ JuMP.@constraint(m, max_input[g in storage_projects, p in invperiods, t in opperiods],
+ p_in[g, p, t] + p_in_rd[g, p, t] <= max_input[g]) # Maximum input dispatch
+
+
+ JuMP.@constraint(m, storage_reserve_up[g in storage_projects, p in invperiods, t in opperiods],
+ p_ru[g, p, t] <= p_out_ru[g, p, t] + p_in_ru[g, p, t]) # Maximum storage reserve up
+
+ JuMP.@constraint(m, storage_reserve_down[g in storage_projects, p in invperiods, t in opperiods],
+ p_rd[g, p, t] <= p_out_rd[g, p, t] + p_in_rd[g, p, t]) # Maximum storage reserve down
+
+ JuMP.@constraint(m, min_storage_level[g in storage_projects, p in invperiods, t in opperiods],
+ storage_level[g, p, t] >= min_storage[g]) # Minimum storage level
+
+ JuMP.@constraint(m, max_storage_level[g in storage_projects, p in invperiods, t in opperiods],
+ storage_level[g, p, t] <= max_storage[g]) # Maximum storage level
+
+ JuMP.@constraint(m, end_day_storage_level[g in storage_projects, p in invperiods, t in end_of_day],
+ storage_level[g, p, t] == init_storage[g] * max_storage[g]) # Maximum storage level after reserve provision
+
+
+ println("Actual Energy and A/S Market Clearing:")
+ JuMP.optimize!(m)
+ println(JuMP.termination_status(m))
+ println(JuMP.objective_value(m))
+
+ energy_dual = JuMP.dual.(energy_market)
+ reserve_up_dual = JuMP.dual.(reserve_up_market)
+ reserve_down_dual = JuMP.dual.(reserve_down_market)
+
+ energy_price = AxisArrays.AxisArray(zeros(size(energy_dual)), zones, invperiods, opperiods)
+ reserve_up_price = AxisArrays.AxisArray(zeros(size(reserve_up_dual)), zones, invperiods, opperiods)
+ reserve_down_price = AxisArrays.AxisArray(zeros(size(reserve_down_dual)), zones, invperiods, opperiods)
+
+
+ for p in invperiods
+ for t in opperiods
+ energy_price[:, p, t] = round.((energy_dual[:, p, t] / rep_hour_weight[t]), digits = 5)
+ reserve_up_price[:, p, t] = round.((reserve_up_dual[:, p, t] / rep_hour_weight[t]), digits = 5)
+ reserve_down_price[:, p, t] = round.((reserve_down_dual[:, p, t] / rep_hour_weight[t]), digits = 5)
+ end
+ end
+
+ capacity_factor = Dict([g => zeros(length(invperiods), length(opperiods)) for g in projects])
+ reserve_up_perc = Dict([g => zeros(length(invperiods), length(opperiods)) for g in projects])
+ reserve_down_perc = Dict([g => zeros(length(invperiods), length(opperiods)) for g in projects])
+
+ for g in projects
+ for p in invperiods
+ for t in opperiods
+ capacity_factor[g][p, t] = value.(p_e[g, p, t]) / max_gen[g]
+ reserve_up_perc[g][p, t] = value.(p_ru[g, p, t]) / max_gen[g]
+ reserve_down_perc[g][p, t] = value.(p_rd[g, p, t]) / max_gen[g]
+ end
+
+ end
+ end
+
+ return energy_price, reserve_up_price, reserve_down_price, capacity_factor, reserve_up_perc, reserve_down_perc;
+
+ end
diff --git a/src/markets_simulation/expected_market_simulation.jl b/src/markets_simulation/expected_market_simulation.jl
index 0b20376..acfa7cb 100644
--- a/src/markets_simulation/expected_market_simulation.jl
+++ b/src/markets_simulation/expected_market_simulation.jl
@@ -1,94 +1,105 @@
-"""
-This function creates the expected market data for each investor for each scenario using CEM.
-"""
-function create_expected_marketdata(investor_dir::String,
- market_names::Vector{Symbol},
- carbon_tax::Vector{Float64},
- reserve_products::Vector{String},
- ordc_products::Vector{String},
- expected_portfolio::Vector{<: Project{<: BuildPhase}},
- zones::Vector{String},
- lines::Vector{ZonalLine},
- peak_load::Float64,
- rep_hour_weight::Vector{Float64},
- average_capital_cost_multiplier::Float64,
- scenario::Scenario,
- iteration_year::Int64,
- yearly_horizon::Int64,
- solver::JuMP.MOI.OptimizerWithAttributes,
- sys_results_dir::String,
- investor_name::String)
-
-
- system = create_cem_mkt_clr_problem(investor_dir,
- market_names,
- carbon_tax,
- reserve_products,
- ordc_products,
- expected_portfolio,
- zones,
- lines,
- peak_load,
- rep_hour_weight,
- average_capital_cost_multiplier,
- scenario,
- iteration_year,
- yearly_horizon)
-
- capacity_price,
- energy_price,
- reserve_price,
- rec_price,
- capacity_factors,
- total_utilization,
- capacity_accepted_perc,
- new_options = cem(
- system,
- solver,
- "C:/Users/manwar2/Documents/GitRepos/emt-tests/data/simulation_data/results/cem_results.txt"
- )
-
- output_file = joinpath(investor_dir, "expected_market_data", "$(get_name(scenario))_year_$(iteration_year).jld2")
-
- FileIO.save(output_file,
- "capacity_price", capacity_price,
- "energy_price", energy_price,
- "reserve_price", reserve_price,
- "rec_price", rec_price,
- "capacity_factors", capacity_factors,
- "total_utilization", total_utilization,
- "capacity_accepted_perc", capacity_accepted_perc,
- "new_options", new_options
- )
-
- sys_results_file = joinpath(sys_results_dir, investor_name, "expected_market_data", "$(get_name(scenario))_year_$(iteration_year).jld2")
-
- FileIO.save(sys_results_file,
- "capacity_price", capacity_price,
- "energy_price", energy_price,
- "reserve_price", reserve_price,
- "rec_price", rec_price,
- "capacity_factors", capacity_factors,
- "total_utilization", total_utilization,
- "capacity_accepted_perc", capacity_accepted_perc,
- "new_options", new_options
- )
-
- return
-end
-
-"""
-This function updates the maximum new options which can be built by the investor based on CEM predictions.
-"""
-function update_max_new_options!(max_new_options::Dict{String, Int64},
- scenario_new_options::Dict{String, Int64},
- option_projects::Vector{Project})
- for project in option_projects
- type = get_type(get_tech(project))
- zone = get_zone(get_tech(project))
- if scenario_new_options["option_$(type)_$(zone)"] > max_new_options[get_name(project)]
- max_new_options[get_name(project)] = scenario_new_options["option_$(type)_$(zone)"]
- end
- end
- return max_new_options
-end
+"""
+This function creates the expected market data for each investor for each scenario using CEM.
+"""
+function create_expected_marketdata(investor_dir::String,
+ market_names::Vector{Symbol},
+ carbon_tax::Vector{Float64},
+ reserve_products::Vector{String},
+ ordc_products::Vector{String},
+ rps_target::String,
+ reserve_penalty::String,
+ resource_adequacy::ResourceAdequacy,
+ irm_scalar::Float64,
+ expected_portfolio::Vector{<: Project{<: BuildPhase}},
+ zones::Vector{String},
+ lines::Vector{ZonalLine},
+ peak_load::Float64,
+ rep_hour_weight::Vector{Float64},
+ average_capital_cost_multiplier::Float64,
+ scenario::Scenario,
+ iteration_year::Int64,
+ yearly_horizon::Int64,
+ solver::JuMP.MOI.OptimizerWithAttributes,
+ sys_results_dir::String,
+ investor_name::String)
+
+
+ system = create_cem_mkt_clr_problem(investor_dir,
+ market_names,
+ carbon_tax,
+ reserve_products,
+ ordc_products,
+ rps_target,
+ reserve_penalty,
+ resource_adequacy,
+ irm_scalar,
+ expected_portfolio,
+ zones,
+ lines,
+ peak_load,
+ rep_hour_weight,
+ average_capital_cost_multiplier,
+ scenario,
+ iteration_year,
+ yearly_horizon)
+
+ capacity_price,
+ energy_price,
+ reserve_price,
+ rec_price,
+ inertia_price,
+ capacity_factors,
+ total_utilization,
+ capacity_accepted_perc,
+ new_options = cem(
+ system,
+ solver,
+ "C:/Users/manwar2/Documents/GitRepos/emt-tests/data/simulation_data/results/cem_results.txt"
+ )
+
+ output_file = joinpath(investor_dir, "expected_market_data", "$(get_name(scenario))_year_$(iteration_year).jld2")
+
+ FileIO.save(output_file,
+ "capacity_price", capacity_price,
+ "energy_price", energy_price,
+ "reserve_price", reserve_price,
+ "rec_price", rec_price,
+ "inertia_price", inertia_price,
+ "capacity_factors", capacity_factors,
+ "total_utilization", total_utilization,
+ "capacity_accepted_perc", capacity_accepted_perc,
+ "new_options", new_options
+ )
+
+ sys_results_file = joinpath(sys_results_dir, investor_name, "expected_market_data", "$(get_name(scenario))_year_$(iteration_year).jld2")
+
+ FileIO.save(sys_results_file,
+ "capacity_price", capacity_price,
+ "energy_price", energy_price,
+ "reserve_price", reserve_price,
+ "rec_price", rec_price,
+ "inertia_price", inertia_price,
+ "capacity_factors", capacity_factors,
+ "total_utilization", total_utilization,
+ "capacity_accepted_perc", capacity_accepted_perc,
+ "new_options", new_options
+ )
+
+ return
+end
+
+"""
+This function updates the maximum new options which can be built by the investor based on CEM predictions.
+"""
+function update_max_new_options!(max_new_options::Dict{String, Int64},
+ scenario_new_options::Dict{String, Int64},
+ option_projects::Vector{Project})
+ for project in option_projects
+ type = get_type(get_tech(project))
+ zone = get_zone(get_tech(project))
+ if scenario_new_options["option_$(type)_$(zone)"] > max_new_options[get_name(project)]
+ max_new_options[get_name(project)] = scenario_new_options["option_$(type)_$(zone)"]
+ end
+ end
+ return max_new_options
+end
diff --git a/src/markets_simulation/ordc_construction/error_distributions.jl b/src/markets_simulation/ordc_construction/error_distributions.jl
index c90079d..b7e5d08 100644
--- a/src/markets_simulation/ordc_construction/error_distributions.jl
+++ b/src/markets_simulation/ordc_construction/error_distributions.jl
@@ -1,148 +1,190 @@
-"""
-This function constructs the net load forecast error distribution based on Day-Ahead and Real-Time data.
-"""
-function construct_net_load_forecast_error_distribution(simulation_dir::String,
- renewable_generators::Vector{Project},
- months::Vector{Int64},
- hours::Vector{Int64},
- zonal::Bool)
- load_n_vg_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data.csv"))
- load_n_vg_df_rt = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data_rt.csv"))
- zones = chop.(filter(n -> occursin("load", n), names(load_n_vg_df)), head = 5, tail = 0)
-
- num_rt_intervals = round(Int, DataFrames.nrow(load_n_vg_df_rt)/DataFrames.nrow(load_n_vg_df))
-
- rt_periods = find_rt_periods(hours, num_rt_intervals)
- filter!(row -> in(row["Month"], months) && in(row["Period"], hours), load_n_vg_df)
-
- filter!(row -> in(row["Month"], months) && in(row["Period"], rt_periods), load_n_vg_df_rt)
-
- #half hourly forecasts
- filter!(row -> row["Period"] % (num_rt_intervals/2) == 0, load_n_vg_df_rt)
-
- load_n_vg_df_extrap = repeat(load_n_vg_df; inner = 2, outer = 1)
- load_n_vg_df_extrap[:, "Period"] = load_n_vg_df_rt[:, "Period"]
-
- net_load_forecast_error = load_n_vg_df_extrap[:, 1:4]
-
- if zonal
-
- zonal_gens = Dict{String, Vector{Project}}()
- error_mean = Dict{String, Float64}()
- error_std = Dict{String, Float64}()
-
- for zone in zones
- zonal_gens[zone] = filter(p -> get_zone(get_tech(p)) == zone, renewable_generators)
-
- total_gen_rt = zeros(DataFrames.nrow(load_n_vg_df_rt))
- total_gen_extrap = zeros(DataFrames.nrow(load_n_vg_df_extrap))
- for gen in get_name.(zonal_gens[zone])
- total_gen_rt += load_n_vg_df_rt[:, gen]
- total_gen_extrap += load_n_vg_df_extrap[:, gen]
- end
-
- load_n_vg_df_rt[:, "Total Generation_$(zone)"] = total_gen_rt
- load_n_vg_df_extrap[:, "Total Generation_$(zone)"] = total_gen_extrap
-
- load_n_vg_df_rt[:, "Net Load_$(zone)"] = load_n_vg_df_rt[:, "load_$(zone)"] - total_gen_rt
- load_n_vg_df_extrap[:, "Net Load_$(zone)"] = load_n_vg_df_extrap[:, "load_$(zone)"] - total_gen_extrap
-
- net_load_forecast_error[:, zone] = load_n_vg_df_rt[:, "Net Load_$(zone)"] - load_n_vg_df_extrap[:, "Net Load_$(zone)"]
-
- error_mean[zone] = Statistics.mean(net_load_forecast_error[:, zone])
- error_std[zone] = Statistics.std(net_load_forecast_error[:, zone])
- end
- else
- total_gen_rt = zeros(DataFrames.nrow(load_n_vg_df_rt))
- total_gen_extrap = zeros(DataFrames.nrow(load_n_vg_df_extrap))
- total_load_rt = zeros(DataFrames.nrow(load_n_vg_df_extrap))
- total_load_extrap = zeros(DataFrames.nrow(load_n_vg_df_extrap))
-
- for gen in get_name.(renewable_generators)
- total_gen_rt += load_n_vg_df_rt[:, gen]
- total_gen_extrap += load_n_vg_df_extrap[:, gen] * 2/3 + load_n_vg_df_rt[:, gen] * 1/3 # blended forecasts
- end
-
- for zone in zones
- total_load_rt += load_n_vg_df_rt[:, "load_$(zone)"]
- total_load_extrap += load_n_vg_df_extrap[:, "load_$(zone)"] * 2/3 + load_n_vg_df_rt[:, "load_$(zone)"] * 1/3 # blended forecasts
- end
-
- load_n_vg_df_rt[:, "Total Generation"] = total_gen_rt
- load_n_vg_df_extrap[:, "Total Generation"] = total_gen_extrap
-
- load_n_vg_df_rt[:, "Net Load"] = total_load_rt - total_gen_rt
- load_n_vg_df_extrap[:, "Net Load"] = total_load_extrap - total_gen_extrap
-
-
- net_load_forecast_error[:, "Total"] = load_n_vg_df_rt[:, "Net Load"] - load_n_vg_df_extrap[:, "Net Load"]
-
- error_mean = Statistics.mean(net_load_forecast_error[:, "Total"])
- error_std = Statistics.std(net_load_forecast_error[:, "Total"])
- end
-
- return error_mean, error_std
-
-end
-
-
-"""
-This function constructs the generator unavailability distribution based on Forced Outage Rates.
-"""
-function construct_gen_unavail_distribution(simulation_dir::String,
- generators::Vector{Project},
- zonal::Bool)
-
- load_n_vg_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data.csv"))
- zones = chop.(filter(n -> occursin("load", n), names(load_n_vg_df)), head = 5, tail = 0)
-
- if zonal
- unavail_mean = Dict{String, Float64}()
- unavail_std = Dict{String, Float64}()
- for zone in zones
- gens = filter(p -> get_zone(get_tech(p)) == zone, generators)
- capacity_vector = round.(Int, get_maxcap.(gens)) #PRAS needs integer capacities
- FOR_vector = get_FOR.(get_tech.(gens))
-
- distribution = PRAS.ResourceAdequacy.spconv(capacity_vector, FOR_vector)
-
- unavail_mean[zone] = Distributions.mean(distribution)
- unavail_std[zone] = sqrt(Distributions.var(distribution))
- end
- else
- capacity_vector = round.(Int, get_maxcap.(generators)) #PRAS needs integer capacities
- FOR_vector = get_FOR.(get_tech.(generators))
-
- distribution = spconv(capacity_vector, FOR_vector)
-
- unavail_mean = Distributions.mean(distribution)
- unavail_std = sqrt(Distributions.var(distribution))
- end
-
- return unavail_mean, unavail_std
-end
-
-function calculate_min_reserve_req(simulation_dir::String,
- generators::Vector{Project},
- MRR_scale::Real,
- zonal::Bool)
-
- load_n_vg_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data.csv"))
- zones = chop.(filter(n -> occursin("load", n), names(load_n_vg_df)), head = 5, tail = 0)
-
- thermal_generators = filter(p -> typeof(p) == ThermalGenEMIS{Existing}, generators)
-
- if zonal
- req = Dict{String, Float64}()
-
- for zone in zones
- largest_gen = maximum(get_maxcap.(filter(p -> get_zone(get_tech(p)) == zone, thermal_generators)))
- req[zone] = largest_gen * MRR_scale
- end
- else
- largest_gen = maximum(get_maxcap.(thermal_generators))
- req = largest_gen * MRR_scale
- end
-
- return req
-end
+"""
+This function constructs the net load forecast error distribution based on Day-Ahead and Real-Time data.
+"""
+function construct_net_load_forecast_error_distribution(simulation_dir::String,
+ renewable_generators::Vector{Project},
+ months::Vector{Int64},
+ hours::Vector{Int64},
+ zonal::Bool)
+ load_n_vg_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data.csv"))
+ load_n_vg_df_rt = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data_rt.csv"))
+ zones = chop.(filter(n -> occursin("load", n), names(load_n_vg_df)), head = 5, tail = 0)
+
+ num_rt_intervals = round(Int, DataFrames.nrow(load_n_vg_df_rt)/DataFrames.nrow(load_n_vg_df))
+
+ rt_periods = find_rt_periods(hours, num_rt_intervals)
+ filter!(row -> in(row["Month"], months) && in(row["Period"], hours), load_n_vg_df)
+
+ filter!(row -> in(row["Month"], months) && in(row["Period"], rt_periods), load_n_vg_df_rt)
+
+ #half hourly forecasts
+ filter!(row -> row["Period"] % (num_rt_intervals/2) == 0, load_n_vg_df_rt)
+
+ # load_n_vg_df_extrap = repeat(load_n_vg_df; inner = 2, outer = 1)
+ # load_n_vg_df_extrap[:, "Period"] = load_n_vg_df_rt[:, "Period"]
+ load_n_vg_df_extrap = load_n_vg_df
+
+ net_load_forecast_error = load_n_vg_df_extrap[:, 1:4]
+
+ if zonal
+
+ zonal_gens = Dict{String, Vector{Project}}()
+ error_mean = Dict{String, Float64}()
+ error_std = Dict{String, Float64}()
+
+ for zone in zones
+ zonal_gens[zone] = filter(p -> get_zone(get_tech(p)) == zone, renewable_generators)
+
+ total_gen_rt = zeros(DataFrames.nrow(load_n_vg_df_rt))
+ total_gen_extrap = zeros(DataFrames.nrow(load_n_vg_df_extrap))
+ for gen in get_name.(zonal_gens[zone])
+ total_gen_rt += load_n_vg_df_rt[:, gen]
+ total_gen_extrap += load_n_vg_df_extrap[:, gen]
+ end
+
+ load_n_vg_df_rt[:, "Total Generation_$(zone)"] = total_gen_rt
+ load_n_vg_df_extrap[:, "Total Generation_$(zone)"] = total_gen_extrap
+
+ load_n_vg_df_rt[:, "Net Load_$(zone)"] = load_n_vg_df_rt[:, "load_$(zone)"] - total_gen_rt
+ load_n_vg_df_extrap[:, "Net Load_$(zone)"] = load_n_vg_df_extrap[:, "load_$(zone)"] - total_gen_extrap
+
+ net_load_forecast_error[:, zone] = load_n_vg_df_rt[:, "Net Load_$(zone)"] - load_n_vg_df_extrap[:, "Net Load_$(zone)"]
+
+ error_mean[zone] = Statistics.mean(net_load_forecast_error[:, zone])
+ error_std[zone] = Statistics.std(net_load_forecast_error[:, zone])
+ end
+ else
+ total_gen_rt = zeros(DataFrames.nrow(load_n_vg_df_rt))
+ total_gen_extrap = zeros(DataFrames.nrow(load_n_vg_df_extrap))
+ total_load_rt = zeros(DataFrames.nrow(load_n_vg_df_extrap))
+ total_load_extrap = zeros(DataFrames.nrow(load_n_vg_df_extrap))
+
+ for gen in get_name.(renewable_generators)
+ total_gen_rt += load_n_vg_df_rt[:, gen]
+ total_gen_extrap += load_n_vg_df_extrap[:, gen] * 2/3 + load_n_vg_df_rt[:, gen] * 1/3 # blended forecasts
+ end
+
+ for zone in zones
+ total_load_rt += load_n_vg_df_rt[:, "load_$(zone)"]
+ total_load_extrap += load_n_vg_df_extrap[:, "load_$(zone)"] * 2/3 + load_n_vg_df_rt[:, "load_$(zone)"] * 1/3 # blended forecasts
+ end
+
+ load_n_vg_df_rt[:, "Total Generation"] = total_gen_rt
+ load_n_vg_df_extrap[:, "Total Generation"] = total_gen_extrap
+
+ load_n_vg_df_rt[:, "Net Load"] = total_load_rt - total_gen_rt
+ load_n_vg_df_extrap[:, "Net Load"] = total_load_extrap - total_gen_extrap
+
+
+ net_load_forecast_error[:, "Total"] = load_n_vg_df_rt[:, "Net Load"] - load_n_vg_df_extrap[:, "Net Load"]
+
+ error_mean = Statistics.mean(net_load_forecast_error[:, "Total"])
+ error_std = Statistics.std(net_load_forecast_error[:, "Total"])
+ end
+
+ return error_mean, error_std
+
+end
+
+
+"""
+This function constructs the generator unavailability distribution based on Forced Outage Rates using colvolution.
+"""
+function construct_conv_unavailabilities(simulation_dir::String,
+ generators::Vector{Project},
+ zonal::Bool,
+ ordc_unavailability_method::String)
+
+ if ordc_unavailability_method == "Convolution"
+ load_n_vg_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data.csv"))
+ zones = chop.(filter(n -> occursin("load", n), names(load_n_vg_df)), head = 5, tail = 0)
+
+ if zonal
+ unavail_mean = Dict{String, Float64}()
+ unavail_std = Dict{String, Float64}()
+ for zone in zones
+ gens = filter(p -> get_zone(get_tech(p)) == zone, generators)
+ capacity_vector = round.(Int, get_maxcap.(gens)) #PRAS needs integer capacities
+ FOR_vector = get_FOR.(get_tech.(gens))
+
+ distribution = PRAS.ResourceAdequacy.spconv(capacity_vector, FOR_vector)
+
+ unavail_mean[zone] = Distributions.mean(distribution)
+ unavail_std[zone] = sqrt(Distributions.var(distribution))
+ end
+ else
+ capacity_vector = round.(Int, get_maxcap.(generators)) #PRAS needs integer capacities
+ FOR_vector = get_FOR.(get_tech.(generators))
+
+ distribution = spconv(capacity_vector, FOR_vector)
+
+ unavail_mean = Distributions.mean(distribution)
+ unavail_std = sqrt(Distributions.var(distribution))
+ end
+ else
+ unavail_mean = nothing
+ unavail_std = nothing
+ end
+
+ return unavail_mean, unavail_std
+end
+
+"""
+This function constructs the generator unavailability distribution using PRAS unavailibility timeseries.
+"""
+function construct_gen_unavail_distribution(simulation_dir::String,
+ smc_unavailability_timeseries::Array{Float64, 2},
+ conv_unavailability_mean::Nothing,
+ conv_unavailability_std::Nothing,
+ months::Vector{Int64},
+ hours::Vector{Int64})
+
+ gen_unavail_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data.csv"))[:, 1:4]
+ @assert DataFrames.nrow(gen_unavail_df) == size(smc_unavailability_timeseries, 2)
+ for i in 1:size(smc_unavailability_timeseries, 1)
+ gen_unavail_df[!, "$(i)"] = smc_unavailability_timeseries[1, :]
+ end
+ filter!(row -> in(row["Month"], months) && in(row["Period"], hours), gen_unavail_df)
+
+ unavail_mean = Statistics.mean(Matrix(gen_unavail_df[:, 5:end]))
+ unavail_std = Statistics.std(Matrix(gen_unavail_df[:, 5:end]))
+ return unavail_mean, unavail_std
+end
+
+function construct_gen_unavail_distribution(simulation_dir::String,
+ smc_unavailability_timeseries::Nothing,
+ conv_unavailability_mean::Float64,
+ conv_unavailability_std::Float64,
+ months::Vector{Int64},
+ hours::Vector{Int64})
+
+ unavail_mean = conv_unavailability_mean
+ unavail_std = conv_unavailability_std
+ return unavail_mean, unavail_std
+end
+
+
+function calculate_min_reserve_req(simulation_dir::String,
+ generators::Vector{Project},
+ MRR_scale::Real,
+ zonal::Bool)
+
+ load_n_vg_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data.csv"))
+ zones = chop.(filter(n -> occursin("load", n), names(load_n_vg_df)), head = 5, tail = 0)
+
+ thermal_generators = filter(p -> typeof(p) == ThermalGenEMIS{Existing}, generators)
+
+ if zonal
+ req = Dict{String, Float64}()
+
+ for zone in zones
+ largest_gen = maximum(get_maxcap.(filter(p -> get_zone(get_tech(p)) == zone, thermal_generators)))
+ req[zone] = largest_gen * MRR_scale
+ end
+ else
+ largest_gen = maximum(get_maxcap.(thermal_generators))
+ req = largest_gen * MRR_scale
+ end
+
+ return req
+end
diff --git a/src/markets_simulation/ordc_construction/ordc_construction.jl b/src/markets_simulation/ordc_construction/ordc_construction.jl
index 41c5bbf..cf3b56b 100644
--- a/src/markets_simulation/ordc_construction/ordc_construction.jl
+++ b/src/markets_simulation/ordc_construction/ordc_construction.jl
@@ -1,261 +1,333 @@
-"""
-This function constructs the ORDCs for spinning and primary reserve products.
-"""
-function construct_ordc(simulation_dir::String,
- investors::Vector{Investor},
- iteration_year::Int64,
- rep_days::Dict{Dates.Date,Int64})
-
- products = split(read_data(joinpath(simulation_dir, "markets_data", "reserve_products.csv"))[1,"ordc_products"], "; ")
- println("Creating ORDC for products: $(products)")
- generators = filter(p -> in(typeof(p), leaftypes(GeneratorEMIS{Existing})), vcat(get_existing.(investors)...))
- zonal = false
-
- load_n_vg_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data.csv"))
- load_n_vg_df_rt = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data_rt.csv"))
- zones = chop.(filter(n -> occursin("load", n), names(load_n_vg_df)), head = 5, tail = 0)
-
- num_rt_intervals = round(Int, DataFrames.nrow(load_n_vg_df_rt)/DataFrames.nrow(load_n_vg_df))
-
- renewable_generators = filter(g -> typeof(g) == RenewableGenEMIS{Existing}, generators)
- unavail_mean, unavail_std = construct_gen_unavail_distribution(simulation_dir, generators, zonal)
-
- for product in products
-
- product_data = read_data(joinpath(simulation_dir, "markets_data", "$(product).csv"))
-
- MRR_scale = product_data[1,"MRR_scale"]
- penalty = product_data[1,"penalty"]
- step_size = product_data[1, "stepsize (MW)"]
- curve = product_data[1, "curve"]
-
- MRR = calculate_min_reserve_req(simulation_dir, generators, MRR_scale, zonal)
-
- println("MRR_$(product): ", MRR)
- println("Penalty_$(product): ", penalty)
-
- seasons = chop.(split(product_data[1, "seasons"], ";"), head = 1, tail = 1)
- timeblocks = chop.(split(product_data[1, "timeblocks"], ";"), head = 1, tail = 1)
-
- season_months = Dict{String, Vector{Int64}}()
- timeblock_hours = Dict{String, Vector{Int64}}()
-
- for season in seasons
- months = split(season, "-")
- start_month = month_lookup(months[1])
- end_month = month_lookup(months[2])
-
- if start_month <= end_month
- season_months[season] = collect(start_month:end_month)
- else
- season_months[season] = collect(1:end_month)
- append!(season_months[season], collect(start_month:12))
- end
- end
-
- for timeblock in timeblocks
- hours = parse.(Int64, split(timeblock, "-"))/100
- start_hour = hours[1]
- end_hour = hours[2]
-
- if start_hour <= end_hour
- timeblock_hours[timeblock] = collect(start_hour:end_hour)
- else
- timeblock_hours[timeblock] = collect(1:end_hour)
- append!(timeblock_hours[timeblock], collect(start_hour:24))
- end
- end
-
-
- ordc_df = load_n_vg_df[:, 1:4]
- ordc_df_rt = load_n_vg_df_rt[:, 1:4]
-
- if zonal
- for zone in zones
- ordc_df[:,"$(product)_$(zone)"] = Vector{Vector{Tuple{Float64, Float64}}}(undef, DataFrames.nrow(ordc_df))
- ordc_df_rt[:,"$(product)_$(zone)"] = Vector{Vector{Tuple{Float64, Float64}}}(undef, DataFrames.nrow(ordc_df_rt))
- end
- else
- ordc_df[:,"$(product)"] = Vector{Vector{Tuple{Float64, Float64}}}(undef, DataFrames.nrow(ordc_df))
- ordc_df_rt[:,"$(product)"] = Vector{Vector{Tuple{Float64, Float64}}}(undef, DataFrames.nrow(ordc_df_rt))
- end
-
- for months_key in keys(season_months)
- months = season_months[months_key]
- for hours_key in keys(timeblock_hours)
- hours = timeblock_hours[hours_key]
- rt_periods = find_rt_periods(hours, num_rt_intervals)
-
- error_mean, error_var = construct_net_load_forecast_error_distribution(simulation_dir, renewable_generators, months, hours, zonal)
- if zonal
- aggregate_distribution = Dict{String, Distributions.Normal}()
-
- for zone in zones
- aggregate_distribution_mean = error_mean[zone] + unavail_mean[zone]
- aggregate_distribution_std = sqrt(error_mean[zone]^2 + unavail_mean[zone]^2)
- aggregate_distribution[zone] = Distributions.Normal(aggregate_distribution_mean, aggregate_distribution_std)
-
- three_std = aggregate_distribution_mean + 3 * aggregate_distribution_std
- maximum_error = round(ceil(three_std/step_size))*step_size ## Round to the next step_size
-
- initial_points = [(0.0, Float64(penalty)), (MRR[zone], Float64(penalty))]
-
- ordc_points = [((step * step_size) + MRR[zone],
- (1 - Distributions.cdf(aggregate_distribution[zone], step * step_size)) * penalty)
- for step in 1:Int64(maximum_error/step_size)]
-
- ordc_points = append!(initial_points, ordc_points)
-
- end
-
- for month in months
- for hour in hours
- rows = findall((ordc_df.Month .== month) .& (ordc_df.Period .== hour))
- for row in rows
- ordc_df[row, "$(product)_$(zone)"] = ordc_points
- end
- end
-
- for rt_period in rt_periods
- rows = findall((ordc_df_rt.Month .== month) .& (ordc_df_rt.Period .== rt_period))
- for row in rows
- ordc_df_rt[row, "$(product)_$(zone)"] = ordc_points
- end
- end
- end
-
- else
- aggregate_distribution_mean = error_mean + unavail_mean
- aggregate_distribution_std = sqrt(error_mean^2 + unavail_mean^2)
-
- aggregate_distribution = Distributions.Normal(aggregate_distribution_mean, aggregate_distribution_std)
-
- three_std = aggregate_distribution_mean + 3*aggregate_distribution_std
- maximum_error = round(ceil(three_std/step_size))*step_size ## Round to the next step_size MW
-
- initial_points = [(0.0, Float64(penalty)), (MRR, Float64(penalty))]
-
- n_steps = 10
-
- step_size = maximum_error/n_steps
-
- ordc_points = [((step * step_size) + MRR,
- (1 - Distributions.cdf(aggregate_distribution, step * step_size)) * penalty)
- for step in 1:n_steps]
- if curve
- ordc_points = append!(initial_points, ordc_points)
- end
-
- for month in months
- for hour in hours
- rows = findall((ordc_df.Month .== month) .& (ordc_df.Period .== hour))
- for row in rows
- ordc_df[row, "$(product)"] = ordc_points
- end
- end
-
- for rt_period in rt_periods
- rows = findall((ordc_df_rt.Month .== month) .& (ordc_df_rt.Period .== rt_period))
- for row in rows
- ordc_df_rt[row, "$(product)"] = ordc_points
- end
- end
- end
-
- end
- end
- end
-
- write_data(joinpath(simulation_dir, "timeseries_data_files", "Reserves"), "$(product)_$(iteration_year).csv", ordc_df)
- write_data(joinpath(simulation_dir, "timeseries_data_files", "Reserves"), "$(product)_REAL_TIME_$(iteration_year).csv", ordc_df_rt)
-
- rep_ordc_df = filter(row -> in(Dates.Date(row[:Year], row[:Month], row[:Day]), keys(rep_days)), ordc_df)
-
- write_data(joinpath(simulation_dir, "timeseries_data_files", "Reserves"), "rep_$(product)_$(iteration_year).csv", rep_ordc_df)
- end
- return
-end
-
-function add_psy_ordc!(simulation_dir::String,
- markets_dict::Dict{Symbol, Bool},
- sys::Nothing,
- type::String)
- return
-end
-
-function add_psy_ordc!(simulation_dir::String,
- markets_dict::Dict{Symbol, Bool},
- sys::PSY.System,
- type::String
- )
-
- products = split(read_data(joinpath(simulation_dir, "markets_data", "reserve_products.csv"))[1,"ordc_products"], "; ")
- da_products = split(read_data(joinpath(simulation_dir, "markets_data", "reserve_products.csv"))[1,"da_products"], "; ")
- rt_products = split(read_data(joinpath(simulation_dir, "markets_data", "reserve_products.csv"))[1,"rt_products"], "; ")
-
- for product in products
- if markets_dict[Symbol(product)]
- product_data = read_data(joinpath(simulation_dir, "markets_data", "$(product).csv"))
- eligible_categories = product_data[1, "eligible categories"]
-
- ####### Adding ORDC reserve
- reserve = PSY.ReserveDemandCurve{PSY.ReserveUp}(
- nothing,
- product,
- true,
- product_data[1, "timescale (min)"],
- )
-
- PSY.add_service!(sys, reserve, PSY.get_components(PSY.ThermalStandard, sys))
-
- if occursin("Hydro", eligible_categories)
- for component in PSY.get_components(PSY.HydroDispatch, sys)
- PSY.add_service!(component, reserve, sys)
- end
- for component in PSY.get_components(PSY.HydroEnergyReservoir, sys)
- PSY.add_service!(component, reserve, sys)
- end
- end
- if occursin("Wind", eligible_categories)
- for component in PSY.get_components(PSY.RenewableDispatch, sys)
- PSY.add_service!(component, reserve, sys)
- end
- end
- if occursin("Battery", eligible_categories)
- for component in PSY.get_components(PSY.GenericBattery, sys)
- PSY.add_service!(component, reserve_UC, sys)
- end
- end
-
-
- time_stamps = TS.timestamp(PSY.get_data(PSY.get_time_series(
- PSY.SingleTimeSeries,
- first(PSY.get_components(PSY.ElectricLoad, sys)),
- "max_active_power"
- )))
-
- if type == "UC"
- product_da_ts_raw = read_data(joinpath(simulation_dir, "timeseries_data_files", "Reserves", "$(product)_0.csv"))[:, product]
- elseif type == "ED"
- product_da_ts_raw = read_data(joinpath(simulation_dir, "timeseries_data_files", "Reserves", "$(product)_REAL_TIME_0.csv"))[:, product]
- else
- error("Type should be either DA or ED")
- end
-
- T = length(product_da_ts_raw)
-
- product_da_ts = Vector{Vector{Tuple{Float64, Float64}}}(undef, T)
-
- for t = 1:T
- tuples = split.(chop.(split(chop(product_da_ts_raw[t], head = 1, tail = 2), "), "), head = 1, tail = 0), ", ")
- product_da_ts[t] = [(parse.(Float64, tuple)[2], parse.(Float64, tuple)[1]) for tuple in tuples]
- end
-
- ts = TS.TimeArray(time_stamps, product_da_ts);
- forecast = PSY.SingleTimeSeries("variable_cost", ts)
- PSY.set_variable_cost!(sys, reserve, forecast)
- end
- end
-
- return
-end
+"""
+This function constructs the ORDCs for spinning and primary reserve products.
+"""
+function construct_ordc(sys::PSY.System,
+ simulation_dir::String,
+ investors::Vector{Investor},
+ iteration_year::Int64,
+ rep_days::Dict{Dates.Date,Int64},
+ ordc_curved::Bool,
+ ordc_unavailability_method::String,
+ reserve_penalty::String)
+
+ products = split(read_data(joinpath(simulation_dir, "markets_data", "reserve_products.csv"))[1,"ordc_products"], "; ")
+ println("Creating ORDC for products: $(products)")
+ generators = filter(p -> in(typeof(p), leaftypes(GeneratorEMIS{Existing})), vcat(get_existing.(investors)...))
+ zonal = false
+
+ smc_unavailability_timeseries = construct_smc_unavailabilities(sys, ordc_unavailability_method)
+ conv_unavail_mean, conv_unavail_std = construct_conv_unavailabilities(simulation_dir, generators, zonal, ordc_unavailability_method)
+
+ load_n_vg_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data.csv"))
+ load_n_vg_df_rt = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data_rt.csv"))
+ zones = chop.(filter(n -> occursin("load", n), names(load_n_vg_df)), head = 5, tail = 0)
+
+
+ num_rt_intervals = round(Int, DataFrames.nrow(load_n_vg_df_rt)/DataFrames.nrow(load_n_vg_df))
+
+ renewable_generators = filter(g -> typeof(g) == RenewableGenEMIS{Existing}, generators)
+
+ for product in products
+
+ product_data = read_data(joinpath(simulation_dir, "markets_data", "$(reserve_penalty)_reserve_penalty", "$(product).csv"))
+
+ MRR_scale = product_data[1,"MRR_scale"]
+ penalty = product_data[1,"penalty"]
+ step_size = product_data[1, "stepsize (MW)"]
+
+ MRR = calculate_min_reserve_req(simulation_dir, generators, MRR_scale, zonal)
+
+ println("MRR_$(product): ", MRR)
+ println("Penalty_$(product): ", penalty)
+
+ seasons = chop.(split(product_data[1, "seasons"], ";"), head = 1, tail = 1)
+ timeblocks = chop.(split(product_data[1, "timeblocks"], ";"), head = 1, tail = 1)
+
+ season_months = Dict{String, Vector{Int64}}()
+ timeblock_hours = Dict{String, Vector{Int64}}()
+
+ for season in seasons
+ months = split(season, "-")
+ start_month = month_lookup(months[1])
+ end_month = month_lookup(months[2])
+
+ if start_month <= end_month
+ season_months[season] = collect(start_month:end_month)
+ else
+ season_months[season] = collect(1:end_month)
+ append!(season_months[season], collect(start_month:12))
+ end
+ end
+
+ for timeblock in timeblocks
+ hours = parse.(Int64, split(timeblock, "-"))/100
+ start_hour = hours[1]
+ end_hour = hours[2]
+
+ if start_hour <= end_hour
+ timeblock_hours[timeblock] = collect(start_hour:end_hour)
+ else
+ timeblock_hours[timeblock] = collect(1:end_hour)
+ append!(timeblock_hours[timeblock], collect(start_hour:24))
+ end
+ end
+
+
+ ordc_df = load_n_vg_df[:, 1:4]
+ ordc_df_rt = load_n_vg_df_rt[:, 1:4]
+ #Need code fix to work with zonal ORDC markets
+ if zonal
+ for zone in zones
+ ordc_df[:,"$(product)_$(zone)"] = Vector{Vector{Tuple{Float64, Float64}}}(undef, DataFrames.nrow(ordc_df))
+ ordc_df_rt[:,"$(product)_$(zone)"] = Vector{Vector{Tuple{Float64, Float64}}}(undef, DataFrames.nrow(ordc_df_rt))
+ end
+ else
+ ordc_df[:,"$(product)"] = Vector{Vector{Tuple{Float64, Float64}}}(undef, DataFrames.nrow(ordc_df))
+ ordc_df_rt[:,"$(product)"] = Vector{Vector{Tuple{Float64, Float64}}}(undef, DataFrames.nrow(ordc_df_rt))
+ end
+
+ for months_key in keys(season_months)
+ months = season_months[months_key]
+ for hours_key in keys(timeblock_hours)
+ hours = timeblock_hours[hours_key]
+ rt_periods = find_rt_periods(hours, num_rt_intervals)
+
+ error_mean, error_var = construct_net_load_forecast_error_distribution(simulation_dir, renewable_generators, months, hours, zonal)
+ unavail_mean, unavail_std = construct_gen_unavail_distribution(simulation_dir, smc_unavailability_timeseries, conv_unavail_mean, conv_unavail_std, months, hours)
+
+ if zonal
+ aggregate_distribution = Dict{String, Distributions.Normal}()
+
+ for zone in zones
+ aggregate_distribution_mean = error_mean[zone] + unavail_mean[zone]
+ aggregate_distribution_std = sqrt(error_var[zone] + unavail_std[zone]^2)
+ aggregate_distribution[zone] = Distributions.Normal(aggregate_distribution_mean, aggregate_distribution_std)
+
+ three_std = aggregate_distribution_mean + 3 * aggregate_distribution_std
+ maximum_error = round(ceil(three_std/step_size))*step_size ## Round to the next step_size
+
+ initial_points = [(0.0, Float64(penalty)), (MRR[zone], Float64(penalty))]
+
+ ordc_points = [((step * step_size) + MRR[zone],
+ (1 - Distributions.cdf(aggregate_distribution[zone], step * step_size)) * penalty)
+ for step in 1:Int64(maximum_error/step_size)]
+
+ if ordc_curved
+ ordc_points = append!(initial_points, ordc_points)
+ else
+ ordc_points = initial_points
+ end
+
+ end
+
+ for month in months
+ for hour in hours
+ rows = findall((ordc_df.Month .== month) .& (ordc_df.Period .== hour))
+ for row in rows
+ ordc_df[row, "$(product)_$(zone)"] = ordc_points
+ end
+ end
+
+ for rt_period in rt_periods
+ rows = findall((ordc_df_rt.Month .== month) .& (ordc_df_rt.Period .== rt_period))
+ for row in rows
+ ordc_df_rt[row, "$(product)_$(zone)"] = ordc_points
+ end
+ end
+ end
+
+ else
+ aggregate_distribution_mean = error_mean + unavail_mean
+ aggregate_distribution_std = sqrt(error_var + unavail_std^2)
+
+ aggregate_distribution = Distributions.Normal(aggregate_distribution_mean, aggregate_distribution_std)
+
+ three_std = aggregate_distribution_mean + 3*aggregate_distribution_std
+ maximum_error = round(ceil(three_std/step_size))*step_size ## Round to the next step_size MW
+
+ initial_points = [(0.0, Float64(penalty)), (MRR, Float64(penalty))]
+
+ n_steps = 10
+
+ step_size = maximum_error/n_steps
+
+ ordc_points = [((step * step_size) + MRR,
+ (1 - Distributions.cdf(aggregate_distribution, step * step_size)) * penalty)
+ for step in 1:n_steps]
+ if ordc_curved
+ ordc_points = append!(initial_points, ordc_points)
+ else
+ ordc_points = initial_points
+ end
+
+ for month in months
+ for hour in hours
+ rows = findall((ordc_df.Month .== month) .& (ordc_df.Period .== hour))
+ for row in rows
+ ordc_df[row, "$(product)"] = ordc_points
+ end
+ end
+
+ for rt_period in rt_periods
+ rows = findall((ordc_df_rt.Month .== month) .& (ordc_df_rt.Period .== rt_period))
+ for row in rows
+ ordc_df_rt[row, "$(product)"] = ordc_points
+ end
+ end
+ end
+
+ end
+ end
+ end
+
+ write_data(joinpath(simulation_dir, "timeseries_data_files", "Reserves"), "$(product)_$(iteration_year).csv", ordc_df)
+ write_data(joinpath(simulation_dir, "timeseries_data_files", "Reserves"), "$(product)_REAL_TIME_$(iteration_year).csv", ordc_df_rt)
+
+ rep_ordc_df = filter(row -> in(Dates.Date(row[:Year], row[:Month], row[:Day]), keys(rep_days)), ordc_df)
+
+ write_data(joinpath(simulation_dir, "timeseries_data_files", "Reserves"), "rep_$(product)_$(iteration_year).csv", rep_ordc_df)
+ end
+ return
+end
+
+function process_ordc_data_for_siip(raw_data::Union{Vector{String}, PooledArrays.PooledVector{String, UInt32, Vector{UInt32}}})
+ T = length(raw_data)
+
+ product_da_ts = Vector{Vector{Tuple{Float64, Float64}}}(undef, T)
+
+ # for t = 1:T
+ # tuples = split.(chop.(split(chop(raw_data[t], head = 1, tail = 2), "), "), head = 1, tail = 0), ", ")
+ # product_da_ts[t] = [(parse.(Float64, tuple)[2], parse.(Float64, tuple)[1]) for tuple in tuples]
+ # l = length(product_da_ts[t])
+ # if l > 2
+ # for s in 3:2:(l * 2) - 2
+ # temp_1 = copy(product_da_ts[t][1:(s-1)])
+ # temp_2 = copy(product_da_ts[t][s:end])
+ # push!(temp_1, (product_da_ts[t][s][1], product_da_ts[t][s-1][2]))
+ # new = vcat(temp_1, temp_2)
+ # product_da_ts[t] = copy(new)
+ # end
+ # end
+ # end
+
+ # reconstruct the ORDC curve to be (total $, total quantity) pair
+ for t = 1:T
+ tuples = split.(chop.(split(chop(raw_data[t], head = 1, tail = 2), "), "), head = 1, tail = 0), ", ")[2:end]
+ product_da_ts[t] = [(parse.(Float64, tuple)[2], parse.(Float64, tuple)[1]) for tuple in tuples]
+ l = length(product_da_ts[t])
+ for i in 1:l
+ if i == 1
+ product_da_ts[t][i] = (product_da_ts[t][i][1] * product_da_ts[t][i][2], product_da_ts[t][i][2])
+ else
+ product_da_ts[t][i] = (product_da_ts[t][i-1][1] + product_da_ts[t][i][1] * (product_da_ts[t][i][2] - product_da_ts[t][i-1][2]), product_da_ts[t][i][2])
+ end
+ end
+ end
+
+ return product_da_ts
+end
+
+function add_psy_ordc!(simulation_dir::String,
+ markets_dict::Dict{Symbol, Bool},
+ sys::Nothing,
+ type::String,
+ iteration_year::Int64,
+ da_resolution::Int64,
+ rt_resolution::Int64,
+ reserve_penalty::String)
+ return
+end
+
+function add_psy_ordc!(simulation_dir::String,
+ markets_dict::Dict{Symbol, Bool},
+ sys::PSY.System,
+ type::String,
+ iteration_year::Int64,
+ da_resolution::Int64,
+ rt_resolution::Int64,
+ reserve_penalty::String
+ )
+
+ products = split(read_data(joinpath(simulation_dir, "markets_data", "reserve_products.csv"))[1,"ordc_products"], "; ")
+ da_products = split(read_data(joinpath(simulation_dir, "markets_data", "reserve_products.csv"))[1,"da_products"], "; ")
+ rt_products = split(read_data(joinpath(simulation_dir, "markets_data", "reserve_products.csv"))[1,"rt_products"], "; ")
+
+ for product in products
+ if markets_dict[Symbol(product)]
+ product_data = read_data(joinpath(simulation_dir, "markets_data", "$(reserve_penalty)_reserve_penalty", "$(product).csv"))
+ eligible_categories = product_data[1, "eligible categories"]
+
+ ####### Adding ORDC reserve
+ reserve = PSY.ReserveDemandCurve{PSY.ReserveUp}(
+ nothing, #InfrastructureSystems.TimeSeriesKey
+ product,
+ true,
+ product_data[1, "timescale (min)"] * 60,
+ )
+
+ PSY.add_service!(sys, reserve, PSY.get_components(PSY.ThermalStandard, sys))
+
+ # for component in PSY.get_components(PSYE.ThermalCleanEnergy, sys)
+ # PSY.add_service!(component, reserve, sys)
+ # end
+
+ for component in PSY.get_components(ThermalFastStartSIIP, sys)
+ PSY.add_service!(component, reserve, sys)
+ end
+
+ if occursin("Hydro", eligible_categories)
+ for component in PSY.get_components(PSY.HydroDispatch, sys)
+ PSY.add_service!(component, reserve, sys)
+ end
+ for component in PSY.get_components(PSY.HydroEnergyReservoir, sys)
+ PSY.add_service!(component, reserve, sys)
+ end
+ end
+ if occursin("Wind", eligible_categories)
+ for component in PSY.get_components(PSY.RenewableDispatch, sys)
+ PSY.add_service!(component, reserve, sys)
+ end
+ end
+ if occursin("Battery", eligible_categories)
+ for component in PSY.get_components(PSY.GenericBattery, sys)
+ PSY.add_service!(component, reserve, sys)
+ end
+ end
+
+ # time_stamps = TS.timestamp(PSY.get_data(PSY.get_time_series(
+ # PSY.SingleTimeSeries,
+ # first(PSY.get_components(PSY.ElectricLoad, sys)),
+ # "max_active_power"
+ # )))
+
+ if type == "UC"
+ time_stamps = StepRange(Dates.DateTime("2018-01-01T00:00:00"), Dates.Hour(1), Dates.DateTime("2019-01-01T11:00:00"));
+ product_ts_raw = read_data(joinpath(simulation_dir, "timeseries_data_files", "Reserves", "$(product)_$(iteration_year - 1).csv"))[:, product]
+ product_data_ts = process_ordc_data_for_siip(product_ts_raw)
+ product_data_ts = [product_data_ts;product_data_ts[1:12]]
+ elseif type == "ED"
+ time_stamps = StepRange(Dates.DateTime("2018-01-01T00:00:00"), Dates.Hour(1), Dates.DateTime("2019-01-01T00:00:00"));
+ product_ts_raw = read_data(joinpath(simulation_dir, "timeseries_data_files", "Reserves", "$(product)_REAL_TIME_$(iteration_year - 1).csv"))[:, product]
+ product_data_ts = process_ordc_data_for_siip(product_ts_raw)
+ product_data_ts = [product_data_ts;product_data_ts[[1]]]
+ else
+ error("Type should be UC or ED")
+ end
+ forecast = PSY.SingleTimeSeries("variable_cost", TimeSeries.TimeArray(time_stamps, product_data_ts))
+ PSY.add_time_series!(sys, reserve, forecast)
+ key = IS.TimeSeriesKey(forecast)
+ PSY.set_variable!(reserve, key)
+ end
+ end
+
+ return
+end
+
+#=
+system = SystemModel("../PLEXOS2PRAS/test/rts/rts_interfaces.pras")
+nsamples = 100
+
+unavailable = unavailabilities(system, nsamples) # samples x timesteps
+mus = vec(mean(unavailable, dims=1))
+sigmas = vec(std(unavailable, dims=1))
+=#
diff --git a/src/markets_simulation/ordc_construction/ordc_market_creator.jl b/src/markets_simulation/ordc_construction/ordc_market_creator.jl
index 8c05a92..6c68db7 100644
--- a/src/markets_simulation/ordc_construction/ordc_market_creator.jl
+++ b/src/markets_simulation/ordc_construction/ordc_market_creator.jl
@@ -1,23 +1,23 @@
-"""
-This function creates the ReserveORDCMarket struct
-based on the vector of point tuples
-"""
-function create_ordc_market(points::Union{Vector{String}, PooledArrays.PooledArray{String,UInt32,1,Array{UInt32,1}}}, parameters::DataFrames.DataFrame, eligible_products::Vector{String})
- T = length(points)
- break_points = AxisArrays.AxisArray([Vector{Float64}() for t in 1:T], (1:T))
- price_points = AxisArrays.AxisArray([Vector{Float64}() for t in 1:T], (1:T))
-
- for t = 1:T
- tuples = split.(chop.(split(chop(points[t], head = 1, tail = 2), "), "), head = 1, tail = 0), ", ")
- break_points[t] = [parse.(Float64, tuple)[1] for tuple in tuples]
- price_points[t] = [parse.(Float64, tuple)[2] for tuple in tuples]
- end
-
- zones = ["zone_$(n)" for n in split(parameters[1, "eligible_zones"], ";")]
-
- stepped = parameters[1, "stepped"]
-
- ordc_market = ReserveORDCMarket(break_points, price_points, zones, eligible_products, stepped)
-
- return ordc_market
-end
+"""
+This function creates the ReserveORDCMarket struct
+based on the vector of point tuples
+"""
+function create_ordc_market(points::Union{Vector{String}, PooledArrays.PooledArray{String,UInt32,1,Array{UInt32,1}}}, parameters::DataFrames.DataFrame, eligible_products::Vector{String})
+ T = length(points)
+ break_points = AxisArrays.AxisArray([Vector{Float64}() for t in 1:T], (1:T))
+ price_points = AxisArrays.AxisArray([Vector{Float64}() for t in 1:T], (1:T))
+
+ for t = 1:T
+ tuples = split.(chop.(split(chop(points[t], head = 1, tail = 2), "), "), head = 1, tail = 0), ", ")
+ break_points[t] = [parse.(Float64, tuple)[1] for tuple in tuples]
+ price_points[t] = [parse.(Float64, tuple)[2] for tuple in tuples]
+ end
+
+ zones = ["zone_$(n)" for n in split(parameters[1, "eligible_zones"], ";")]
+
+ stepped = parameters[1, "stepped"]
+
+ ordc_market = ReserveORDCMarket(break_points, price_points, zones, eligible_products, stepped)
+
+ return ordc_market
+end
diff --git a/src/markets_simulation/siip_simulation_definition.jl b/src/markets_simulation/siip_simulation_definition.jl
index 0633622..1a758c4 100644
--- a/src/markets_simulation/siip_simulation_definition.jl
+++ b/src/markets_simulation/siip_simulation_definition.jl
@@ -1,13 +1,173 @@
# This file contains functions for calling, running and post-processing
# SIIP PSI Simulation for actual Energy and A/S Market Clearing
+function adjust_reserve_voll!(sys::PSY.System,
+ problem::PSI.OperationModel,
+ simulation_dir::String,
+ reserve_penalty::String,
+ zones::Vector{String},
+ default_balance_slack_cost::Float64,
+ default_service_slack_cost::Float64,
+ energy_voll_cost::AxisArrays.AxisArray{Float64, 1}
+ )
+
+ base_power = PSY.get_base_power(sys)
+ services = get_system_services(sys)
+
+ optimization_container = PSI.get_optimization_container(problem)
+ variables = PSI.get_variables(optimization_container)
+
+ for zone in zones
+ bus = find_zonal_bus(zone, sys)
+ slack_coefficients = [PSI.SystemBalanceSlackUp, PSI.SystemBalanceSlackDown]
+ for c in slack_coefficients
+ slack_key = PSI.VariableKey{c, PSY.Bus}("")
+ index = findall(x -> x == PSY.get_number(bus), variables[slack_key].axes[1])[1]
+ slack_variables = variables[slack_key].data[index, :]
+ delta_cost = energy_voll_cost[zone] * base_power - default_balance_slack_cost
+ for v in slack_variables
+ PSI.add_to_objective_variant_expression!(optimization_container, v * delta_cost)
+ end
+ end
+ end
+
+ for s in services
+ name = PSY.get_name(s)
+ delta_cost = 0.0
+ slack_variables = Symbol[]
+
+ # we don't have slack for Primary and Synchronous reserves?
+ if name == "Clean_Energy"
+ delta_cost = default_service_slack_cost * 5
+ #delta_cost = -default_service_slack_cost
+ slack_variables = variables[PSI.VariableKey{PSI.ReserveRequirementSlack, PSY.VariableReserve{PSY.ReserveUp}}("Clean_Energy")]
+ println(PSY.get_requirement(s))
+ elseif name == "Reg_Up" || name == "Inertia"
+ reserve_data = read_data(joinpath(simulation_dir, "markets_data", "$(reserve_penalty)_reserve_penalty", "$(name).csv"))
+ slack_variables = variables[PSI.VariableKey{PSI.ReserveRequirementSlack, PSY.VariableReserve{PSY.ReserveUp}}("$name")]
+ penalty_price = reserve_data[1, "price_cap"] * base_power
+ delta_cost = penalty_price - default_service_slack_cost
+ elseif name == "Reg_Down"
+ reserve_data = read_data(joinpath(simulation_dir, "markets_data", "$(reserve_penalty)_reserve_penalty", "$(name).csv"))
+ slack_variables = variables[PSI.VariableKey{PSI.ReserveRequirementSlack, PSY.VariableReserve{PSY.ReserveDown}}("$name")]
+ penalty_price = reserve_data[1, "price_cap"] * base_power
+ delta_cost = penalty_price - default_service_slack_cost
+ end
+
+
+ for v in slack_variables
+ PSI.add_to_objective_variant_expression!(optimization_container, v * delta_cost)
+ end
+ end
+
+end
+
+function scale_voll(price::Union{Array{Float64, 2}, AxisArrays.AxisArray{Float64, 2}}, resolution::Int64)
+ for t in 1:size(price, 2)
+ if price[1, t] >= 990.0
+ price[1, t] = price[1, t] * resolution / 60
+ end
+ end
+end
+
+function scale_voll(price::AxisArrays.AxisArray{Float64, 3}, resolution::Int64)
+ for z in 1:size(price, 1)
+ for t in 1:size(price, 3)
+ if price[z, 1, t] >= 990.0
+ price[z, 1, t] = price[z, 1, t] * resolution / 60
+ end
+ end
+ end
+end
+
"""
-This function returns realized capacity factors for Thermal generators from PSI Simulation.
+This function returns start-up costs for Thermal generators from PSI Simulation.
+"""
+function get_start_costs(device::PSY.ThermalStandard,
+ results::Dict{String, DataFrames.DataFrame},
+ data_length_uc::Int64
+ )
+ start_ups = results["StartVariable__ThermalStandard"][:, Symbol(get_name(device))]
+ start_up_costs = start_ups * PSY.get_start_up(PSY.get_operation_cost(device))
+ return start_up_costs
+end
+
+
+function get_start_costs(device::ThermalFastStartSIIP,
+ results::Dict{String, DataFrames.DataFrame},
+ data_length_uc::Int64
+ )
+ start_ups = results["StartVariable__ThermalFastStartSIIP"][:, Symbol(get_name(device))]
+ start_up_costs = start_ups * PSY.get_start_up(PSY.get_operation_cost(device))
+ return start_up_costs
+end
+
+"""
+This function returns total start-up costs for other generators from PSI Simulation.
+"""
+function get_start_costs(device::PSY.Device,
+ results::Dict{String, DataFrames.DataFrame},
+ data_length_uc::Int64
+ )
+ return zeros(data_length_uc)
+end
+
+"""
+This function returns shut-down costs for Thermal generators from PSI Simulation.
+"""
+function get_shut_costs(device::PSY.ThermalStandard,
+ results::Dict{String, DataFrames.DataFrame},
+ data_length_uc::Int64
+ )
+ shut_downs = results["StopVariable__ThermalStandard"][:, Symbol(get_name(device))]
+ shut_down_costs = shut_downs * PSY.get_shut_down(PSY.get_operation_cost(device))
+ return shut_down_costs
+end
+
+
+function get_shut_costs(device::ThermalFastStartSIIP,
+ results::Dict{String, DataFrames.DataFrame},
+ data_length_uc::Int64
+ )
+ shut_downs = results["StopVariable__ThermalFastStartSIIP"][:, Symbol(get_name(device))]
+ shut_down_costs = shut_downs * PSY.get_shut_down(PSY.get_operation_cost(device))
+ return shut_down_costs
+end
+"""
+This function returns total shut-down costs for other generators from PSI Simulation.
+"""
+function get_shut_costs(device::PSY.Device,
+ results::Dict{String, DataFrames.DataFrame},
+ data_length_uc::Int64
+ )
+ return zeros(data_length_uc)
+end
+
+
+"""
+This function returns realized capacity factors for ThermalStandard generators from PSI Simulation.
"""
function get_realized_capacity_factors(device::PSY.ThermalStandard,
- results::Dict{Symbol, DataFrames.DataFrame}
+ results::Dict{String, DataFrames.DataFrame},
+ results_uc::Dict{String, DataFrames.DataFrame}
)
- energy_production = results[:P__ThermalStandard][:, Symbol(get_name(device))]
+ energy_production = results["ActivePowerVariable__ThermalStandard"][:, Symbol(get_name(device))]
+ capacity_factors = energy_production / get_device_size(device)
+ return capacity_factors
+end
+
+"""
+This function returns realized capacity factors for ThermalCleanEnergy generators from PSI Simulation.
+"""
+
+"""
+This function returns realized capacity factors for ThermalFastStartSIIP generators from PSI Simulation.
+"""
+function get_realized_capacity_factors(device::ThermalFastStartSIIP,
+ results::Dict{String, DataFrames.DataFrame},
+ results_uc::Dict{String, DataFrames.DataFrame}
+ )
+ energy_production = results["ActivePowerVariable__ThermalFastStartSIIP"][:, Symbol(get_name(device))]
capacity_factors = energy_production / get_device_size(device)
return capacity_factors
end
@@ -16,9 +176,10 @@ end
This function returns realized capacity factors for Renewable generators from PSI Simulation.
"""
function get_realized_capacity_factors(device::PSY.RenewableDispatch,
- results::Dict{Symbol, DataFrames.DataFrame}
+ results::Dict{String, DataFrames.DataFrame},
+ results_uc::Dict{String, DataFrames.DataFrame}
)
- energy_production = results[:P__RenewableDispatch][:, Symbol(get_name(device))]
+ energy_production = results["ActivePowerVariable__RenewableDispatch"][:, Symbol(get_name(device))]
capacity_factors = energy_production / get_device_size(device)
return capacity_factors
end
@@ -27,9 +188,10 @@ end
This function returns realized capacity factors for Hydropower generators from PSI Simulation.
"""
function get_realized_capacity_factors(device::PSY.HydroDispatch,
- results::Dict{Symbol, DataFrames.DataFrame}
+ results::Dict{String, DataFrames.DataFrame},
+ results_uc::Dict{String, DataFrames.DataFrame}
)
- energy_production = results[:P__HydroDispatch][:, Symbol(get_name(device))]
+ energy_production = results["ActivePowerVariable__HydroDispatch"][:, Symbol(get_name(device))]
capacity_factors = energy_production / get_device_size(device)
return capacity_factors
end
@@ -38,21 +200,40 @@ end
This function returns realized capacity factors for Hydropower generators from PSI Simulation.
"""
function get_realized_capacity_factors(device::PSY.HydroEnergyReservoir,
- results::Dict{Symbol, DataFrames.DataFrame}
+ results::Dict{String, DataFrames.DataFrame},
+ results_uc::Dict{String, DataFrames.DataFrame}
)
- energy_production = results[:P__HydroEnergyReservoirh][:, Symbol(get_name(device))]
+ energy_production = results["ActivePowerVariable__HydroEnergyReservoir"][:, Symbol(get_name(device))]
capacity_factors = energy_production / get_device_size(device)
return capacity_factors
end
+"""
+This function returns realized capacity factors for Hydropower generators from PSI Simulation.
+"""
+function get_realized_capacity_factors(device::PSY.GenericBattery,
+ results::Dict{String, DataFrames.DataFrame},
+ results_uc::Dict{String, DataFrames.DataFrame}
+ )
+ energy_production = results["ActivePowerOutVariable__GenericBattery"][:, Symbol(get_name(device))] - results["ActivePowerInVariable__GenericBattery"][:, Symbol(get_name(device))]
+ capacity_factors = energy_production / get_device_size(device)
+ generation = filter(x -> x > 0, capacity_factors)
+
+ energy_production_uc = results_uc["ActivePowerOutVariable__GenericBattery"][:, Symbol(get_name(device))] - results_uc["ActivePowerInVariable__GenericBattery"][:, Symbol(get_name(device))]
+ capacity_factors_uc = energy_production_uc / get_device_size(device)
+ generation_uc = filter(x -> x > 0, capacity_factors_uc)
+ return capacity_factors
+end
+
"""
This function returns nothing if Service is not of ReserveUp or ReserveDown type.
"""
function update_realized_reserve_perc!(device::PSY.Device,
service::S,
- results_ed::Dict{Symbol, DataFrames.DataFrame},
- results_uc::Dict{Symbol, DataFrames.DataFrame},
+ results_ed::Dict{String, DataFrames.DataFrame},
+ results_uc::Dict{String, DataFrames.DataFrame},
reserve_perc::Dict{String, Dict{String, Array{Float64, 2}}},
+ inertia_perc::Dict{String, Array{Float64, 2}},
rt_products::Vector{SubString{String}},
only_da_products::Vector{String}) where S <: PSY.Service
return
@@ -63,25 +244,33 @@ This function returns realized reserve up provision percentages from PSI Simulat
"""
function update_realized_reserve_perc!(device::PSY.Device,
service::PSY.VariableReserve{PSY.ReserveUp},
- results_ed::Dict{Symbol, DataFrames.DataFrame},
- results_uc::Dict{Symbol, DataFrames.DataFrame},
+ results_ed::Dict{String, DataFrames.DataFrame},
+ results_uc::Dict{String, DataFrames.DataFrame},
reserve_perc::Dict{String, Dict{String, Array{Float64, 2}}},
+ inertia_perc::Dict{String, Array{Float64, 2}},
rt_products::Vector{SubString{String}},
only_da_products::Vector{String})
service_name = PSY.get_name(service)
- if service_name in rt_products
- reserve_provision = results_ed[Symbol("$(service_name)__VariableReserve_PowerSystems.ReserveUp")][:, Symbol(get_name(device))]
- reserve_perc_value = reserve_provision / get_device_size(device)
- reserve_perc[get_name(device)][service_name][1, :] = reserve_perc_value
-
- elseif service_name in only_da_products
- reserve_provision = results_uc[Symbol("$(service_name)__VariableReserve_PowerSystems.ReserveUp")][:, Symbol(get_name(device))]
- reserve_perc_value = reserve_provision / get_device_size(device)
- reserve_perc[get_name(device)][service_name][1, :] = reserve_perc_value
+ if service_name == "Inertia"
+ inertia_provision = results_ed["ActivePowerReserveVariable__VariableReserve__ReserveUp__$(service_name)"][:, Symbol(get_name(device))]
+ inertia_perc_value = inertia_provision / get_device_size(device)
+ inertia_perc[get_name(device)][1, :] = inertia_perc_value
+ else
+ if service_name in rt_products
+ reserve_provision = results_ed["ActivePowerReserveVariable__VariableReserve__ReserveUp__$(service_name)"][:, Symbol(get_name(device))]
+ reserve_perc_value = reserve_provision / get_device_size(device)
+ reserve_perc[get_name(device)][service_name][1, :] = reserve_perc_value
+
+ elseif service_name in only_da_products
+ reserve_provision = results_uc["ActivePowerReserveVariable__VariableReserve__ReserveUp__$(service_name)"][:, Symbol(get_name(device))]
+ reserve_perc_value = reserve_provision / get_device_size(device)
+ reserve_perc[get_name(device)][service_name][1, :] = reserve_perc_value
+ end
end
+
return
end
@@ -90,21 +279,22 @@ This function returns realized ordc provision percentages from PSI Simulation.
"""
function update_realized_reserve_perc!(device::PSY.Device,
service::PSY.ReserveDemandCurve{PSY.ReserveUp},
- results_ed::Dict{Symbol, DataFrames.DataFrame},
- results_uc::Dict{Symbol, DataFrames.DataFrame},
+ results_ed::Dict{String, DataFrames.DataFrame},
+ results_uc::Dict{String, DataFrames.DataFrame},
reserve_perc::Dict{String, Dict{String, Array{Float64, 2}}},
+ inertia_perc::Dict{String, Array{Float64, 2}},
rt_products::Vector{SubString{String}},
only_da_products::Vector{String})
service_name = PSY.get_name(service)
if service_name in rt_products
- reserve_provision = results_ed[Symbol("$(service_name)__ReserveDemandCurve_PowerSystems.ReserveUp")][:, Symbol(get_name(device))]
+ reserve_provision = results_ed["ActivePowerReserveVariable__ReserveDemandCurve__ReserveUp__$(service_name)"][:, Symbol(get_name(device))]
reserve_perc_value = reserve_provision / get_device_size(device)
reserve_perc[get_name(device)][service_name][1, :] = reserve_perc_value
elseif service_name in only_da_products
- reserve_provision = results_uc[Symbol("$(service_name)__ReserveDemandCurve_PowerSystems.ReserveUp")][:, Symbol(get_name(device))]
+ reserve_provision = results_uc["ActivePowerReserveVariable__ReserveDemandCurve__ReserveUp__$(service_name)"][:, Symbol(get_name(device))]
reserve_perc_value = reserve_provision / get_device_size(device)
reserve_perc[get_name(device)][service_name][1, :] = reserve_perc_value
@@ -117,21 +307,22 @@ This function returns realized reserve down provision percentages from PSI Simul
"""
function update_realized_reserve_perc!(device::PSY.Device,
service::PSY.VariableReserve{PSY.ReserveDown},
- results_ed::Dict{Symbol, DataFrames.DataFrame},
- results_uc::Dict{Symbol, DataFrames.DataFrame},
+ results_ed::Dict{String, DataFrames.DataFrame},
+ results_uc::Dict{String, DataFrames.DataFrame},
reserve_perc::Dict{String, Dict{String, Array{Float64, 2}}},
+ inertia_perc::Dict{String, Array{Float64, 2}},
rt_products::Vector{SubString{String}},
only_da_products::Vector{String})
service_name = PSY.get_name(service)
if service_name in rt_products
- reserve_provision = results_ed[Symbol("$(service_name)__VariableReserve_PowerSystems.ReserveDown")][:, Symbol(get_name(device))]
+ reserve_provision = results_ed["ActivePowerReserveVariable__VariableReserve__ReserveDown__$(service_name)"][:, Symbol(get_name(device))]
reserve_perc_value = reserve_provision / get_device_size(device)
reserve_perc[get_name(device)][service_name][1, :] = reserve_perc_value
elseif service_name in only_da_products
- reserve_provision = results_uc[Symbol("$(service_name)__VariableReserve_PowerSystems.ReserveDown")][:, Symbol(get_name(device))]
+ reserve_provision = results_uc["ActivePowerReserveVariable__VariableReserve__ReserveDown__$(service_name)"][:, Symbol(get_name(device))]
reserve_perc_value = reserve_provision / get_device_size(device)
reserve_perc[get_name(device)][service_name][1, :] = reserve_perc_value
@@ -143,23 +334,140 @@ end
This function creates the Unit Commitment template for PSI Simulation.
"""
#TODO: Update needed
-function create_uc_template()
-
- template = PSI.OperationsProblemTemplate(PSI.PM.NFAPowerModel)
- PSI.set_device_model!(template, PSY.ThermalStandard, PSI.ThermalStandardUnitCommitment)
- PSI.set_device_model!(template, PSY.RenewableDispatch, PSI.RenewableFullDispatch)
- PSI.set_device_model!(template, PSY.RenewableFix, PSI.FixedOutput)
- PSI.set_device_model!(template, PSY.PowerLoad, PSI.StaticPowerLoad)
- #PSI.set_device_model!(template, PSY.HydroEnergyReservoir, PSI.HydroDispatchRunOfRiver)
- PSI.set_device_model!(template, PSY.HydroDispatch, PSI.HydroDispatchRunOfRiver) # TODO: check which hydro device we have
- PSI.set_device_model!(template, PSY.GenericBattery, PSI.BookKeepingwReservation)
- PSI.set_device_model!(template, PSY.Line, PSI.StaticBranch)
- PSI.set_device_model!(template, PSY.Transformer2W, PSI.StaticBranch)
- PSI.set_device_model!(template, PSY.TapTransformer, PSI.StaticBranch)
- PSI.set_device_model!(template, PSY.HVDCLine, PSI.HVDCLossless)
- PSI.set_service_model!(template, PSI.ServiceModel(PSY.VariableReserve{PSY.ReserveUp}, PSIE.RampReserve))
- PSI.set_service_model!(template, PSI.ServiceModel(PSY.VariableReserve{PSY.ReserveDown}, PSI.RangeReserve))
- PSI.set_service_model!(template, PSI.ServiceModel(PSY.ReserveDemandCurve{PSY.ReserveUp}, PSIE.QuadraticCostRampReserve))
+function create_uc_template(inertia_product)
+
+ if !(isempty(inertia_product))
+
+ template = PSI.ProblemTemplate(
+ PSI.NetworkModel(
+ PSI.PM.NFAPowerModel,
+ duals = [PSI.NodalBalanceActiveConstraint],
+ use_slacks = true,
+ ),
+ )
+ # PSI.set_device_model!(template, PSY.ThermalStandard, PSI.ThermalStandardUnitCommitment)
+ PSI.set_device_model!(template, PSY.ThermalStandard, RPSI.ThermalStandardUCOutages)
+ PSI.set_device_model!(template, ThermalFastStartSIIP, PSI.ThermalStandardUnitCommitment)
+ PSI.set_device_model!(template, PSY.RenewableDispatch, PSI.RenewableFullDispatch)
+ PSI.set_device_model!(template, PSY.RenewableFix, PSI.FixedOutput)
+ PSI.set_device_model!(template, PSY.PowerLoad, PSI.StaticPowerLoad)
+ PSI.set_device_model!(template, PSY.HydroEnergyReservoir, PSI.HydroCommitmentRunOfRiver)
+ PSI.set_device_model!(template, PSY.HydroDispatch, PSI.HydroCommitmentRunOfRiver) # TODO: check which hydro device we have
+ PSI.set_device_model!(template, PSY.GenericBattery, PSI.BatteryAncillaryServices)
+ PSI.set_device_model!(template, PSY.Line, PSI.StaticBranch)
+ PSI.set_device_model!(template, PSY.Transformer2W, PSI.StaticBranch)
+ PSI.set_device_model!(template, PSY.TapTransformer, PSI.StaticBranch)
+ PSI.set_device_model!(template, PSY.HVDCLine, PSI.HVDCLossless)
+ PSI.set_service_model!(
+ template,
+ PSI.ServiceModel(
+ PSY.VariableReserve{PSY.ReserveUp},
+ PSI.RangeReserve,
+ "Reg_Up",
+ use_slacks=true,
+ duals = [PSI.RequirementConstraint],
+ )
+ )
+ PSI.set_service_model!(
+ template,
+ PSI.ServiceModel(
+ PSY.VariableReserve{PSY.ReserveDown},
+ PSI.RangeReserve,
+ "Reg_Down",
+ use_slacks=true,
+ duals = [PSI.RequirementConstraint],
+ )
+ )
+ PSI.set_service_model!(
+ template,
+ PSI.ServiceModel(
+ PSY.ReserveDemandCurve{PSY.ReserveUp},
+ EMISEx.QuadraticCostRampReserve,
+ use_slacks=true,
+ duals = [PSI.RequirementConstraint],
+ )
+ )
+ PSI.set_service_model!(
+ template,
+ PSI.ServiceModel(
+ PSY.VariableReserve{PSY.ReserveUp},
+ EMISEx.InertiaReserve,
+ "Inertia",
+ use_slacks=true,
+ duals = [PSI.RequirementConstraint],
+ )
+ )
+ PSI.set_service_model!(
+ template,
+ PSI.ServiceModel(
+ PSY.VariableReserve{PSY.ReserveUp},
+ EMISEx.CleanEnergyReserve,
+ "Clean_Energy",
+ use_slacks=true,
+ duals = [PSI.RequirementConstraint],
+ )
+ )
+ else
+ template = PSI.ProblemTemplate(
+ PSI.NetworkModel(
+ PSI.PM.NFAPowerModel,
+ duals = [PSI.NodalBalanceActiveConstraint],
+ use_slacks = true,
+ ),
+ )
+ # PSI.set_device_model!(template, PSY.ThermalStandard, PSI.ThermalStandardUnitCommitment)
+ PSI.set_device_model!(template, PSY.ThermalStandard, RPSI.ThermalStandardUCOutages)
+ PSI.set_device_model!(template, ThermalFastStartSIIP, PSI.ThermalStandardUnitCommitment)
+ PSI.set_device_model!(template, PSY.RenewableDispatch, PSI.RenewableFullDispatch)
+ PSI.set_device_model!(template, PSY.RenewableFix, PSI.FixedOutput)
+ PSI.set_device_model!(template, PSY.PowerLoad, PSI.StaticPowerLoad)
+ PSI.set_device_model!(template, PSY.HydroEnergyReservoir, PSI.HydroCommitmentRunOfRiver)
+ PSI.set_device_model!(template, PSY.HydroDispatch, PSI.HydroCommitmentRunOfRiver) # TODO: check which hydro device we have
+ PSI.set_device_model!(template, PSY.GenericBattery, PSI.BatteryAncillaryServices)
+ PSI.set_device_model!(template, PSY.Line, PSI.StaticBranch)
+ PSI.set_device_model!(template, PSY.Transformer2W, PSI.StaticBranch)
+ PSI.set_device_model!(template, PSY.TapTransformer, PSI.StaticBranch)
+ PSI.set_device_model!(template, PSY.HVDCLine, PSI.HVDCLossless)
+ PSI.set_service_model!(
+ template,
+ PSI.ServiceModel(
+ PSY.VariableReserve{PSY.ReserveUp},
+ PSI.RangeReserve,
+ "Reg_Up",
+ use_slacks=true,
+ duals = [PSI.RequirementConstraint],
+ )
+ )
+ PSI.set_service_model!(
+ template,
+ PSI.ServiceModel(
+ PSY.VariableReserve{PSY.ReserveDown},
+ PSI.RangeReserve,
+ "Reg_Down",
+ use_slacks=true,
+ duals = [PSI.RequirementConstraint],
+ )
+ )
+ PSI.set_service_model!(
+ template,
+ PSI.ServiceModel(
+ PSY.ReserveDemandCurve{PSY.ReserveUp},
+ EMISEx.QuadraticCostRampReserve,
+ use_slacks=true,
+ duals = [PSI.RequirementConstraint],
+ )
+ )
+ PSI.set_service_model!(
+ template,
+ PSI.ServiceModel(
+ PSY.VariableReserve{PSY.ReserveUp},
+ EMISEx.CleanEnergyReserve,
+ "Clean_Energy",
+ use_slacks=true,
+ duals = [PSI.RequirementConstraint],
+ )
+ )
+ end
return template
end
@@ -168,24 +476,119 @@ end
This function creates the Economic Dispatch template for PSI Simulation.
"""
#TODO: Update needed
-function create_ed_template()
-
-
- template = PSI.OperationsProblemTemplate(PSI.PM.NFAPowerModel)
- PSI.set_device_model!(template, PSY.ThermalStandard, PSI.ThermalDispatch)
- PSI.set_device_model!(template, PSY.RenewableDispatch, PSI.RenewableFullDispatch)
- PSI.set_device_model!(template, PSY.RenewableFix, PSI.FixedOutput)
- PSI.set_device_model!(template, PSY.PowerLoad, PSI.StaticPowerLoad)
- # PSI.set_device_model!(template, PSY.HydroEnergyReservoir, PSI.HydroDispatch)
- PSI.set_device_model!(template, PSY.HydroDispatch, PSI.HydroDispatchRunOfRiver) # TODO: check which hydro device we have
- PSI.set_device_model!(template, PSY.GenericBattery, PSI.BookKeepingwReservation)
- PSI.set_device_model!(template, PSY.Line, PSI.StaticBranch)
- PSI.set_device_model!(template, PSY.Transformer2W, PSI.StaticBranch)
- PSI.set_device_model!(template, PSY.TapTransformer, PSI.StaticBranch)
- PSI.set_device_model!(template, PSY.HVDCLine, PSI.HVDCLossless)
- PSI.set_service_model!(template, PSI.ServiceModel(PSY.VariableReserve{PSY.ReserveUp}, PSIE.RampReserve))
- PSI.set_service_model!(template, PSI.ServiceModel(PSY.VariableReserve{PSY.ReserveDown}, PSI.RangeReserve))
- PSI.set_service_model!(template, PSI.ServiceModel(PSY.ReserveDemandCurve{PSY.ReserveUp}, PSIE.QuadraticCostRampReserve))
+function create_ed_template(inertia_product)
+
+ if !(isempty(inertia_product))
+ template = PSI.ProblemTemplate(
+ PSI.NetworkModel(
+ PSI.PM.NFAPowerModel,
+ duals = [PSI.NodalBalanceActiveConstraint],
+ use_slacks = true,
+ ),
+ )
+ # PSI.set_device_model!(template, PSY.ThermalStandard, PSI.ThermalStandardDispatch)
+ PSI.set_device_model!(template, PSY.ThermalStandard, RPSI.ThermalDispatchOutages)
+ PSI.set_device_model!(template, ThermalFastStartSIIP, PSI.ThermalStandardDispatch)
+ PSI.set_device_model!(template, PSY.RenewableDispatch, PSI.RenewableFullDispatch)
+ PSI.set_device_model!(template, PSY.RenewableFix, PSI.FixedOutput)
+ PSI.set_device_model!(template, PSY.PowerLoad, PSI.StaticPowerLoad)
+ PSI.set_device_model!(template, PSY.HydroEnergyReservoir, PSI.HydroDispatchRunOfRiver)
+ PSI.set_device_model!(template, PSY.HydroDispatch, PSI.HydroDispatchRunOfRiver) # TODO: check which hydro device we have
+ PSI.set_device_model!(template, PSY.GenericBattery, PSI.BatteryAncillaryServices)
+ PSI.set_device_model!(template, PSY.Line, PSI.StaticBranch)
+ PSI.set_device_model!(template, PSY.Transformer2W, PSI.StaticBranch)
+ PSI.set_device_model!(template, PSY.TapTransformer, PSI.StaticBranch)
+ PSI.set_device_model!(template, PSY.HVDCLine, PSI.HVDCLossless)
+ PSI.set_service_model!(
+ template,
+ PSI.ServiceModel(
+ PSY.VariableReserve{PSY.ReserveUp},
+ PSI.RangeReserve,
+ "Reg_Up",
+ use_slacks=true,
+ duals = [PSI.RequirementConstraint],
+ )
+ )
+ PSI.set_service_model!(
+ template,
+ PSI.ServiceModel(
+ PSY.VariableReserve{PSY.ReserveDown},
+ PSI.RangeReserve,
+ "Reg_Down",
+ use_slacks=true,
+ duals = [PSI.RequirementConstraint],
+ )
+ )
+ PSI.set_service_model!(
+ template,
+ PSI.ServiceModel(
+ PSY.ReserveDemandCurve{PSY.ReserveUp},
+ EMISEx.QuadraticCostRampReserve,
+ use_slacks=true,
+ duals = [PSI.RequirementConstraint],
+ )
+ )
+ PSI.set_service_model!(
+ template,
+ PSI.ServiceModel(
+ PSY.VariableReserve{PSY.ReserveUp},
+ EMISEx.InertiaReserve,
+ "Inertia",
+ use_slacks=true,
+ duals = [PSI.RequirementConstraint],
+ )
+ )
+ else
+ template = PSI.ProblemTemplate(
+ PSI.NetworkModel(
+ PSI.PM.NFAPowerModel,
+ duals = [PSI.NodalBalanceActiveConstraint],
+ use_slacks = true,
+ ),
+ )
+ # PSI.set_device_model!(template, PSY.ThermalStandard, PSI.ThermalStandardDispatch)
+ PSI.set_device_model!(template, PSY.ThermalStandard, RPSI.ThermalDispatchOutages)
+ PSI.set_device_model!(template, ThermalFastStartSIIP, PSI.ThermalStandardDispatch)
+ PSI.set_device_model!(template, PSY.RenewableDispatch, PSI.RenewableFullDispatch)
+ PSI.set_device_model!(template, PSY.RenewableFix, PSI.FixedOutput)
+ PSI.set_device_model!(template, PSY.PowerLoad, PSI.StaticPowerLoad)
+ PSI.set_device_model!(template, PSY.HydroEnergyReservoir, PSI.HydroDispatchRunOfRiver)
+ PSI.set_device_model!(template, PSY.HydroDispatch, PSI.HydroDispatchRunOfRiver) # TODO: check which hydro device we have
+ PSI.set_device_model!(template, PSY.GenericBattery, PSI.BatteryAncillaryServices)
+ PSI.set_device_model!(template, PSY.Line, PSI.StaticBranch)
+ PSI.set_device_model!(template, PSY.Transformer2W, PSI.StaticBranch)
+ PSI.set_device_model!(template, PSY.TapTransformer, PSI.StaticBranch)
+ PSI.set_device_model!(template, PSY.HVDCLine, PSI.HVDCLossless)
+ PSI.set_service_model!(
+ template,
+ PSI.ServiceModel(
+ PSY.VariableReserve{PSY.ReserveUp},
+ PSI.RangeReserve,
+ "Reg_Up",
+ use_slacks=true,
+ duals = [PSI.RequirementConstraint],
+ )
+ )
+ PSI.set_service_model!(
+ template,
+ PSI.ServiceModel(
+ PSY.VariableReserve{PSY.ReserveDown},
+ PSI.RangeReserve,
+ "Reg_Down",
+ use_slacks=true,
+ duals = [PSI.RequirementConstraint],
+ )
+ )
+ PSI.set_service_model!(
+ template,
+ PSI.ServiceModel(
+ PSY.ReserveDemandCurve{PSY.ReserveUp},
+ EMISEx.QuadraticCostRampReserve,
+ use_slacks=true,
+ duals = [PSI.RequirementConstraint],
+ )
+ )
+ end
return template
end
@@ -194,44 +597,38 @@ end
This function creates the Problem for PSI Simulation.
"""
-function create_problem(template::PSI.OperationsProblemTemplate, sys::PSY.System, type::String, solver::JuMP.MOI.OptimizerWithAttributes)
-
- duals = [:nodal_balance_active__Bus]
-
- services = get_system_services(sys)
- if in(PSY.VariableReserve{PSY.ReserveUp}, typeof.(services))
- push!(duals, Symbol("requirement__VariableReserve_PowerSystems.ReserveUp"))
- end
- if in(PSY.ReserveDemandCurve{PSY.ReserveUp}, typeof.(services))
- push!(duals, Symbol("requirement__ReserveDemandCurve_PowerSystems.ReserveUp"))
- end
- if in(PSY.VariableReserve{PSY.ReserveDown}, typeof.(services))
- push!(duals, Symbol("requirement__VariableReserve_PowerSystems.ReserveDown"))
- end
+function create_problem(template::PSI.ProblemTemplate, sys::PSY.System, type::String, solver::JuMP.MOI.OptimizerWithAttributes, inertia_product)
if type == "UC"
- problem = PSI.OperationsProblem(
- PSI.UnitCommitmentProblem,
+ problem = PSI.DecisionModel(
template,
sys;
optimizer = solver,
- optimizer_log_print = true,
- balance_slack_variables = true,
- constraint_duals = duals,
- horizon = 2,
+ name = "UC",
+ optimizer_solve_log_print = false,
+ warm_start = true,
)
elseif type == "ED"
- problem = PSI.OperationsProblem(
- PSI.EconomicDispatchProblem,
- template,
- sys;
- optimizer = solver,
- optimizer_log_print = false,
- balance_slack_variables = true,
- constraint_duals = duals,
- horizon = 2,
- )
+ if !(isempty(inertia_product))
+ problem = PSI.DecisionModel(
+ template,
+ sys;
+ optimizer = solver,
+ name = "ED",
+ optimizer_solve_log_print = false,
+ warm_start = true,
+ )
+ else
+ problem = PSI.DecisionModel(
+ template,
+ sys;
+ optimizer = solver,
+ name = "ED",
+ optimizer_solve_log_print = false,
+ warm_start = true,
+ )
+ end
else
error("Type should be either UC or ED")
end
@@ -242,21 +639,11 @@ end
"""
This function creates the Sequences for PSI Simulation.
"""
-function create_sequence(problems::PSI.SimulationProblems)
+function create_sequence(problems::PSI.SimulationModels, feedforward_dict)
sequence = PSI.SimulationSequence(
- problems = problems,
- feedforward_chronologies = Dict(("UC" => "ED") => PSI.Synchronize(periods = 24)),
- intervals = Dict(
- "UC" => (Dates.Hour(24), PSI.Consecutive()),
- "ED" => (Dates.Hour(1), PSI.Consecutive()),
- ),
- feedforward = Dict(
- ("ED", :devices, Symbol("PowerSystems.ThermalStandard")) => PSI.SemiContinuousFF(
- binary_source_problem = PSI.ON,
- affected_variables = [PSI.ACTIVE_POWER],
- ),
- ),
+ models = problems,
+ feedforwards = feedforward_dict,
ini_cond_chronology = PSI.InterProblemChronology(),
)
return sequence
@@ -270,56 +657,138 @@ This function creates the PSI Simulation and post-processes the results.
function create_simulation( sys_UC::PSY.System,
sys_ED::PSY.System,
simulation_dir::String,
+ reserve_penalty::String,
zones::Vector{String},
days::Int64,
- solver::JuMP.MOI.OptimizerWithAttributes,
- iteration_year::Int64;
+ da_resolution::Int64,
+ rt_resolution::Int64,
+ case_name::String,
+ solver::JuMP.MOI.OptimizerWithAttributes;
kwargs...)
+ inertia_product = collect(PSY.get_components_by_name(PSY.Service, sys_ED, "Inertia"))
+
+ template_uc = create_uc_template(inertia_product)
+ template_ed = create_ed_template(inertia_product)
+
+ uc_problem = create_problem(template_uc, sys_UC, "UC", solver, inertia_product)
+ ed_problem = create_problem(template_ed, sys_ED, "ED", solver, inertia_product)
+
+ if isempty(inertia_product)
+ feedforward_dict = Dict(
+ "ED" => [
+ PSI.SemiContinuousFeedforward(
+ component_type = PSY.ThermalStandard,
+ source = PSI.OnVariable,
+ affected_values = [PSI.ActivePowerVariable],
+ ),
+ PSI.SemiContinuousFeedforward(
+ component_type = ThermalFastStartSIIP,
+ source = PSI.OnVariable,
+ affected_values = [PSI.ActivePowerVariable],
+ ),
+ PSI.EnergyTargetFeedforward(
+ component_type = PSY.GenericBattery,
+ source = PSI.EnergyVariable,
+ target_period = 1,
+ penalty_cost = 1e5,
+ affected_values = [PSI.EnergyVariable],
+ ),
+ RPSI.SemiContinuousOutageFeedforward(
+ component_type = PSY.ThermalStandard,
+ source = PSI.OnVariable,
+ affected_values = [PSI.ActivePowerVariable],
+ ),
+ ],
+ )
+ else
+ feedforward_dict = Dict(
+ "ED" => [
+ PSI.SemiContinuousFeedforward(
+ component_type = PSY.ThermalStandard,
+ source = PSI.OnVariable,
+ affected_values = [PSI.ActivePowerVariable],
+ ),
+ PSI.SemiContinuousFeedforward(
+ component_type = ThermalFastStartSIIP,
+ source = PSI.OnVariable,
+ affected_values = [PSI.ActivePowerVariable],
+ ),
+ PSI.EnergyTargetFeedforward(
+ component_type = PSY.GenericBattery,
+ source = PSI.EnergyVariable,
+ target_period = 1,
+ penalty_cost = 1e5,
+ affected_values = [PSI.EnergyVariable],
+ ),
+ RPSI.SemiContinuousOutageFeedforward(
+ component_type = PSY.ThermalStandard,
+ source = PSI.OnVariable,
+ affected_values = [PSI.ActivePowerVariable],
+ ),
+ ],
+ )
+ end
+
- template_uc = create_uc_template()
- template_ed = create_ed_template()
+ rt_products = split(read_data(joinpath(simulation_dir, "markets_data", "reserve_products.csv"))[1,"rt_products"], "; ")
+ da_products = split(read_data(joinpath(simulation_dir, "markets_data", "reserve_products.csv"))[1,"da_products"], "; ")
- uc_problem = create_problem(template_uc, sys_UC, "UC", solver)
- ed_problem = create_problem(template_ed, sys_ED, "ED", solver)
+ default_balance_slack_cost = PSI.BALANCE_SLACK_COST
+ default_service_slack_cost = PSI.SERVICES_SLACK_COST
- problems = PSI.SimulationProblems(
- UC = uc_problem,
- ED = ed_problem,
- )
+ energy_mkt_data = read_data(joinpath(simulation_dir, "markets_data", "Energy.csv"))
+ energy_voll_cost = AxisArrays.AxisArray(energy_mkt_data.price_cap * 1.0, zones)
- sequence = create_sequence(problems)
+ models = PSI.SimulationModels(
+ decision_models = [
+ uc_problem,
+ ed_problem
+ ]
+ );
+
+ sequence = create_sequence(models, feedforward_dict);
sim = PSI.Simulation(
- name = "emis_energy_mkt_$(iteration_year)",
- steps = 2,
+ name = "emis_$(case_name)",
+ steps = 360,
problems = problems,
sequence = sequence,
simulation_folder = ".",
- )
+ # initial_time = Dates.DateTime("2018-02-28T00:00:00")
+ );
build_out = PSI.build!(sim; serialize = false)
- execute_out = PSI.execute!(sim)
+
+ adjust_reserve_voll!(sys_UC, uc_problem, simulation_dir, reserve_penalty, zones, default_balance_slack_cost, default_service_slack_cost, energy_voll_cost)
+ adjust_reserve_voll!(sys_ED, ed_problem, simulation_dir, reserve_penalty, zones, default_balance_slack_cost, default_service_slack_cost, energy_voll_cost)
+
+ execute_out = PSI.execute!(sim; enable_progress_bar = true)
+
+ sim_results = PSI.SimulationResults(sim)
base_power = PSY.get_base_power(sys_UC)
- sim_results = PSI.SimulationResults(sim);
- res_uc = PSI.get_problem_results(sim_results, "UC")
- res_ed = PSI.get_problem_results(sim_results, "ED")
+ res_uc = PSI.get_decision_problem_results(sim_results, "UC")
+ res_ed = PSI.get_decision_problem_results(sim_results, "ED")
dual_values_ed = PSI.read_realized_duals(res_ed)
dual_values_uc = PSI.read_realized_duals(res_uc)
+ result_variables_ed = PSI.read_realized_variables(res_ed)
+ result_variables_uc = PSI.read_realized_variables(res_uc)
- data_length_ed = DataFrames.nrow(dual_values_ed[:nodal_balance_active__Bus])
- data_length_uc = DataFrames.nrow(dual_values_uc[:nodal_balance_active__Bus])
+ data_length_ed = DataFrames.nrow(dual_values_ed["NodalBalanceActiveConstraint__Bus"])
+ data_length_uc = DataFrames.nrow(dual_values_uc["NodalBalanceActiveConstraint__Bus"])
energy_price = AxisArrays.AxisArray(zeros(length(zones), 1, data_length_ed), zones, 1:1, 1:data_length_ed)
-
- rt_products = split(read_data(joinpath(simulation_dir, "markets_data", "reserve_products.csv"))[1,"rt_products"], "; ")
- da_products = split(read_data(joinpath(simulation_dir, "markets_data", "reserve_products.csv"))[1,"da_products"], "; ")
+ energy_voll = AxisArrays.AxisArray(zeros(length(zones), 1, data_length_ed), zones, 1:1, 1:data_length_ed)
reserve_price = Dict(s => zeros(1, data_length_ed) for s in String.(rt_products))
+ reserve_voll = Dict(s => zeros(1, data_length_ed) for s in String.(rt_products))
+
+ inertia_price = AxisArrays.AxisArray(zeros(1, data_length_ed), 1:1, 1:data_length_ed)
+ inertia_voll = AxisArrays.AxisArray(zeros(1, data_length_ed), 1:1, 1:data_length_ed)
only_da_products = String[]
@@ -327,6 +796,7 @@ function create_simulation( sys_UC::PSY.System,
if !(product in rt_products)
push!(only_da_products, product)
reserve_price[String(product)] = zeros(1, data_length_uc)
+ reserve_voll[String(product)] = zeros(1, data_length_uc)
end
end
@@ -335,36 +805,50 @@ function create_simulation( sys_UC::PSY.System,
zone_num = parse(Int64, last(zone, 1))
if isnothing(bus)
energy_price[zone, 1, :] = zeros(data_length_ed)
+ energy_voll[zone, 1, :] = zeros(data_length_ed)
else
energy_price[zone, 1, :] =
- abs.(round.(dual_values_ed[:nodal_balance_active__Bus][:, PSY.get_name(bus)], digits = 5)) / base_power
+ abs.(round.(dual_values_ed["NodalBalanceActiveConstraint__Bus"][:, PSY.get_name(bus)], digits = 5)) / base_power
+ energy_voll[zone, 1, :] = abs.(round.(result_variables_ed["SystemBalanceSlackUp__Bus"][:, string(PSY.get_number(bus))], digits = 5)) / base_power
+ energy_voll[zone, 1, :] += abs.(round.(result_variables_ed["SystemBalanceSlackDown__Bus"][:, string(PSY.get_number(bus))], digits = 5)) / base_power
end
-
end
- println("energy_price")
- println(energy_price)
+
+ println(any(isnan, energy_price))
+ replace!(energy_price, NaN => 0.0)
+ scale_voll(energy_price, rt_resolution)
+
+ println(any(isnan, energy_price))
+ println(Statistics.mean(energy_price))
+
for service in get_system_services(sys_ED)
name = PSY.get_name(service)
- println(name)
+ #println(name)
if typeof(service) == PSY.VariableReserve{PSY.ReserveUp}
- reserve_price[name][1, :] = abs.(round.(dual_values_ed[Symbol("requirement__VariableReserve_PowerSystems.ReserveUp")][:, Symbol("$(name)")], digits = 5)) / base_power
- replace!(reserve_price[name], NaN => 0.0)
- replace!(reserve_price[name], 10000.0 => 10000 / 12)
- replace!(reserve_price[name], 1000.0 => 1000 / 12)
- println(reserve_price[name])
+ if name == "Inertia"
+ inertia_price[1, :] = abs.(round.(dual_values_ed["RequirementConstraint__VariableReserve__ReserveUp__$(name)"][:, Symbol("$(name)")], digits = 5)) / base_power
+ replace!(inertia_price, NaN => 0.0)
+ scale_voll(inertia_price, rt_resolution)
+ inertia_voll[1, :] = abs.(round.(result_variables_ed["ReserveRequirementSlack__VariableReserve__ReserveUp__$(name)"][:, Symbol("ReserveRequirementSlack__VariableReserve__ReserveUp__$(name)")], digits = 5)) / base_power
+ else
+ reserve_price[name][1, :] = abs.(round.(dual_values_ed["RequirementConstraint__VariableReserve__ReserveUp__$(name)"][:, Symbol("$(name)")], digits = 5)) / base_power
+ replace!(reserve_price[name], NaN => 0.0)
+ scale_voll(reserve_price[name], rt_resolution)
+ reserve_voll[name][1, :] = abs.(round.(result_variables_ed["ReserveRequirementSlack__VariableReserve__ReserveUp__$(name)"][:, Symbol("ReserveRequirementSlack__VariableReserve__ReserveUp__$(name)")], digits = 5)) / base_power
+ #println(reserve_price[name])
+ end
elseif typeof(service) == PSY.ReserveDemandCurve{PSY.ReserveUp}
- reserve_price[name][1, :] = abs.(round.(dual_values_ed[Symbol("requirement__ReserveDemandCurve_PowerSystems.ReserveUp")][:, Symbol("$(name)")], digits = 5)) / base_power
+ reserve_price[name][1, :] = abs.(round.(dual_values_ed["RequirementConstraint__ReserveDemandCurve__ReserveUp__$(name)"][:, Symbol("$(name)")], digits = 5)) / base_power
replace!(reserve_price[name], NaN => 0.0)
- replace!(reserve_price[name], 10000.0 => 10000 / 12)
- replace!(reserve_price[name], 1000.0 => 1000 / 12)
- println(reserve_price[name])
+ scale_voll(reserve_price[name], rt_resolution)
+ #println(reserve_price[name])
elseif typeof(service) == PSY.VariableReserve{PSY.ReserveDown}
- reserve_price[name][1, :] = abs.(round.(dual_values_ed[Symbol("requirement__VariableReserve_PowerSystems.ReserveDown")][:, Symbol("$(name)")], digits = 5)) / base_power
+ reserve_price[name][1, :] = abs.(round.(dual_values_ed["RequirementConstraint__VariableReserve__ReserveDown__$(name)"][:, Symbol("$(name)")], digits = 5)) / base_power
replace!(reserve_price[name], NaN => 0.0)
- replace!(reserve_price[name], 10000.0 => 10000 / 12)
- replace!(reserve_price[name], 1000.0 => 1000 / 12)
- println(reserve_price[name])
+ scale_voll(reserve_price[name], rt_resolution)
+ reserve_voll[name][1, :] = abs.(round.(result_variables_ed["ReserveRequirementSlack__VariableReserve__ReserveDown__$(name)"][:, Symbol("ReserveRequirementSlack__VariableReserve__ReserveDown__$(name)")], digits = 5)) / base_power
+ #println(reserve_price[name])
end
end
@@ -372,33 +856,44 @@ function create_simulation( sys_UC::PSY.System,
name = PSY.get_name(service)
if name in only_da_products
if typeof(service) == PSY.VariableReserve{PSY.ReserveUp}
- reserve_price[name][1, :] = abs.(round.(dual_values_uc[Symbol("requirement__VariableReserve_PowerSystems.ReserveUp")][:, Symbol("$(name)")], digits = 5)) / base_power
- replace!(reserve_price[name], NaN => 0.0)
- replace!(reserve_price[name], 10000.0 => 10000 / 12)
- replace!(reserve_price[name], 1000.0 => 1000 / 12)
+ if name == "Inertia"
+ inertia_price[1, :] = abs.(round.(dual_values_uc["RequirementConstraint__VariableReserve__ReserveUp__$(name)"][:, Symbol("$(name)")], digits = 5)) / base_power
+ replace!(inertia_price, NaN => 0.0)
+ scale_voll(inertia_price, da_resolution)
+ # inertia_voll[1, :] = abs.(round.(result_variables_uc["ReserveRequirementSlack__VariableReserve__ReserveUp__$(name)"][:, Symbol("ReserveRequirementSlack__VariableReserve__ReserveUp__$(name)")], digits = 5)) / base_power
+ else
+ reserve_price[name][1, :] = abs.(round.(dual_values_uc["RequirementConstraint__VariableReserve__ReserveUp__$(name)"][:, Symbol("$(name)")], digits = 5)) / base_power
+ replace!(reserve_price[name], NaN => 0.0)
+ scale_voll(reserve_price[name], da_resolution)
+ reserve_voll[name][1, :] = abs.(round.(result_variables_uc["ReserveRequirementSlack__VariableReserve__ReserveUp__$(name)"][:, Symbol("ReserveRequirementSlack__VariableReserve__ReserveUp__$(name)")], digits = 5)) / base_power
+ #println(reserve_price[name])
+ end
elseif typeof(service) == PSY.ReserveDemandCurve{PSY.ReserveUp}
- reserve_price[name][1, :] = abs.(round.(dual_values_uc[Symbol("requirement__ReserveDemandCurve_PowerSystems.ReserveUp")][:, Symbol("$(name)")], digits = 5)) / base_power
+ reserve_price[name][1, :] = abs.(round.(dual_values_uc["RequirementConstraint__ReserveDemandCurve__ReserveUp__$(name)"][:, Symbol("$(name)")], digits = 5)) / base_power
replace!(reserve_price[name], NaN => 0.0)
- replace!(reserve_price[name], 10000.0 => 10000 / 12)
- replace!(reserve_price[name], 1000.0 => 1000 / 12)
+ scale_voll(reserve_price[name], da_resolution)
+ #println(reserve_price[name])
elseif typeof(service) == PSY.VariableReserve{PSY.ReserveDown}
- reserve_price[name][1, :] = abs.(round.(dual_values_uc[Symbol("requirement__VariableReserve_PowerSystems.ReserveDown")][:, Symbol("$(name)")], digits = 5)) / base_power
+ reserve_price[name][1, :] = abs.(round.(dual_values_uc["RequirementConstraint__VariableReserve__ReserveDown__$(name)"][:, Symbol("$(name)")], digits = 5)) / base_power
replace!(reserve_price[name], NaN => 0.0)
- replace!(reserve_price[name], 10000.0 => 10000 / 12)
- replace!(reserve_price[name], 1000.0 => 1000 / 12)
+ scale_voll(reserve_price[name], da_resolution)
+ reserve_voll[name][1, :] = abs.(round.(result_variables_uc["ReserveRequirementSlack__VariableReserve__ReserveDown__$(name)"][:, Symbol("ReserveRequirementSlack__VariableReserve__ReserveDown__$(name)")], digits = 5)) / base_power
+ #println(reserve_price[name])
end
end
end
- result_variables_ed = PSI.read_realized_variables(res_ed)
- result_variables_uc = PSI.read_realized_variables(res_uc)
-
sys_techs = get_all_techs(sys_ED)
+
tech_names = get_name.(sys_techs)
capacity_factors = Dict([g => zeros(1, data_length_ed) for g in tech_names])
+ start_up_costs = Dict([g => zeros(1, data_length_uc) for g in tech_names])
+ shut_down_costs = Dict([g => zeros(1, data_length_uc) for g in tech_names])
reserve_perc = Dict(g => Dict(s => zeros(1, data_length_ed) for s in String.(rt_products)) for g in tech_names)
+ inertia_perc = Dict([g => zeros(1, data_length_ed) for g in tech_names])
+
for g in tech_names
for product in only_da_products
reserve_perc[g][string(product)] = zeros(1, data_length_uc)
@@ -407,29 +902,28 @@ function create_simulation( sys_UC::PSY.System,
for tech in sys_techs
name = get_name(tech)
- capacity_factors[name][1, :] = get_realized_capacity_factors(tech, result_variables_ed)
+ capacity_factors[name][1, :] = get_realized_capacity_factors(tech, result_variables_ed, result_variables_uc)
+ start_up_costs[name][1, :] = get_start_costs(tech, result_variables_uc, data_length_uc)
+ shut_down_costs[name][1, :] = get_shut_costs(tech, result_variables_uc, data_length_uc)
services_ED = PSY.get_services(tech)
services_UC = PSY.get_services(PSY.get_components_by_name(PSY.Device, sys_UC, name)[1])
- services = append!(services_UC, services_ED)
-
- for service in services
+ for service in services_UC
update_realized_reserve_perc!(tech,
service,
result_variables_ed,
result_variables_uc,
reserve_perc,
+ inertia_perc,
rt_products,
only_da_products)
end
-
- open("model_ED_$(iteration_year).txt", "w") do f
- println(f, sim.problems["ED"].internal.optimization_container.JuMPmodel)
- end
-
end
- return energy_price, reserve_price, capacity_factors, reserve_perc;
+ for g in keys(inertia_perc)
+ inertia_perc[g] = inertia_perc[g] / PSY.get_base_power(sys_UC)
+ end
+ return energy_price, reserve_price, inertia_price, capacity_factors, reserve_perc, inertia_perc, start_up_costs, shut_down_costs, energy_voll, reserve_voll, inertia_voll;
end
diff --git a/src/representative_days/clustering.jl b/src/representative_days/clustering.jl
index 95635d7..000eb62 100644
--- a/src/representative_days/clustering.jl
+++ b/src/representative_days/clustering.jl
@@ -1,89 +1,91 @@
-
-function cluster(period_labels::AbstractVector,
- load::AbstractVector{Float64},
- wind::AbstractVector{Float64},
- pv::AbstractVector{Float64},
- n_clusters::Int)
-
- n_labels = length(period_labels)
- n_load = length(load)
- n_wind = length(wind)
- n_pv = length(pv)
-
- (n_labels == n_load == n_wind == n_pv) ||
- error("Time series inputs must have matching length")
-
- freqs = StatsBase.countmap(period_labels)
- periods = collect(keys(freqs))
- counts = values(freqs)
- period_ntimesteps = first(counts)
- all(x -> x == period_ntimesteps, counts) ||
- error("Each period must contain the same number of timesteps")
-
- n_periods = length(periods)
- period_key = Dict(zip(keys(freqs), 1:n_periods))
-
- # Create feature vectors for each period
- features = Matrix{Float64}(undef, 3 * period_ntimesteps, n_periods)
- idxs = 1:period_ntimesteps
- for p in keys(freqs)
- p_idx = period_key[p]
- include_timesteps = findall(isequal(p), period_labels)
- features[idxs, p_idx] = load[include_timesteps]
- features[period_ntimesteps .+ idxs, p_idx] = wind[include_timesteps]
- features[2*period_ntimesteps .+ idxs, p_idx] = pv[include_timesteps]
- end
-
- # Normalize feature vectors
- features .-= Statistics.mean(features, dims=2)
- stds = Statistics.std(features, dims=2)
- for i in 1:size(features, 1)
- stds[i] > 0 && (features[i, :] ./= stds[i])
- end
-
- # Create period distance matrix
- feature_dists = [LinearAlgebra.norm(ci .- cj, 2)
- for ci in eachcol(features), cj in eachcol(features)]
-
- result = Clustering.kmedoids(feature_dists, n_clusters)
- medoid_periods = getindex.(Ref(periods), result.medoids)
- cluster_nperiods = Dict(zip(medoid_periods, result.counts))
- period_clusters = Dict(zip(periods, result.assignments))
-
- return cluster_nperiods, period_clusters
-
-end
-
-function find_representative_days(simulation_dir::String,
- base_dir::String,
- n_clusters::Int64)
-
- representative_days_file = joinpath(base_dir, "representative_days_data_$(n_clusters).csv")
-
- # Use the same representative days if file already exists, otherwise create new clusters
- if isfile(representative_days_file)
- representative_days_data = read_data(representative_days_file)
- clusters = Dict(representative_days_data[i, "Date"] => representative_days_data[i, "Weight"] for i in 1:DataFrames.nrow(representative_days_data))
- else
- net_load_data = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data.csv"))
- labels = [Dates.Date(net_load_data[i, :Year], net_load_data[i, :Month], net_load_data[i, :Day]) for i in 1:DataFrames.nrow(net_load_data)]
- load = vec(sum(Matrix(net_load_data[:, r"load"]), dims=2))
- wind = zeros(length(load))
- pv = zeros(length(load))
-
- for i in names(net_load_data)
- if occursin("WIND", i) || occursin("WT", i)
- wind += net_load_data[:, i]
- elseif occursin("PV", i) || occursin("PVe", i)
- pv += net_load_data[:, i]
- end
- end
- clusters, allperiods = cluster(labels, load, wind, pv, n_clusters)
- representative_days_data = DataFrames.DataFrame(Date = Dates.Date[], Weight = Int64[])
- for i in keys(clusters)
- push!(representative_days_data, (i, clusters[i]))
- end
- CSV.write(representative_days_file, representative_days_data)
- end
- return clusters
-end
+
+function cluster(period_labels::AbstractVector,
+ load::AbstractVector{Float64},
+ wind::AbstractVector{Float64},
+ pv::AbstractVector{Float64},
+ n_clusters::Int)
+
+ n_labels = length(period_labels)
+ n_load = length(load)
+ n_wind = length(wind)
+ n_pv = length(pv)
+
+ (n_labels == n_load == n_wind == n_pv) ||
+ error("Time series inputs must have matching length")
+
+ freqs = StatsBase.countmap(period_labels)
+ periods = collect(keys(freqs))
+ counts = values(freqs)
+ period_ntimesteps = first(counts)
+ all(x -> x == period_ntimesteps, counts) ||
+ error("Each period must contain the same number of timesteps")
+
+ n_periods = length(periods)
+ period_key = Dict(zip(keys(freqs), 1:n_periods))
+
+ # Create feature vectors for each period
+ features = Matrix{Float64}(undef, 3 * period_ntimesteps, n_periods)
+ idxs = 1:period_ntimesteps
+ for p in keys(freqs)
+ p_idx = period_key[p]
+ include_timesteps = findall(isequal(p), period_labels)
+ features[idxs, p_idx] = load[include_timesteps]
+ features[period_ntimesteps .+ idxs, p_idx] = wind[include_timesteps]
+ features[2*period_ntimesteps .+ idxs, p_idx] = pv[include_timesteps]
+ end
+
+ # Normalize feature vectors
+ features .-= Statistics.mean(features, dims=2)
+ stds = Statistics.std(features, dims=2)
+ for i in 1:size(features, 1)
+ stds[i] > 0 && (features[i, :] ./= stds[i])
+ end
+
+ # Create period distance matrix
+ feature_dists = [LinearAlgebra.norm(ci .- cj, 2)
+ for ci in eachcol(features), cj in eachcol(features)]
+
+ result = Clustering.kmedoids(feature_dists, n_clusters)
+ medoid_periods = getindex.(Ref(periods), result.medoids)
+ cluster_nperiods = Dict(zip(medoid_periods, result.counts))
+ period_clusters = Dict(zip(periods, result.assignments))
+
+ return cluster_nperiods, period_clusters
+
+end
+
+function find_representative_days(simulation_dir::String,
+ test_system_dir::String,
+ base_dir::String,
+ n_clusters::Int64)
+
+ representative_days_file = joinpath(base_dir, "representative_days_data_$(n_clusters).csv")
+
+ # Use the same representative days if file already exists, otherwise create new clusters
+ if isfile(representative_days_file)
+ representative_days_data = read_data(representative_days_file)
+ clusters = Dict(representative_days_data[i, "Date"] => representative_days_data[i, "Weight"] for i in 1:DataFrames.nrow(representative_days_data))
+ else
+ net_load_data = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data.csv"))
+ labels = [Dates.Date(net_load_data[i, :Year], net_load_data[i, :Month], net_load_data[i, :Day]) for i in 1:DataFrames.nrow(net_load_data)]
+ load = vec(sum(Matrix(net_load_data[:, r"load"]), dims=2))
+ wind = zeros(length(load))
+ pv = zeros(length(load))
+
+ existing_generator_data = DataFrames.DataFrame(CSV.File(joinpath(test_system_dir, "RTS_Data", "SourceData", "gen.csv")))
+ for i in names(net_load_data)
+ if occursin("WIND", filter( row -> row."GEN UID" in [i], existing_generator_data)."Unit Type"[1]) #occursin("WIND", i) || occursin("WT", i)
+ wind += net_load_data[:, i]
+ elseif occursin("PV", filter( row -> row."GEN UID" in [i], existing_generator_data)."Unit Type"[1])#occursin("PV", i) || occursin("PVe", i)
+ pv += net_load_data[:, i]
+ end
+ end
+ clusters, allperiods = cluster(labels, load, wind, pv, n_clusters)
+ representative_days_data = DataFrames.DataFrame(Date = Dates.Date[], Weight = Int64[])
+ for i in keys(clusters)
+ push!(representative_days_data, (i, clusters[i]))
+ end
+ CSV.write(representative_days_file, representative_days_data)
+ end
+ return clusters
+end
diff --git a/src/resource_adequacy/PSY2PRAS.jl b/src/resource_adequacy/PSY2PRAS.jl
new file mode 100644
index 0000000..53ba7ff
--- /dev/null
+++ b/src/resource_adequacy/PSY2PRAS.jl
@@ -0,0 +1,692 @@
+#######################################################
+# Surya
+# NREL
+# January 2021
+# SIIP --> PRAS Linkage Module
+#######################################################
+# Loading the required packages
+#######################################################
+const PSY = PowerSystems
+#######################################################
+# Outage Information CSV
+#######################################################
+const OUTAGE_INFO_FILE =
+ joinpath(@__DIR__, "descriptors", "outage-rates-ERCOT-modified.csv")
+
+df_outage = DataFrames.DataFrame(CSV.File(OUTAGE_INFO_FILE));
+#######################################################
+# Structs to parse and store the outage information
+#######################################################
+struct outage_data
+ prime_mover::String
+ thermal_fuel::String
+ capacity::Int64
+ FOR::Float64
+ MTTR::Int64
+
+ outage_data(prime_mover = "PrimeMovers.Default", thermal_fuel ="ThermalFuels.Default", capacity = 100, FOR=0.5,MTTR = 50) =new(prime_mover,thermal_fuel,capacity,FOR,MTTR)
+end
+
+outage_values =[]
+for row in eachrow(df_outage)
+ push!(outage_values, outage_data(row.PrimeMovers,row.ThermalFuels,row.NameplateLimit_MW,(row.FOR/100),row.MTTR))
+end
+##############################################
+# Converting FOR and MTTR to λ and μ
+##############################################
+function outage_to_rate(outage_data::Tuple{Float64, Int64})
+ for_gen = outage_data[1]
+ mttr = outage_data[2]
+ if (mttr != 0)
+ μ = 1 / mttr
+ else
+ μ = 1.0
+ end
+ λ = (μ * for_gen) / (1 - for_gen)
+
+ return (λ = λ, μ = μ)
+end
+#######################################################
+# Main Function to make the PRAS System
+#######################################################
+function make_pras_system(sys::PSY.System;
+ system_model::Union{Nothing, String} = nothing,aggregation::Union{Nothing, String} = nothing,
+ period_of_interest::Union{Nothing, UnitRange} = nothing,outage_flag=true)
+ """
+ make_pras_system(psy_sys,system_model)
+
+ PSY System and System Model ("Single-Node","Zonal") are taken as arguments
+ and a PRAS SystemModel is returned.
+
+ ...
+ # Arguments
+ - `psy_sys::PSY.System`: PSY System
+ - `system_model::String`: "Single-Node" (or) "Zonal"
+ - `aggregation::String`: "Area" (or) "LoadZone" {Optional}
+ - `num_time_steps::UnitRange`: Number of timesteps of PRAS SystemModel {Optional}
+ ...
+
+ # Examples
+ ```julia-repl
+ julia> make_pras_system(psy_sys,system_model,period_of_interest)
+ PRAS SystemModel
+ ```
+ """
+ PSY.set_units_base_system!(sys, PSY.IS.UnitSystem.NATURAL_UNITS); # PRAS needs PSY System to be in NATURAL_UNITS
+ #######################################################
+ # Double counting of HybridSystem subcomponents
+ #######################################################
+ dup_uuids =[];
+ for h_s in PSY.get_components(PSY.HybridSystem, sys)
+ h_s_subcomps = PSY._get_components(h_s)
+ for subcomp in h_s_subcomps
+ push!(dup_uuids,PSY.IS.get_uuid(subcomp))
+ end
+ end
+ #######################################################
+ # kwargs Handling
+ #######################################################
+ if (aggregation === nothing)
+ aggregation_topology = "Area";
+ end
+
+ if (aggregation=="Area")
+ aggregation_topology = PSY.Area;
+
+ elseif (aggregation=="LoadZone")
+ aggregation_topology = PSY.LoadZone;
+ else
+ error("Unrecognized PSY AggregationTopology")
+ end
+
+ first_ts_temp = first(PSY.get_time_series_multiple(sys));
+
+ if (period_of_interest === nothing)
+ period_of_interest = range(1,length = length(first_ts_temp.data))
+ end
+
+ N = length(period_of_interest);
+
+ if ((N+(period_of_interest.start-1))>length(first_ts_temp.data))
+ error("Cannot make a PRAS System with $(N) timesteps with a PSY System with only $(length(first_ts_temp.data) - (period_of_interest.start-1)) timesteps of time series data")
+ end
+ if (period_of_interest.start > length(first_ts_temp.data) || period_of_interest.stop > length(first_ts_temp.data))
+ error("Please check the system period of interest selected")
+ end
+ #######################################################
+ # Aux Functions
+ # Function to get Line Rating
+ #######################################################
+ function line_rating(line::PSY.Line)
+ rate = PSY.get_rate(line);
+ return(forward_capacity = rate , backward_capacity = rate)
+ end
+
+ function line_rating(line::PSY.HVDCLine)
+ forward_capacity = getfield(PSY.get_active_power_limits_from(line), :max)
+ backward_capacity = getfield(PSY.get_active_power_limits_to(line), :max)
+ return(forward_capacity = forward_capacity, backward_capacity = backward_capacity)
+ end
+
+ #######################################################
+ # PRAS timestamps
+ # Need this to select timeseries values of interest
+ #######################################################
+ start_datetime = PSY.IS.get_initial_timestamp(first_ts_temp);
+ sys_res_in_hour = round(Dates.Millisecond(PSY.get_time_series_resolution(sys)), Dates.Hour);
+ start_datetime = start_datetime + Dates.Hour((period_of_interest.start-1)*sys_res_in_hour);
+ start_datetime_tz = TimeZones.ZonedDateTime(start_datetime,TimeZones.tz"UTC");
+ finish_datetime_tz = start_datetime_tz + Dates.Hour((N-1)*sys_res_in_hour);
+ my_timestamps = StepRange(start_datetime_tz, Dates.Hour(sys_res_in_hour), finish_datetime_tz);
+ @info "The first timestamp of PRAS System being built is : $(start_datetime_tz) and last timestamp is : $(finish_datetime_tz) "
+ #######################################################
+ # PRAS Regions - Areas in SIIP
+ #######################################################
+ @info "Processing Regions in PSY System... "
+ regions = collect(PSY.get_components(aggregation_topology, sys));
+ if (length(regions)!=0)
+ @info "The PSY System has $(length(regions)) regions based on PSY AggregationTopology : $(aggregation_topology)."
+ else
+ error("No regions in the PSY System. Cannot proceed with the process of making a PRAS SystemModel.")
+ end
+
+ region_names = PSY.get_name.(regions);
+ num_regions = length(region_names);
+
+ region_load = Array{Int64,2}(undef,num_regions,N);
+
+ # for (idx,region) in enumerate(regions)
+ # region_load[idx,:]=floor.(Int,sum(PSY.get_time_series_values.(PSY.SingleTimeSeries,PSY.get_components_in_aggregation_topology(PSY.PowerLoad, sys, region),
+ # "max_active_power",start_time = start_datetime,len=N)));
+ # end
+ finish_datetime = start_datetime + Dates.Hour((N-1)*sys_res_in_hour);
+ my_timestamps_notz = StepRange(start_datetime, Dates.Hour(sys_res_in_hour), finish_datetime);
+ for (idx,region) in enumerate(regions)
+ forecast = PSY.get_time_series(PSY.Deterministic,PSY.get_components_in_aggregation_topology(PSY.PowerLoad, sys, region)[1],"max_active_power")
+ region_load_max_active_power = PSY.get_max_active_power(PSY.get_components_in_aggregation_topology(PSY.PowerLoad, sys, region)[1])
+ for idx_ts in 1:N
+ region_load[idx,idx_ts]=round(Int,values(PSY.get_window(forecast, my_timestamps_notz[idx_ts]; len=1))[1]*region_load_max_active_power);
+ end
+ end
+
+ new_regions = PRAS.Regions{N,PRAS.MW}(region_names, region_load);
+
+ #######################################################
+ # kwargs Handling
+ #######################################################
+ if (system_model === nothing)
+ if (num_regions>1)
+ system_model = "Zonal"
+ else
+ system_model = "Single-Node"
+ end
+ end
+ #######################################################
+ # Generator Region Indices
+ #######################################################
+ gens=[];
+ start_id = Array{Int64}(undef,num_regions);
+ region_gen_idxs = Array{UnitRange{Int64},1}(undef,num_regions);
+
+ for (idx,region) in enumerate(regions)
+ #gs= [g for g in PSY.get_components_in_aggregation_topology(PSY.Generator, sys, region) if (typeof(g) != PSY.HydroEnergyReservoir && PSY.get_max_active_power(g)!=0)]
+ gs= [g for g in PSY.get_components_in_aggregation_topology(PSY.Generator, sys, region) if (typeof(g) != PSY.HydroEnergyReservoir && PSY.get_max_active_power(g)!=0 && PSY.IS.get_uuid(g) ∉ dup_uuids)]
+ push!(gens,gs)
+ idx==1 ? start_id[idx] = 1 : start_id[idx] =start_id[idx-1]+length(gens[idx-1])
+ region_gen_idxs[idx] = range(start_id[idx], length=length(gens[idx]))
+ end
+
+ #######################################################
+ # Storages Region Indices
+ #######################################################
+ stors=[];
+ start_id = Array{Int64}(undef,num_regions);
+ region_stor_idxs = Array{UnitRange{Int64},1}(undef,num_regions);
+
+ for (idx,region) in enumerate(regions)
+ #push!(stors,[s for s in PSY.get_components_in_aggregation_topology(PSY.Storage, sys, region)])
+ push!(stors,[s for s in PSY.get_components_in_aggregation_topology(PSY.Storage, sys, region) if (PSY.IS.get_uuid(s) ∉ dup_uuids)])
+ idx==1 ? start_id[idx] = 1 : start_id[idx] =start_id[idx-1]+length(stors[idx-1])
+ region_stor_idxs[idx] = range(start_id[idx], length=length(stors[idx]))
+ end
+
+ #######################################################
+ # GeneratorStorages Region Indices
+ #######################################################
+ gen_stors=[];
+ start_id = Array{Int64}(undef,num_regions);
+ region_genstor_idxs = Array{UnitRange{Int64},1}(undef,num_regions);
+
+ for (idx,region) in enumerate(regions)
+ gs= [g for g in PSY.get_components_in_aggregation_topology(PSY.StaticInjection, sys, region) if (typeof(g) == PSY.HydroEnergyReservoir || typeof(g)==PSY.HybridSystem)]
+ push!(gen_stors,gs)
+ idx==1 ? start_id[idx] = 1 : start_id[idx] =start_id[idx-1]+length(gen_stors[idx-1])
+ region_genstor_idxs[idx] = range(start_id[idx], length=length(gen_stors[idx]))
+ end
+
+ #######################################################
+ # PRAS Generators
+ #######################################################
+ @info "Processing Generators in PSY System... "
+
+ gen=[];
+ for i in 1: num_regions
+ if (length(gens[i]) != 0)
+ append!(gen,gens[i])
+ end
+ end
+
+ if(length(gen) ==0)
+ gen_names = String[];
+ else
+ gen_names = PSY.get_name.(gen);
+ end
+
+ gen_categories = string.(typeof.(gen));
+ n_gen = length(gen_names);
+
+ gen_cap_array = Matrix{Int64}(undef, n_gen, N);
+ λ_gen = Matrix{Float64}(undef, n_gen, N);
+ μ_gen = Matrix{Float64}(undef, n_gen, N);
+
+ for (idx,g) in enumerate(gen)
+ # if (PSY.has_time_series(g) && ("max_active_power" in PSY.get_time_series_names(PSY.SingleTimeSeries,g)))
+ if (PSY.has_time_series(g) && ("max_active_power" in PSY.get_time_series_names(PSY.Deterministic,g)))
+ # gen_cap_array[idx,:] = floor.(Int,PSY.get_time_series_values(PSY.SingleTimeSeries,g,"max_active_power",start_time = start_datetime,len=N));
+ forecast = PSY.get_time_series(PSY.Deterministic,g,"max_active_power")
+ gen_max_power = PSY.get_max_active_power(g)
+ for idx_ts in 1:N
+ gen_cap_array[idx,idx_ts]=round(Int,values(PSY.get_window(forecast, my_timestamps_notz[idx_ts]; len=1))[1]*gen_max_power);
+ end
+ else
+ gen_cap_array[idx,:] = fill.(floor.(Int,PSY.get_max_active_power(g)),1,N);
+ end
+ if (~outage_flag)
+ if (gen_categories[idx] == "PowerSystems.ThermalStandard") || (gen_categories[idx] == "PowerSystemExtensions.ThermalCleanEnergy") || (gen_categories[idx] == "ThermalFastStartSIIP")
+ p_m = string(PSY.get_prime_mover(g))
+ fl = string(PSY.get_fuel(g))
+
+ p_m_idx = findall(x -> x == p_m, getfield.(outage_values,:prime_mover))
+ fl_idx = findall(x -> x == fl, getfield.(outage_values[p_m_idx],:thermal_fuel))
+
+ if (length(fl_idx) ==0)
+ fl_idx = findall(x -> x == "NA", getfield.(outage_values[p_m_idx],:thermal_fuel))
+ end
+
+ temp_range = p_m_idx[fl_idx]
+
+ temp_cap = floor(Int,PSY.get_max_active_power(g))
+
+ if (length(temp_range)>1)
+ for (x,y) in zip(temp_range,getfield.(outage_values[temp_range],:capacity))
+ temp=0
+ if (temp x == p_m, getfield.(outage_values,:prime_mover))
+
+ temp_cap = floor(Int,PSY.get_max_active_power(g))
+
+ if (length(p_m_idx)>1)
+ for (x,y) in zip(p_m_idx,getfield.(outage_values[p_m_idx],:capacity))
+ temp=0
+ if (temp x == p_m, getfield.(outage_values,:prime_mover))
+
+ temp_cap = floor(Int,PSY.get_max_active_power(g_s))
+
+ if (length(p_m_idx)>1)
+ for (x,y) in zip(p_m_idx,getfield.(outage_values[p_m_idx],:capacity))
+ temp=0
+ if (temp typeof(x) != PSY.TapTransformer));
+ mapping_dict = PSY.get_aggregation_topology_mapping(aggregation_topology,sys); # Dict with mapping from Areas to Bus_Names
+ new_mapping_dict=Dict{String,Array{Int64,1}}();
+
+ for key in keys(mapping_dict)
+ push!(new_mapping_dict, key => PSY.get_number.(mapping_dict[key]))
+ end
+
+ #######################################################
+ # Finding the inter-regional lines and regions_from
+ #######################################################
+ regional_lines = [];
+ regions_from = [];
+ for i in 1:length(line)
+ for key in keys(mapping_dict)
+ if(PSY.get_number(line[i].arc.from) in new_mapping_dict[key])
+ if(~(PSY.get_number(line[i].arc.to) in new_mapping_dict[key]))
+ push!(regional_lines,line[i])
+ push!(regions_from,key)
+ end
+ end
+ end
+ end
+
+ #######################################################
+ # Finding the regions_to
+ #######################################################
+ n_lines = length(regional_lines);
+ regions_to = [];
+ for i in 1:n_lines
+ for key in keys(mapping_dict)
+ if(PSY.get_number(regional_lines[i].arc.to) in new_mapping_dict[key])
+ push!(regions_to,key)
+ end
+ end
+ end
+
+ #######################################################
+ # If there are lines from region1 --> region3 and
+ # region3 --> region1; lines like these need to be grouped
+ # to make interface_line_idxs
+ #######################################################
+ regions_tuple = [];
+ for i in 1:length(regions_from)
+ region_from_idx = findfirst(x->x==regions_from[i],region_names)
+ region_to_idx = findfirst(x->x==regions_to[i],region_names)
+ if (region_from_idx < region_to_idx)
+ push!(regions_tuple,(regions_from[i],regions_to[i]))
+ else
+ push!(regions_tuple,(regions_to[i],regions_from[i]))
+ end
+ end
+
+ temp_regions_tuple = unique(regions_tuple);
+ interface_dict = Dict();
+
+ for i in 1: length(temp_regions_tuple)
+ temp = findall(x -> x == temp_regions_tuple[i], regions_tuple);
+ push!(interface_dict, temp_regions_tuple[i] => (temp,length(temp)))
+ end
+
+ num_interfaces = length(temp_regions_tuple);
+ sorted_regional_lines = [];
+ interface_line_idxs = Array{UnitRange{Int64},1}(undef,num_interfaces);
+ start_id = Array{Int64}(undef,num_interfaces);
+ for i in 1: num_interfaces
+ for j in interface_dict[temp_regions_tuple[i]][1]
+ push!(sorted_regional_lines, regional_lines[j])
+ end
+ i==1 ? start_id[i] = 1 : start_id[i] =start_id[i-1]+interface_dict[temp_regions_tuple[i-1]][2]
+ interface_line_idxs[i] = range(start_id[i], length=interface_dict[temp_regions_tuple[i]][2])
+ end
+
+ @info "Processing all inter regional lines in PSY System..."
+ line_names = PSY.get_name.(sorted_regional_lines);
+ line_categories = string.(typeof.(sorted_regional_lines));
+
+ line_forward_capacity_array = Matrix{Int64}(undef, n_lines, N);
+ line_backward_capacity_array = Matrix{Int64}(undef, n_lines, N);
+
+ λ_lines = Matrix{Float64}(undef, n_lines, N); # Not currently available/ defined in PowerSystems
+ μ_lines = Matrix{Float64}(undef, n_lines, N); # Not currently available/ defined in PowerSystems
+ for i in 1:n_lines
+ line_forward_capacity_array[i,:] = fill.(floor.(Int,getfield(line_rating(sorted_regional_lines[i]),:forward_capacity)),1,N);
+ line_backward_capacity_array[i,:] = fill.(floor.(Int,getfield(line_rating(sorted_regional_lines[i]),:backward_capacity)),1,N);
+
+ λ_lines[i,:] .= 0.0; # Not currently available/ defined in PowerSystems # should change when we have this
+ μ_lines[i,:] .= 1.0; # Not currently available/ defined in PowerSystems
+ end
+
+ new_lines = PRAS.Lines{N,1,PRAS.Hour,PRAS.MW}(line_names, line_categories, line_forward_capacity_array, line_backward_capacity_array, λ_lines ,μ_lines);
+ #######################################################
+ # PRAS Interfaces
+ #######################################################
+ interface_regions_from = [findfirst(x->x==temp_regions_tuple[i][1],region_names) for i in 1:num_interfaces];
+ interface_regions_to = [findfirst(x->x==temp_regions_tuple[i][2],region_names) for i in 1:num_interfaces];
+
+ @info "Processing interfaces from inter regional lines in PSY System..."
+ interface_forward_capacity_array = Matrix{Int64}(undef, num_interfaces, N);
+ interface_backward_capacity_array = Matrix{Int64}(undef, num_interfaces, N);
+ for i in 1:num_interfaces
+ interface_forward_capacity_array[i,:] = sum(line_forward_capacity_array[interface_line_idxs[i],:],dims=1)
+ interface_backward_capacity_array[i,:] = sum(line_backward_capacity_array[interface_line_idxs[i],:],dims=1)
+ end
+
+ new_interfaces = PRAS.Interfaces{N,PRAS.MW}(interface_regions_from, interface_regions_to, interface_forward_capacity_array, interface_backward_capacity_array);
+
+ pras_system = PRAS.SystemModel(new_regions, new_interfaces, new_generators, region_gen_idxs, new_storage, region_stor_idxs, new_gen_stors,
+ region_genstor_idxs, new_lines,interface_line_idxs,my_timestamps);
+
+ elseif (system_model =="Single-Node")
+ load_vector = vec(sum(region_load,dims=1));
+ pras_system = PRAS.SystemModel(new_generators, new_storage, new_gen_stors, my_timestamps, load_vector);
+ else
+ error("Unrecognized SystemModel; Please specify correctly if SystemModel is Single-Node or Zonal.")
+ end
+ @info "Successfully built a PRAS $(system_model) system of type $(typeof(pras_system))."
+ return pras_system
+end
+#=
+dup_uuids =[];
+for h_s in PSY.get_components(PSY.HybridSystem, sys)
+ h_s_subcomps = PSY._get_components(h_s)
+ for subcomp in h_s_subcomps
+ push!(dup_uuids,PSY.IS.get_uuid(subcomp))
+ end
+end
+ get_components(ThermalGen, sys, x -> get_uuid(x) ∉ subcomponents)
+
+ h_s = PSY.get_components(PSY.HybridSystem, sys)
+ subcomponents =Set(PSY.IS.get_uuid.(PSY._get_components.(h_s)))
+ get_components(ThermalGen, sys, x -> get_uuid(x) ∉ subcomponents)
+
+ dup_uuids = PSY.IS.get_uuid.([component for component in PSY._get_components.(PSY.get_components(PSY.HybridSystem, sys))])
+=#
diff --git a/src/resource_adequacy/conv.jl b/src/resource_adequacy/conv.jl
new file mode 100644
index 0000000..c0c66a9
--- /dev/null
+++ b/src/resource_adequacy/conv.jl
@@ -0,0 +1,121 @@
+CapacityDistribution = Distributions.DiscreteNonParametric{Int,Float64,Vector{Int},Vector{Float64}}
+
+
+
+function assess(distr::CapacityDistribution)
+
+ xs = support(distr)
+ ps = probs(distr)
+
+ i = 1
+ lolp = 0.
+ eul = 0.
+
+ while i <= length(xs)
+
+ xs[i] >= 0 && break
+ lolp += ps[i]
+ eul -= ps[i] * xs[i]
+ i += 1
+
+ end
+
+ return lolp, eul
+
+end
+
+function spconv(hvsraw::AbstractVector{Int}, hpsraw::AbstractVector{Float64})
+
+ zeroidxs = hvsraw .!= 0
+ hvs = hvsraw[zeroidxs]
+ hps = hpsraw[zeroidxs]
+
+ length(hvs) == 0 &&
+ return Distributions.DiscreteNonParametric([0], [1.], check_args=false)
+
+ max_n = sum(hvs) + 1
+ current_probs = Vector{Float64}(undef, max_n)
+ prev_probs = Vector{Float64}(undef, max_n)
+ current_values = Vector{Int}(undef, max_n)
+ prev_values = Vector{Int}(undef, max_n)
+
+ current_n = 2
+ current_values[1:current_n] = [0, hvs[1]]
+ current_probs[1:current_n] = [1 - hps[1], hps[1]]
+
+ for (hv, hp) in zip(hvs[2:end], hps[2:end])
+ current_values, current_probs, current_n, prev_values, prev_probs =
+ spconv!(prev_values, prev_probs, hv, hp,
+ current_values, current_probs, current_n)
+ end
+
+ resize!(current_values, current_n)
+ resize!(current_probs, current_n)
+ nonzeroprob_idxs = findall(x -> x>0, current_probs)
+
+ return Distributions.DiscreteNonParametric(
+ current_values[nonzeroprob_idxs],
+ current_probs[nonzeroprob_idxs],
+ check_args=false)
+
+end
+
+function spconv!(y_values::Vector{Int}, y_probs::Vector{Float64},
+ h_value::Int, h_prob::Float64,
+ x_values::Vector{Int}, x_probs::Vector{Float64}, nx::Int)
+
+ h_q = 1 - h_prob
+
+ ix = ixsh = 1
+ iy = 0
+ lastval = -1
+
+ @inbounds while ix <= nx
+
+ x = x_values[ix]
+ xsh = x_values[ixsh] + h_value
+
+ if lastval == x
+ @fastmath y_probs[iy] += h_q * x_probs[ix]
+ ix += 1
+
+ elseif lastval == xsh
+ @fastmath y_probs[iy] += h_prob * x_probs[ixsh]
+ ixsh += 1
+
+ elseif x == xsh
+ iy += 1
+ y_values[iy] = x
+ @fastmath y_probs[iy] = h_q * x_probs[ix] + h_prob * x_probs[ixsh]
+ lastval = x
+ ix += 1
+ ixsh += 1
+
+ elseif x < xsh
+ iy += 1
+ y_values[iy] = x
+ @fastmath y_probs[iy] = h_q * x_probs[ix]
+ lastval = x
+ ix += 1
+
+ elseif xsh < x
+ iy += 1
+ y_values[iy] = xsh
+ @fastmath y_probs[iy] = h_prob * x_probs[ixsh]
+ lastval = xsh
+ ixsh += 1
+
+ end
+
+ end
+
+ @inbounds while ixsh <= nx
+ iy += 1
+ y_values[iy] = x_values[ixsh] + h_value
+ @fastmath y_probs[iy] = h_prob * x_probs[ixsh]
+ ixsh += 1
+ end
+
+ return y_values, y_probs, iy, x_values, x_probs
+
+end
diff --git a/src/resource_adequacy/descriptors/cc_restrictions.json b/src/resource_adequacy/descriptors/cc_restrictions.json
new file mode 100644
index 0000000..4ebcd94
--- /dev/null
+++ b/src/resource_adequacy/descriptors/cc_restrictions.json
@@ -0,0 +1 @@
+{"TEXAS_CITY_POWER":["TEXAS_CITY_POWER_CC9","TEXAS_CITY_POWER_CC1","TEXAS_CITY_POWER_CC3","TEXAS_CITY_POWER_CC6","TEXAS_CITY_POWER_CC7"],"PASADENA_COGENERATION":["PASADENA_COGENERATION_CC1","PASADENA_COGENERATION_CC2"],"BASTROP_ENERGY_CENTER":["BASTROP_ENERGY_CENTER_CC1","BASTROP_ENERGY_CENTER_CC2","BASTROP_ENERGY_CENTER_CC3"],"COLORADO_BEND_ENERGY_CENTER_1":["COLORADO_BEND_ENERGY_CENTER_1_CC2"],"TENASKA_FRONTIER_STATION":["TENASKA_FRONTIER_STATION_CC1","TENASKA_FRONTIER_STATION_CC4","TENASKA_FRONTIER_STATION_CC9","TENASKA_FRONTIER_STATION_CC12"],"HIDALGO_ENERGY_CENTER":["HIDALGO_ENERGY_CENTER_CC1","HIDALGO_ENERGY_CENTER_CC2","HIDALGO_ENERGY_CENTER_CC3"],"GUADALUPE_ENERGY_CENTER2":["GUADALUPE_ENERGY_CENTER2_CC1","GUADALUPE_ENERGY_CENTER2_CC2","GUADALUPE_ENERGY_CENTER2_CC4"],"GREENS_BAYOU":["GREENS_BAYOU_CC2","GREENS_BAYOU_CC3"],"BAYTOWN_ENERGY2":["BAYTOWN_ENERGY2_CC1","BAYTOWN_ENERGY2_CC2"],"LAMAR_ENERGY_CENTER2":["LAMAR_ENERGY_CENTER2_CC1","LAMAR_ENERGY_CENTER2_CC2","LAMAR_ENERGY_CENTER2_CC4"],"RIO_NOGALES_POWER":["RIO_NOGALES_POWER_CC1","RIO_NOGALES_POWER_CC2","RIO_NOGALES_POWER_CC3","RIO_NOGALES_POWER_CC4"],"FREESTONE_ENERGY_CENTER1":["FREESTONE_ENERGY_CENTER1_CC1","FREESTONE_ENERGY_CENTER1_CC2","FREESTONE_ENERGY_CENTER1_CC3"],"QUAIL_RUN_ENERGY2":["QUAIL_RUN_ENERGY2_CC1","QUAIL_RUN_ENERGY2_CC2","QUAIL_RUN_ENERGY2_CC4"],"FREESTONE_ENERGY_CENTER2":["FREESTONE_ENERGY_CENTER2_CC1","FREESTONE_ENERGY_CENTER2_CC2","FREESTONE_ENERGY_CENTER2_CC3"],"ENNIS_POWER_STATION":["ENNIS_POWER_STATION_CC1","ENNIS_POWER_STATION_CC2"],"INGLESIDE":["INGLESIDE_CC1","INGLESIDE_CC2","INGLESIDE_CC3","INGLESIDE_CC4","INGLESIDE_CC5","INGLESIDE_CC6"],"CHANNEL_ENERGY_CENTER2":["CHANNEL_ENERGY_CENTER2_CC1","CHANNEL_ENERGY_CENTER2_CC3","CHANNEL_ENERGY_CENTER2_CC4"],"CEDAR_BAYOU":["CEDAR_BAYOU_CC1","CEDAR_BAYOU_CC2","CEDAR_BAYOU_CC4"],"SILAS_RAY":["SILAS_RAY_CC1","SILAS_RAY_CC2"],"GUADALUPE_ENERGY_CENTER1":["GUADALUPE_ENERGY_CENTER1_CC1","GUADALUPE_ENERGY_CENTER1_CC3","GUADALUPE_ENERGY_CENTER1_CC4"],"CHANNEL_ENERGY_CENTER1":["CHANNEL_ENERGY_CENTER1_CC1","CHANNEL_ENERGY_CENTER1_CC2"],"ARTHUR_VON_ROSENBERG":["ARTHUR_VON_ROSENBERG_CC1","ARTHUR_VON_ROSENBERG_CC2"],"SAM_RAYBURN":["SAM_RAYBURN_CC1","SAM_RAYBURN_CC2","SAM_RAYBURN_CC3"],"PANDA_TEMPLE_I":["PANDA_TEMPLE_I_CC1","PANDA_TEMPLE_I_CC2","PANDA_TEMPLE_I_CC3"],"POWER_SYSTEMS_ARCO_COGEN3":["POWER_SYSTEMS_ARCO_COGEN3_CC1","POWER_SYSTEMS_ARCO_COGEN3_CC2"],"BARNEY_DAVIS":["BARNEY_DAVIS_CC1","BARNEY_DAVIS_CC2","BARNEY_DAVIS_CC3"],"LAMAR_ENERGY_CENTER1":["LAMAR_ENERGY_CENTER1_CC1","LAMAR_ENERGY_CENTER1_CC2","LAMAR_ENERGY_CENTER1_CC4"],"POWER_SYSTEMS_ARCO_COGEN1":["POWER_SYSTEMS_ARCO_COGEN1_CC1","POWER_SYSTEMS_ARCO_COGEN1_CC2"],"BRAZOS_VALLEY":["BRAZOS_VALLEY_CC1","BRAZOS_VALLEY_CC2","BRAZOS_VALLEY_CC3","BRAZOS_VALLEY_CC4"],"COLORADO_BEND_ENERGY_CENTER_2":["COLORADO_BEND_ENERGY_CENTER_2_CC1","COLORADO_BEND_ENERGY_CENTER_2_CC2"],"LOST_PINES_POWER":["LOST_PINES_POWER_CC1","LOST_PINES_POWER_CC2"],"CORPUS_CHRISTI":["CORPUS_CHRISTI_CC2","CORPUS_CHRISTI_CC4"],"BAYTOWN_ENERGY1":["BAYTOWN_ENERGY1_CC1","BAYTOWN_ENERGY1_CC2"],"VICTORIA_POWER1":["VICTORIA_POWER2_CC1"],"CHANNELVIEW_COGENERATION_PLANT":["CHANNELVIEW_COGENERATION_PLANT_CC1","CHANNELVIEW_COGENERATION_PLANT_CC2","CHANNELVIEW_COGENERATION_PLANT_CC5","CHANNELVIEW_COGENERATION_PLANT_CC6"],"SANDHILL_ENERGY_CENTER":["SANDHILL_ENERGY_CENTER_CC1","SANDHILL_ENERGY_CENTER_CC2","SANDHILL_ENERGY_CENTER_CC3"],"QUAIL_RUN_ENERGY1":["QUAIL_RUN_ENERGY1_CC1","QUAIL_RUN_ENERGY1_CC2","QUAIL_RUN_ENERGY1_CC4"],"FORNEY_ENERGY_CENTER1":["FORNEY_ENERGY_CENTER1_CC1","FORNEY_ENERGY_CENTER1_CC2","FORNEY_ENERGY_CENTER1_CC3"],"TH_WARTON2":["TH_WARTON2_CC2","TH_WARTON2_CC4","TH_WARTON2_CC7","TH_WARTON2_CC8"],"JACK_COUNTY_GEN_FACILITY_2":["JACK_COUNTY_GEN_FACILITY_2_CC1","JACK_COUNTY_GEN_FACILITY_2_CC2","JACK_COUNTY_GEN_FACILITY_2_CC3"],"COLORADO_BEND_ENERGY_CENTER_3":["COLORADO_BEND_ENERGY_CENTER_3_CC1","COLORADO_BEND_ENERGY_CENTER_3_CC2"],"CAPITOL_GEN":["CAPITOL_GEN_CC1","CAPITOL_GEN_CC2","CAPITOL_GEN_CC3"],"WICHITA_FALLS_COGEN":["WICHITA_FALLS_COGEN_CC6","WICHITA_FALLS_COGEN_CC8","WICHITA_FALLS_COGEN_CC9"],"PANDA_SHERMAN":["PANDA_SHERMAN_CC1","PANDA_SHERMAN_CC2","PANDA_SHERMAN_CC3"],"POWER_SYSTEMS_ARCO_COGEN2":["POWER_SYSTEMS_ARCO_COGEN2_CC1","POWER_SYSTEMS_ARCO_COGEN2_CC2"],"NUECES_BAY_REPOWER":["NUECES_BAY_REPOWER_CC1","NUECES_BAY_REPOWER_CC2"],"WOLF_HOLLOW_1":["WOLF_HOLLOW_1_CC1","WOLF_HOLLOW_1_CC2","WOLF_HOLLOW_2_CC2"],"ODESSA_ECTOR_POWER1":["ODESSA_ECTOR_POWER1_CC2","ODESSA_ECTOR_POWER1_CC4"],"PANDA_TEMPLE_II":["PANDA_TEMPLE_II_CC1","PANDA_TEMPLE_II_CC2"],"TH_WARTON1":["TH_WARTON1_CC2","TH_WARTON1_CC3","TH_WARTON1_CC4","TH_WARTON1_CC7","TH_WARTON1_CC8"],"WISE_TRACTEBEL_POWER":["WISE_TRACTEBEL_POWER_CC1","WISE_TRACTEBEL_POWER_CC2","WISE_TRACTEBEL_POWER_CC3"],"DEER_PARK_ENERGY_CENTER":["DEER_PARK_ENERGY_CENTER_CC1","DEER_PARK_ENERGY_CENTER_CC2","DEER_PARK_ENERGY_CENTER_CC4","DEER_PARK_ENERGY_CENTER_CC5","DEER_PARK_ENERGY_CENTER_CC6"],"PARIS_ENERGY_CENTER":["PARIS_ENERGY_CENTER_CC1","PARIS_ENERGY_CENTER_CC2"],"JACK_COUNTY_GEN_FACILITY_1":["JACK_COUNTY_GEN_FACILITY_1_CC1","JACK_COUNTY_GEN_FACILITY_1_CC2","JACK_COUNTY_GEN_FACILITY_1_CC3"],"ODESSA_ECTOR_POWER2":["ODESSA_ECTOR_POWER2_CC2","ODESSA_ECTOR_POWER2_CC4"],"FORNEY_ENERGY_CENTER2":["FORNEY_ENERGY_CENTER2_CC1","FORNEY_ENERGY_CENTER2_CC2","FORNEY_ENERGY_CENTER2_CC3"],"TENASKA_GATEWAY_STATION":["TENASKA_GATEWAY_STATION_CC1","TENASKA_GATEWAY_STATION_CC2","TENASKA_GATEWAY_STATION_CC3","TENASKA_GATEWAY_STATION_CC4"]}
\ No newline at end of file
diff --git a/src/resource_adequacy/descriptors/outage-rates-ERCOT-modified.csv b/src/resource_adequacy/descriptors/outage-rates-ERCOT-modified.csv
new file mode 100644
index 0000000..af314fb
--- /dev/null
+++ b/src/resource_adequacy/descriptors/outage-rates-ERCOT-modified.csv
@@ -0,0 +1,34 @@
+tech,PrimeMovers,ThermalFuels,NameplateLimit_MW,FOR,POR,MTTR
+coal_st,ST,COAL,99,6.92,6.64,44
+coal_st,ST,COAL,199,8.64,7.78,44
+coal_st,ST,COAL,299,10.66,8.52,44
+coal_st,ST,COAL,399,9.46,9.52,44
+coal_st,ST,COAL,599,9.22,9.34,44
+coal_st,ST,COAL,799,8.2,8.85,44
+coal_st,ST,COAL,999,8.87,9.82,44
+coal_st,ST,COAL,9999,12.17,13.63,44
+oil_st,ST,NA,99,12.27,4.24,44
+oil_st,ST,NA,199,9.87,7.21,44
+oil_st,ST,NA,299,3.32,5.08,44
+oil_st,ST,NA,399,5.15,11.71,44
+oil_st,ST,NA,599,6.9,8.58,44
+oil_st,ST,NA,799,6.46,11.1,44
+oil_st,ST,NA,999,4.72,16.32,44
+gas_st,ST,NATURAL_GAS,99,10.59,6.79,44
+gas_st,ST,NATURAL_GAS,199,7.15,10.44,44
+gas_st,ST,NATURAL_GAS,299,7.35,9.87,44
+gas_st,ST,NATURAL_GAS,399,7.04,12.16,44
+gas_st,ST,NATURAL_GAS,599,12.82,11.74,44
+gas_st,ST,NATURAL_GAS,799,11.57,18.67,44
+gas_st,ST,NATURAL_GAS,999,5.57,9.95,44
+nuclear,ST,NUCLEAR,799,2.36,4.95,126
+nuclear,ST,NUCLEAR,999,1.51,5.2,126
+nuclear,ST,NUCLEAR,9999,1.94,5.84,126
+gt,GT,NATURAL_GAS,19,9.27,2.97,31
+gt,GT,NATURAL_GAS,49,7.1,3.27,31
+gt,GT,NATURAL_GAS,9999,5.81,4.78,31
+cc,CC,NATURAL_GAS,9999,3.93,8.02,21
+hydro,HY,NA,29,13.09,11,22
+hydro,HY,NA,9999,5.15,11.6,22
+ct,CT,NATURAL_GAS,9999,3.93,8.02,31
+re_ct,CT,OTHER_GAS,9999,3.93,8.02,31
diff --git a/src/resource_adequacy/generator_unavailability.jl b/src/resource_adequacy/generator_unavailability.jl
new file mode 100644
index 0000000..fa3f731
--- /dev/null
+++ b/src/resource_adequacy/generator_unavailability.jl
@@ -0,0 +1,84 @@
+function construct_smc_unavailabilities(sys::PSY.System, ordc_unavailability_method::String)
+ if ordc_unavailability_method == "SMC"
+ system_period_of_interest = range(1, length = 8784);
+ pras_system = make_pras_system(sys,
+ system_model = "Single-Node",
+ aggregation = "Area",
+ period_of_interest = system_period_of_interest,
+ outage_flag = true);
+
+ nsamples = 100
+ timeseries = unavailabilities(pras_system, nsamples)
+ else
+ timeseries = nothing
+ end
+ return timeseries
+end
+
+
+function unavailabilities(
+ sys::SystemModel{N,L,T,P,E}, nsamples::Int
+) where {N,L,T,P,E}
+
+ resultspecs = (GeneratorAvailability(),
+ StorageAvailability(), GeneratorStorageAvailability(),
+ StorageEnergySamples(), GeneratorStorageEnergySamples())
+
+ gen_av, stor_av, genstor_av, stor_soc, genstor_soc =
+ PRAS.assess(sys, SequentialMonteCarlo(samples=nsamples), resultspecs...)
+
+ result = Matrix{Float64}(undef, nsamples, N)
+
+ for t in 1:N
+ for s in 1:nsamples
+
+ gen_unavailable =
+ sum(sys.generators.capacity[.!gen_av.available[:, t, s], t])
+
+ stor_unavailable = 0
+ for st in 1:length(sys.storages)
+
+ efficiency = sys.storages.discharge_efficiency[st,t]
+ capacitylimit = sys.storages.discharge_capacity[st,t]
+ energylimit = if t > 1
+ energytopower(stor_soc.energy[st,t-1,s], E, L, T, P) * efficiency
+ else
+ 0
+ end
+
+ if !stor_av.available[st, t, s]
+ stor_unavailable += capacitylimit
+ elseif energylimit < capacitylimit
+ stor_unavailable += capacitylimit - energylimit
+ end
+
+ end
+
+ genstor_unavailable = 0
+ for gs in 1:length(sys.generatorstorages)
+
+ efficiency = sys.generatorstorages.discharge_efficiency[gs,t]
+ capacitylimit = sys.generatorstorages.gridinjection_capacity[gs,t]
+ inflow = sys.generatorstorages.inflow[gs,t]
+ energylimit = if t > 1
+ energytopower(genstor_soc.energy[gs,t-1,s], E, L, T, P) * efficiency
+ else
+ 0
+ end
+ capacityavailable = energylimit + inflow
+
+ if !stor_av.available[gs, t, s]
+ genstor_unavailable += capacitylimit
+ elseif capacityavailable < capacitylimit
+ genstor_unavailable += capacitylimit - capacityavailable
+ end
+
+ end
+
+ result[s, t] = gen_unavailable + stor_unavailable + genstor_unavailable
+
+ end
+ end
+
+ return result
+end
diff --git a/src/resource_adequacy/parsers/descriptors/generator_mapping.yaml b/src/resource_adequacy/parsers/descriptors/generator_mapping.yaml
new file mode 100644
index 0000000..d7b5541
--- /dev/null
+++ b/src/resource_adequacy/parsers/descriptors/generator_mapping.yaml
@@ -0,0 +1,31 @@
+# Parsing code ignores type=null.
+
+HydroEnergyReservoir:
+- {fuel: HYDRO, type: null}
+- {fuel: HYDRO, type: HYDRO}
+
+HydroDispatch:
+- {fuel: HYDRO, type: ROR}
+
+RenewableDispatch:
+- {fuel: SOLAR, type: PV}
+- {fuel: SOLAR, type: UN}
+- {fuel: WIND, type: WIND}
+- {fuel: WIND, type: null}
+- {fuel: SOLAR, type: CSP} # TODO: may need a new struct
+
+RenewableFix:
+- {fuel: SOLAR, type: RTPV}
+
+ThermalStandard:
+- {fuel: OIL, type: null}
+- {fuel: COAL, type: null}
+- {fuel: NG, type: null}
+- {fuel: GAS, type: null}
+- {fuel: NUCLEAR, type: null}
+- {fuel: NUC, type: null}
+- {fuel: SYNC_COND, type: SYNC_COND}
+- {fuel: OTHER, type: OT}
+
+GenericBattery:
+- {fuel: STORAGE, type: null}
diff --git a/src/resource_adequacy/parsers/descriptors/power_system_inputs.json b/src/resource_adequacy/parsers/descriptors/power_system_inputs.json
new file mode 100644
index 0000000..0a3f186
--- /dev/null
+++ b/src/resource_adequacy/parsers/descriptors/power_system_inputs.json
@@ -0,0 +1,1168 @@
+{
+ "dc_branch": [
+ {
+ "name": "name",
+ "description": "Unique ID"
+ },
+ {
+ "name": "connection_points_from",
+ "description": "From Bus ID"
+ },
+ {
+ "name": "connection_points_to",
+ "description": "To Bus ID"
+ },
+ {
+ "name": "active_power_flow",
+ "description": "Active power flow",
+ "unit_system": "device_base",
+ "default_value": 0.0,
+ "base_reference": "rate"
+ },
+ {
+ "name": "mw_load",
+ "description": "Power demand (MW)",
+ "unit_system": "device_base",
+ "default_value": 0.0,
+ "base_reference": "rate"
+ },
+ {
+ "name": "rate",
+ "unit": "MVA",
+ "description": "Apparent power limit rating",
+ "unit_system": "SYSTEM_BASE",
+ "default_value": "system_base_power"
+ },
+ {
+ "name": "rectifier_firing_angle_max",
+ "unit": "degree",
+ "value_range": [],
+ "description": "Nominal maximum firing angle",
+ "default_value": null
+ },
+ {
+ "name": "rectifier_firing_angle_min",
+ "unit": "degree",
+ "value_range": [],
+ "description": "Minimum steady state firing angle",
+ "default_value": null
+ },
+ {
+ "name": "rectifier_xrc",
+ "description": "Commutating transformer reactance/bridge",
+ "unit_system": "device_base",
+ "default_value": null
+ },
+ {
+ "name": "rectifier_tap_limits_max",
+ "value_range": [],
+ "description": "Max tap setting",
+ "default_value": null
+ },
+ {
+ "name": "rectifier_tap_limits_min",
+ "value_range": [],
+ "description": "Min tap setting",
+ "default_value": null
+ },
+ {
+ "name": "inverter_firing_angle_max",
+ "unit": "degree",
+ "value_range": [],
+ "description": "Nominal maximum firing angle",
+ "default_value": null
+ },
+ {
+ "name": "inverter_firing_angle_min",
+ "unit": "degree",
+ "value_range": [],
+ "description": "Minimum steady state firing angle",
+ "default_value": null
+ },
+ {
+ "name": "inverter_xrc",
+ "description": "Commutating transformer reactance/bridge",
+ "unit_system": "device_base",
+ "default_value": null
+ },
+ {
+ "name": "inverter_tap_limits_max",
+ "value_range": [],
+ "description": "Max tap setting",
+ "default_value": null
+ },
+ {
+ "name": "inverter_tap_limits_min",
+ "value_range": [],
+ "description": "Min tap setting",
+ "default_value": null
+ },
+ {
+ "name": "loss",
+ "unit": "%",
+ "description": "Power Losses on the Line",
+ "default_value": 0.0
+ },
+ {
+ "name": "min_active_power_limit_from",
+ "value_range": [],
+ "description": "Minimum Active Power Limit",
+ "unit_system": "device_base",
+ "default_value": null,
+ "base_reference": "rate"
+ },
+ {
+ "name": "max_active_power_limit_from",
+ "value_range": [],
+ "description": "Maximum Active Power Limit",
+ "unit_system": "device_base",
+ "default_value": null,
+ "base_reference": "rate"
+ },
+ {
+ "name": "min_active_power_limit_to",
+ "value_range": [],
+ "description": "Minimum Active Power Limit",
+ "unit_system": "device_base",
+ "default_value": null,
+ "base_reference": "rate"
+ },
+ {
+ "name": "max_active_power_limit_to",
+ "value_range": [],
+ "description": "Maximum Active Power Limit",
+ "unit_system": "device_base",
+ "default_value": null,
+ "base_reference": "rate"
+ },
+ {
+ "name": "min_reactive_power_limit_from",
+ "value_range": [],
+ "description": "Minimum reActive Power Limit",
+ "unit_system": "device_base",
+ "default_value": 0.0,
+ "base_reference": "rate"
+ },
+ {
+ "name": "max_reactive_power_limit_from",
+ "value_range": [],
+ "description": "Maximum reActive Power Limit",
+ "unit_system": "device_base",
+ "default_value": 0.0,
+ "base_reference": "rate"
+ },
+ {
+ "name": "min_reactive_power_limit_to",
+ "value_range": [],
+ "description": "Minimum reActive Power Limit",
+ "unit_system": "device_base",
+ "default_value": 0.0,
+ "base_reference": "rate"
+ },
+ {
+ "name": "max_reactive_power_limit_to",
+ "value_range": [],
+ "description": "Maximum reActive Power Limit",
+ "unit_system": "device_base",
+ "default_value": 0.0,
+ "base_reference": "rate"
+ },
+ {
+ "name": "control_mode",
+ "description": "Control Mode",
+ "default_value": "Power"
+ },
+ {
+ "name": "dc_line_category",
+ "value_options": [
+ "VSCDCLine",
+ "HVDCLine"
+ ],
+ "description": "Type of Struct",
+ "default_value": "HVDCLine"
+ }
+ ],
+ "branch": [
+ {
+ "name": "name",
+ "description": "Unique branch ID"
+ },
+ {
+ "name": "connection_points_from",
+ "description": "From Bus ID"
+ },
+ {
+ "name": "connection_points_to",
+ "description": "To Bus ID"
+ },
+ {
+ "name": "r",
+ "description": "Branch resistance p.u.",
+ "unit_system": "device_base"
+ },
+ {
+ "name": "x",
+ "description": "Branch reactance p.u.",
+ "unit_system": "device_base"
+ },
+ {
+ "name": "primary_shunt",
+ "description": "Branch line charging susceptance p.u.",
+ "unit_system": "device_base"
+ },
+ {
+ "unit": "MW",
+ "name": "rate",
+ "description": "Continuous MW flow limit",
+ "unit_system": "SYSTEM_BASE"
+ },
+ {
+ "unit": "radian",
+ "name": "min_angle_limits",
+ "description": "Minimum Angle Limits",
+ "default_value": -1.57079
+ },
+ {
+ "unit": "radian",
+ "name": "max_angle_limits",
+ "description": "Maximum endpoint angle limits",
+ "default_value": 1.57079
+ },
+ {
+ "name": "active_power_flow",
+ "unit": "MW",
+ "description": "Active power flow",
+ "unit_system": "device_base",
+ "base_reference": "rate",
+ "default_value": 0.0
+ },
+ {
+ "name": "reactive_power_flow",
+ "unit": "MVAr",
+ "description": "Reactive power flow",
+ "unit_system": "device_base",
+ "base_reference": "rate",
+ "default_value": 0.0
+ },
+ {
+ "name": "tap",
+ "unit": "%",
+ "description": "Transformer winding ratio",
+ "default_value": 1.0
+ }
+ ,
+ {
+ "name": "is_transformer",
+ "unit": "bool",
+ "description": "Transformer flag",
+ "default_value": null
+ }
+ ],
+ "generator": [
+ {
+ "unit": "%",
+ "name": "fotr",
+ "description": "Forced Outage Rate",
+ "default_value": null
+ },
+ {
+ "unit": "hours",
+ "name": "mttr",
+ "description": "Mean Time to Recover (in Hours)",
+ "default_value": null
+ },
+ {
+ "name": "name",
+ "description": "Unique generator ID: Concatenated from Bus ID_Unit Type_Gen ID"
+ },
+ {
+ "name": "available",
+ "description": "Availability",
+ "default_value": true
+ },
+ {
+ "name": "bus_id",
+ "description": "Connection Bus ID"
+ },
+ {
+ "name": "fuel",
+ "description": "Unit Fuel"
+ },
+ {
+ "name": "fuel_price",
+ "unit": "$/MMBTU",
+ "description": "Fuel Price"
+ },
+ {
+ "unit": "MW",
+ "name": "active_power",
+ "description": "Real power injection setpoint",
+ "unit_system": "device_base",
+ "base_reference": "base_mva"
+ },
+ {
+ "unit": "MW",
+ "name": "reactive_power",
+ "description": "Reactive power StaticInjection setpoint",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": 0.0
+ },
+ {
+ "unit": "MW",
+ "name": "active_power_limits_max",
+ "description": "Maximum real power StaticInjection (Unit Capacity)",
+ "unit_system": "device_base",
+ "base_reference": "base_mva"
+ },
+ {
+ "unit": "MW",
+ "name": "active_power_limits_min",
+ "description": "Minimum real power StaticInjection (Unit minimum stable level)",
+ "unit_system": "device_base",
+ "base_reference": "base_mva"
+ },
+ {
+ "unit": "MVAR",
+ "name": "reactive_power_limits_max",
+ "description": "Maximum reactive power StaticInjection",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "MVAR",
+ "name": "reactive_power_limits_min",
+ "description": "Minimum reactive power StaticInjection",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "hours",
+ "name": "min_down_time",
+ "description": "Minimum off time required before unit restart"
+ },
+ {
+ "unit": "hours",
+ "name": "min_up_time",
+ "description": "Minimum on time required before unit shutdown"
+ },
+ {
+ "unit": "MW(p.u.)/Min",
+ "name": "ramp_limits",
+ "description": "Maximum ramp up and ramp down rate",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "MW/Min",
+ "name": "ramp_up",
+ "description": "Maximum ramp up rate",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "MW/Min",
+ "name": "ramp_down",
+ "description": "Maximum ramp down rate",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "MMBTU",
+ "name": "startup_heat_cold_cost",
+ "description": "Heat required to startup from cold",
+ "default_value": null
+ },
+ {
+ "name": "heat_rate_avg_0",
+ "description": "Heat rate Average 0 TODO",
+ "default_value": null
+ },
+ {
+ "name": "heat_rate_incr_1",
+ "description": "Heat rate Incremental 1 TODO",
+ "default_value": null
+ },
+ {
+ "name": "heat_rate_incr_2",
+ "description": "Heat rate Incremental 2 TODO",
+ "default_value": null
+ },
+ {
+ "name": "heat_rate_incr_3",
+ "description": "Heat rate Incremental 3 TODO",
+ "default_value": null
+ },
+ {
+ "name": "heat_rate_incr_4",
+ "description": "Heat rate Incremental 4 TODO",
+ "default_value": null
+ },
+ {
+ "name": "heat_rate_incr_5",
+ "description": "Heat rate Incremental 5 TODO",
+ "default_value": null
+ },
+ {
+ "name": "heat_rate_incr_6",
+ "description": "Heat rate Incremental 6 TODO",
+ "default_value": null
+ },
+ {
+ "name": "heat_rate_incr_7",
+ "description": "Heat rate Incremental 7 TODO",
+ "default_value": null
+ },
+ {
+ "name": "heat_rate_incr_8",
+ "description": "Heat rate Incremental 8 TODO",
+ "default_value": null
+ },
+ {
+ "name": "heat_rate_incr_9",
+ "description": "Heat rate Incremental 9 TODO",
+ "default_value": null
+ },
+ {
+ "name": "heat_rate_incr_10",
+ "description": "Heat rate Incremental 10 TODO",
+ "default_value": null
+ },
+ {
+ "name": "heat_rate_incr_11",
+ "description": "Heat rate Incremental 11 TODO",
+ "default_value": null
+ },
+ {
+ "name": "heat_rate_incr_12",
+ "description": "Heat rate Incremental 12 TODO",
+ "default_value": null
+ },
+ {
+ "unit": "$/hr",
+ "name": "cost_point_0",
+ "description": "Operating cost at output_point_0",
+ "default_value": null
+ },
+ {
+ "unit": "$/hr",
+ "name": "cost_point_1",
+ "description": "Operating cost at output_point_1",
+ "default_value": null
+ },
+ {
+ "unit": "$/hr",
+ "name": "cost_point_2",
+ "description": "Operating cost at output_point_2",
+ "default_value": null
+ },
+ {
+ "unit": "$/hr",
+ "name": "cost_point_3",
+ "description": "Operating cost at output_point_3",
+ "default_value": null
+ },
+ {
+ "unit": "$/hr",
+ "name": "cost_point_4",
+ "description": "Operating cost at output_point_4",
+ "default_value": null
+ },
+ {
+ "unit": "$/hr",
+ "name": "cost_point_5",
+ "description": "Operating cost at output_point_5",
+ "default_value": null
+ },
+ {
+ "unit": "$/hr",
+ "name": "cost_point_6",
+ "description": "Operating cost at output_point_6",
+ "default_value": null
+ },
+ {
+ "unit": "$/hr",
+ "name": "cost_point_7",
+ "description": "Operating cost at output_point_7",
+ "default_value": null
+ },
+ {
+ "unit": "$/hr",
+ "name": "cost_point_8",
+ "description": "Operating cost at output_point_8",
+ "default_value": null
+ },
+ {
+ "unit": "$/hr",
+ "name": "cost_point_9",
+ "description": "Operating cost at output_point_9",
+ "default_value": null
+ },
+ {
+ "unit": "$/hr",
+ "name": "cost_point_10",
+ "description": "Operating cost at output_point_10",
+ "default_value": null
+ },
+ {
+ "unit": "$/hr",
+ "name": "cost_point_11",
+ "description": "Operating cost at output_point_11",
+ "default_value": null
+ },
+ {
+ "unit": "$/hr",
+ "name": "cost_point_12",
+ "description": "Operating cost at output_point_12",
+ "default_value": null
+ },
+ {
+ "unit": "%",
+ "name": "output_point_0",
+ "description": "Output point 0 on heat rate curve as a percentage of PMax",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "%",
+ "name": "output_point_1",
+ "description": "Output point 1 on heat rate curve as a percentage of PMax",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "%",
+ "name": "output_point_2",
+ "description": "Output point 2 on heat rate curve as a percentage of PMax",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "%",
+ "name": "output_point_3",
+ "description": "Output point 3 on heat rate curve as a percentage of PMax",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "%",
+ "name": "output_point_4",
+ "description": "Output point 4 on heat rate curve as a percentage of PMax",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "%",
+ "name": "output_point_5",
+ "description": "Output point 5 on heat rate curve as a percentage of PMax",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "%",
+ "name": "output_point_6",
+ "description": "Output point 6 on heat rate curve as a percentage of PMax",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "%",
+ "name": "output_point_7",
+ "description": "Output point 7 on heat rate curve as a percentage of PMax",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "%",
+ "name": "output_point_8",
+ "description": "Output point 8 on heat rate curve as a percentage of PMax",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "%",
+ "name": "output_point_9",
+ "description": "Output point 9 on heat rate curve as a percentage of PMax",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "%",
+ "name": "output_point_10",
+ "description": "Output point 10 on heat rate curve as a percentage of PMax",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "%",
+ "name": "output_point_11",
+ "description": "Output point 11 on heat rate curve as a percentage of PMax",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "%",
+ "name": "output_point_12",
+ "description": "Output point 12 on heat rate curve as a percentage of PMax",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "MVA",
+ "name": "base_mva",
+ "description": "Unit equivalent circuit base_mva",
+ "unit_system": "natural_units"
+ },
+ {
+ "unit": "$/MW",
+ "name": "variable_cost",
+ "description": "Variable Cost of Generation",
+ "default_value": null
+ },
+ {
+ "unit": "$/MW",
+ "name": "fixed_cost",
+ "description": "Fixed Cost of Generation",
+ "default_value": null
+ },
+ {
+ "unit": "$/start",
+ "name": "startup_cost",
+ "description": "Cost associated with Start-up",
+ "default_value": null
+ },
+ {
+ "unit": "$/start",
+ "name": "shutdown_cost",
+ "description": "Cost associated with Shutdown",
+ "default_value": null
+ },
+ {
+ "unit": "$/MW",
+ "name": "curtailment_cost",
+ "description": "Cost of curtailing production",
+ "default_value": null
+ },
+ {
+ "unit": "%",
+ "name": "power_factor",
+ "description": "Power Factor",
+ "default_value": 1.0
+ },
+ {
+ "name": "unit_type",
+ "description": "Unit Prime Mover Type"
+ },
+ {
+ "name": "category",
+ "description": "Category",
+ "default_value": null
+ },
+ {
+ "name": "cold_start_time",
+ "description": "Time before which a Cold start is eligible",
+ "default_value": null
+ },
+ {
+ "name": "warm_start_time",
+ "description": "Time before which a Warm start is eligible",
+ "default_value": null
+ },
+ {
+ "name": "hot_start_time",
+ "description": "Time before which a Hot Start is eligible",
+ "default_value": null
+ },
+ {
+ "name": "startup_ramp",
+ "description": "Startup ramp capability",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "name": "shutdown_ramp",
+ "description": "Shutdown ramp capability",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "name": "status_at_start",
+ "description": "State of the generator at the start of the simulation",
+ "default_value": true
+ },
+ {
+ "name": "time_at_status",
+ "description": "Number of hours spent in current state ",
+ "default_value": null
+ },
+ {
+ "name": "cold_start_cost",
+ "description": "Cost for Cold start of ThermalGen",
+ "default_value": null
+ },
+ {
+ "name": "warm_start_cost",
+ "description": "Cost for Warm of ThermalGen",
+ "default_value": null
+ },
+ {
+ "name": "hot_start_cost",
+ "description": "Cost for Hot of ThermalGen",
+ "default_value": null
+ },
+ {
+ "name": "must_run",
+ "description": "Boolean that indicates if ThermalGen must be online always",
+ "default_value": false
+ },
+ {
+ "unit": "MW",
+ "name": "pump_load",
+ "description": "PHES Pump Load",
+ "unit_system": "device_base",
+ "base_reference": "pump_rating",
+ "default_value": null
+ },
+ {
+ "unit": "MW",
+ "name": "pump_active_power_limits_max",
+ "description": "Maximum real power StaticInjection (Unit Capacity)",
+ "unit_system": "device_base",
+ "base_reference": "pump_rating",
+ "default_value": 1.0
+ },
+ {
+ "unit": "MW",
+ "name": "pump_active_power_limits_min",
+ "description": "Minimum real power StaticInjection (Unit minimum stable level)",
+ "unit_system": "device_base",
+ "base_reference": "pump_rating",
+ "default_value": 0.0
+ },
+ {
+ "unit": "MVAR",
+ "name": "pump_reactive_power_limits_max",
+ "description": "Maximum reactive power StaticInjection",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "MVAR",
+ "name": "pump_reactive_power_limits_min",
+ "description": "Minimum reactive power StaticInjection",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "hours",
+ "name": "pump_min_down_time",
+ "description": "Minimum off time required before unit restart",
+ "default_value": null
+ },
+ {
+ "unit": "hours",
+ "name": "pump_min_up_time",
+ "description": "Minimum on time required before unit shutdown",
+ "default_value": null
+ },
+ {
+ "unit": "MW(p.u.)/Min",
+ "name": "pump_ramp_limits",
+ "description": "Maximum ramp up and ramp down rate",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "MW/Min",
+ "name": "pump_ramp_up",
+ "description": "Maximum ramp up rate",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "unit": "MW/Min",
+ "name": "pump_ramp_down",
+ "description": "Maximum ramp down rate",
+ "unit_system": "device_base",
+ "base_reference": "base_mva",
+ "default_value": null
+ },
+ {
+ "name": "generator_category",
+ "value_options": [
+ "HydroDispatch",
+ "HydroEnergyReservoir",
+ "RenewableFix",
+ "RenewableDispatch",
+ "ThermalStandard",
+ "ThermalMultiStart"
+ ],
+ "description": "Type of Struct",
+ "default_value": "ThermalStandard"
+ }
+ ],
+ "simulation_objects": [],
+ "reserves": [
+ {
+ "name": "name",
+ "description": "Reserve product name"
+ },
+ {
+ "name": "contributing_devices",
+ "description": "Contributing Devices for reserve requirement",
+ "default_value": null
+ },
+ {
+ "unit": "MW",
+ "name": "requirement",
+ "description": "reserve requirement",
+ "unit_system": "SYSTEM_BASE"
+ },
+ {
+ "unit": "seconds",
+ "name": "timeframe",
+ "description": "Response time to satisfy reserve requirement"
+ },
+ {
+ "name": "eligible_device_categories",
+ "description": "Eligible Device Categories",
+ "default_value": null
+ },
+ {
+ "name": "eligible_device_subcategories",
+ "description": "Eligible Device SubCategories",
+ "default_value": null
+ },
+ {
+ "name": "eligible_regions",
+ "description": "Eligible Regions",
+ "default_value": null
+ },
+ {
+ "name": "reserve_category",
+ "value_options": [
+ "StaticReserve",
+ "VariableReserve",
+ "Transfer"
+ ],
+ "description": "Type of Struct",
+ "default_value": "StaticReserve"
+ },
+ {
+ "name": "direction",
+ "description": "Direction",
+ "value_options": [
+ "Up",
+ "Down"
+ ]
+ }
+ ],
+ "storage": [
+ {
+ "name": "name",
+ "description": "Storage object name"
+ },
+ {
+ "name": "position",
+ "description": "head or tail",
+ "default_value": "head"
+ },
+ {
+ "name": "available",
+ "description": "availability",
+ "default_value": true
+ },
+ {
+ "name": "generator_name",
+ "description": "Generator name associated with storage",
+ "default_value": null
+ },
+ {
+ "name": "bus_id",
+ "description": "Connection Bus ID",
+ "default_value": null
+ },
+ {
+ "name": "energy_level",
+ "unit": "MWh",
+ "description": "Energy Level setpoint",
+ "unit_system": "device_base",
+ "default_value": 0.0
+ },
+ {
+ "unit": "MW",
+ "name": "active_power",
+ "description": "Real power injection setpoint",
+ "unit_system": "device_base",
+ "default_value": 0.0
+ },
+ {
+ "unit": "MVar",
+ "name": "reactive_power",
+ "description": "Reactive power StaticInjection setpoint",
+ "unit_system": "device_base",
+ "default_value": 0.0
+ },
+ {
+ "unit": "MW",
+ "name": "input_active_power_limit_max",
+ "description": "Maximum real power limit on charging",
+ "unit_system": "device_base"
+ },
+ {
+ "unit": "MW",
+ "name": "input_active_power_limit_min",
+ "description": "Minimum real power limit on charging",
+ "unit_system": "device_base",
+ "default_value": 0.0
+ },
+ {
+ "unit": "MW",
+ "name": "output_active_power_limit_max",
+ "description": "Maximum real power StaticInjection",
+ "unit_system": "device_base",
+ "default_value": null
+ },
+ {
+ "unit": "MW",
+ "name": "output_active_power_limit_min",
+ "description": "Minimum real power StaticInjection",
+ "unit_system": "device_base",
+ "default_value": 0.0
+ },
+ {
+ "unit": "MVAR",
+ "name": "output_reactive_power_limits_max",
+ "description": "Maximum reactive power StaticInjection",
+ "unit_system": "device_base",
+ "default_value": null
+ },
+ {
+ "unit": "MVAR",
+ "name": "output_reactive_power_limits_min",
+ "description": "Minimum reactive power StaticInjection",
+ "unit_system": "device_base",
+ "default_value": null
+ },
+ {
+ "unit": "MVA",
+ "name": "rating",
+ "description": "Apparent power injection limit rating",
+ "unit_system": "device_base"
+ },
+ {
+ "unit": "MW",
+ "name": "base_power",
+ "description": "Continuous MW flow limit",
+ "unit_system": "natural_units"
+ },
+ {
+ "unit": "%",
+ "name": "input_efficiency",
+ "description": "Storage Input Efficiency",
+ "default_value": 1.0
+ },
+ {
+ "unit": "%",
+ "name": "output_efficiency",
+ "description": "Storage Output Efficiency",
+ "default_value": 1.0
+ },
+ {
+ "unit": "%",
+ "name": "efficiency",
+ "description": "Battery Efficiency",
+ "default_value": 1.0
+ },
+ {
+ "unit": "MWh",
+ "name": "storage_capacity",
+ "description": "Storage Capacity",
+ "unit_system": "device_base"
+ },
+ {
+ "unit": "MWh",
+ "name": "min_storage_capacity",
+ "description": "Storage Capacity minimum",
+ "unit_system": "device_base",
+ "default_value": 0.0
+ },
+ {
+ "unit": "MWh",
+ "name": "storage_target",
+ "description": "End period storage target level",
+ "unit_system": "device_base",
+ "default_value": 0.0
+ }
+ ],
+ "bus": [
+ {
+ "name": "bus_id",
+ "description": "Numeric Bus ID"
+ },
+ {
+ "name": "name",
+ "description": "Bus name from RTS-96"
+ },
+ {
+ "name": "area",
+ "description": "area membership",
+ "default_value": null
+ },
+ {
+ "name": "zone",
+ "description": "zone membership",
+ "default_value": null
+ },
+ {
+ "unit": "kV",
+ "name": "base_voltage",
+ "description": "Bus voltage rating",
+ "unit_system": "natural_units",
+ "default_value": null
+ },
+ {
+ "name": "bus_type",
+ "value_options": [
+ "PQ",
+ "PV",
+ "REF"
+ ],
+ "description": "Bus control type",
+ "default_value": null
+ },
+ {
+ "unit": "kV",
+ "name": "voltage",
+ "description": "voltage magnitude setpoint",
+ "unit_system": "device_base",
+ "base_reference": "base_voltage",
+ "default_value": null
+ },
+ {
+ "unit": "radian",
+ "name": "angle",
+ "description": "voltage angle setpoint",
+ "default_value": null
+ },
+ {
+ "unit": "kV",
+ "name": "voltage_limits_min",
+ "description": "Minimum voltage setpoint",
+ "unit_system": "device_base",
+ "base_reference": "base_voltage",
+ "default_value": 0.95
+ },
+ {
+ "unit": "kV",
+ "name": "voltage_limits_max",
+ "description": "Maximum voltage setpoint",
+ "unit_system": "device_base",
+ "base_reference": "base_voltage",
+ "default_value": 1.05
+ },
+ {
+ "name": "max_active_power",
+ "description": "Maximum Active Power",
+ "unit_system": "device_base",
+ "base_reference": "base_power",
+ "default_value": 0
+ },
+ {
+ "name": "max_reactive_power",
+ "description": "Maximum Rective Power",
+ "unit_system": "device_base",
+ "base_reference": "base_power",
+ "default_value": 0
+ },
+ {
+ "name": "active_power",
+ "description": "Active Power Setpoint",
+ "unit_system": "device_base",
+ "base_reference": "base_power",
+ "default_value": 0
+ },
+ {
+ "name": "reactive_power",
+ "description": "Rective Power Setpoint",
+ "unit_system": "device_base",
+ "base_reference": "base_power",
+ "default_value": 0
+ },
+ {
+ "name": "base_power",
+ "description": "base power for demand",
+ "unit_system": "natural_units",
+ "default_value": "system_base_power"
+ }
+ ],
+ "load": [
+ {
+ "name": "name",
+ "description": "Load Name"
+ },
+ {
+ "name": "available",
+ "description": "Availability",
+ "default_value": true
+ },
+ {
+ "name": "bus_id",
+ "description": "Connection Bus ID",
+ "default_value": null
+ },
+ {
+ "name": "active_power",
+ "description": "Active power setpoint",
+ "default_value": 0.0,
+ "unit_system": "device_base"
+ },
+ {
+ "name": "reactive_power",
+ "description": "Reactive power setpoint",
+ "default_value": 0.0,
+ "unit_system": "device_base"
+ },
+ {
+ "name": "base_power",
+ "description": "base",
+ "unit_system": "natural_units",
+ "default_value": "system_base_power"
+ },
+ {
+ "name": "max_active_power",
+ "description": "max active power",
+ "unit_system": "device_base"
+ },
+ {
+ "name": "max_reactive_power",
+ "description": "max reactive power",
+ "unit_system": "device_base",
+ "default_value": 0.0
+ }
+ ]
+}
diff --git a/src/resource_adequacy/parsers/power_system_table_data.jl b/src/resource_adequacy/parsers/power_system_table_data.jl
new file mode 100644
index 0000000..a4abd47
--- /dev/null
+++ b/src/resource_adequacy/parsers/power_system_table_data.jl
@@ -0,0 +1,651 @@
+
+##############################################
+# Definitions
+#############################################
+const POWER_SYSTEM_DESCRIPTOR_FILE =
+ joinpath(@__DIR__, "descriptors", "power_system_inputs.json")
+
+const GENERATOR_MAPPING_FILE =
+ joinpath(@__DIR__, "descriptors", "generator_mapping.yaml")
+##############################################
+# Struct for outage information
+##############################################
+struct outage_info
+ outage_probability::Float64
+ recovery_probability::Float64
+
+ outage_info(outage_probability = 0.0, recovery_probability = 1.0) =
+ new(outage_probability, recovery_probability)
+end
+##############################################
+# Converting FOR and MTTR to λ and μ
+##############################################
+function outage_to_rate(outage_data::Tuple{Float64, Int64})
+ for_gen = outage_data[1]
+ mttr = outage_data[2]
+ if (mttr != 0)
+ μ = 1 / mttr
+ else
+ μ = 1.0
+ end
+ λ = (μ * for_gen) / (1 - for_gen)
+
+ return (λ = λ, μ = μ)
+end
+##############################################
+# PowerSystems2PRAS definition of
+# PowerSystemTableData()
+##############################################
+function PSY.PowerSystemTableData(
+ data::Dict{String, Any},
+ directory::String,
+ user_descriptors::Union{String, Dict},
+ descriptors::Union{String, Dict},
+ generator_mapping::Union{String, Dict};
+ timeseries_metadata_file = joinpath(directory, "timeseries_pointers"),
+)
+ category_to_df = Dict{PSY.InputCategory, DataFrames.DataFrame}()
+
+ if !haskey(data, "bus")
+ throw(PSY.DataFormatError("key 'bus' not found in input data"))
+ end
+
+ if !haskey(data, "base_power")
+ @warn "key 'base_power' not found in input data; using default=$(PSY.DEFAULT_BASE_MVA)"
+ end
+ base_power = get(data, "base_power", PSY.DEFAULT_BASE_MVA)
+
+ for (name, category) in PSY.INPUT_CATEGORY_NAMES
+ val = get(data, name, nothing)
+ if isnothing(val)
+ @debug "key '$name' not found in input data, set to nothing"
+ else
+ category_to_df[category] = val
+ end
+ end
+
+ if !isfile(timeseries_metadata_file)
+ if isfile(string(timeseries_metadata_file, ".json"))
+ timeseries_metadata_file = string(timeseries_metadata_file, ".json")
+ elseif isfile(string(timeseries_metadata_file, ".csv"))
+ timeseries_metadata_file = string(timeseries_metadata_file, ".csv")
+ else
+ timeseries_metadata_file = nothing
+ end
+ end
+
+ if user_descriptors isa AbstractString
+ user_descriptors = PSY._read_config_file(user_descriptors)
+ end
+
+ if descriptors isa AbstractString
+ descriptors = PSY._read_config_file(descriptors)
+ end
+
+ if generator_mapping isa AbstractString
+ generator_mapping = PSY.get_generator_mapping(generator_mapping)
+ end
+
+ return PSY.PowerSystemTableData(
+ base_power,
+ category_to_df,
+ timeseries_metadata_file,
+ directory,
+ user_descriptors,
+ descriptors,
+ generator_mapping,
+ )
+end
+##############################################
+# PowerSystems2PRAS definition of
+# PowerSystemTableData()
+##############################################
+"""
+Reads in all the data stored in csv files
+The general format for data is
+ folder:
+ gen.csv
+ branch.csv
+ bus.csv
+ ..
+ load.csv
+
+# Arguments
+- `directory::AbstractString`: directory containing CSV files
+- `base_power::Float64`: base power for System
+- `user_descriptor_file::AbstractString`: customized input descriptor file
+- `descriptor_file=POWER_SYSTEM_DESCRIPTOR_FILE`: PowerSystems descriptor file
+- `generator_mapping_file=GENERATOR_MAPPING_FILE`: generator mapping configuration file
+"""
+function PSY.PowerSystemTableData(
+ directory::AbstractString,
+ base_power::Float64,
+ user_descriptor_file::AbstractString;
+ descriptor_file = POWER_SYSTEM_DESCRIPTOR_FILE,
+ generator_mapping_file = GENERATOR_MAPPING_FILE,
+ timeseries_metadata_file = joinpath(directory, "timeseries_pointers"),
+)
+ files = readdir(directory)
+ REGEX_DEVICE_TYPE = r"(.*?)\.csv"
+ REGEX_IS_FOLDER = r"^[A-Za-z]+$"
+ data = Dict{String, Any}()
+
+ if length(files) == 0
+ error("No files in the folder")
+ else
+ data["base_power"] = base_power
+ end
+
+ encountered_files = 0
+ for d_file in files
+ try
+ if match(REGEX_IS_FOLDER, d_file) !== nothing
+ @info "Parsing csv files in $d_file ..."
+ d_file_data = Dict{String, Any}()
+ for file in readdir(joinpath(directory, d_file))
+ if match(REGEX_DEVICE_TYPE, file) !== nothing
+ @info "Parsing csv data in $file ..."
+ encountered_files += 1
+ fpath = joinpath(directory, d_file, file)
+ raw_data = DataFrames.DataFrame(CSV.File(fpath))
+ d_file_data[split(file, r"[.]")[1]] = raw_data
+ end
+ end
+
+ if length(d_file_data) > 0
+ data[d_file] = d_file_data
+ @info "Successfully parsed $d_file"
+ end
+
+ elseif match(REGEX_DEVICE_TYPE, d_file) !== nothing
+ @info "Parsing csv data in $d_file ..."
+ encountered_files += 1
+ fpath = joinpath(directory, d_file)
+ raw_data = DataFrames.DataFrame(CSV.File(fpath))
+ data[split(d_file, r"[.]")[1]] = raw_data
+ @info "Successfully parsed $d_file"
+ end
+ catch ex
+ @error "Error occurred while parsing $d_file" exception = ex
+ throw(ex)
+ end
+ end
+ if encountered_files == 0
+ error("No csv files or folders in $directory")
+ end
+
+ return PSY.PowerSystemTableData(
+ data,
+ directory,
+ user_descriptor_file,
+ descriptor_file,
+ generator_mapping_file,
+ timeseries_metadata_file = timeseries_metadata_file,
+ )
+end
+##############################################
+# PowerSystems2PRAS definition of System()
+##############################################
+"""
+Construct a System from PowerSystemTableData data.
+
+# Arguments
+- `time_series_resolution::Union{DateTime, Nothing}=nothing`: only store time_series that match
+ this resolution.
+- `time_series_in_memory::Bool=false`: Store time series data in memory instead of HDF5 file
+- `time_series_directory=nothing`: Store time series data in directory instead of tmpfs
+- `runchecks::Bool=true`: Validate struct fields.
+
+Throws DataFormatError if time_series with multiple resolutions are detected.
+- A time_series has a different resolution than others.
+- A time_series has a different horizon than others.
+
+"""
+function PSY.System(
+ data::PSY.PowerSystemTableData;
+ time_series_resolution = nothing,
+ time_series_in_memory = false,
+ time_series_directory = nothing,
+ runchecks = true,
+ kwargs...,
+)
+ sys = PSY.System(
+ data.base_power;
+ time_series_in_memory = time_series_in_memory,
+ time_series_directory = time_series_directory,
+ runchecks = runchecks,
+ kwargs...,
+ )
+ PSY.set_units_base_system!(sys, PSY.IS.UnitSystem.DEVICE_BASE)
+
+ PSY.loadzone_csv_parser!(sys, data)
+ PSY.bus_csv_parser!(sys, data)
+
+ # Services and time_series must be last.
+ parsers = (
+ (PSY.get_dataframe(data, PSY.InputCategory.BRANCH), PSY.branch_csv_parser!),
+ (PSY.get_dataframe(data, PSY.InputCategory.DC_BRANCH), PSY.dc_branch_csv_parser!),
+ (PSY.get_dataframe(data, PSY.InputCategory.GENERATOR), PSY.gen_csv_parser!),
+ (PSY.get_dataframe(data, PSY.InputCategory.LOAD), PSY.load_csv_parser!),
+ (PSY.get_dataframe(data, PSY.InputCategory.RESERVE), PSY.services_csv_parser!),
+ )
+
+ for (val, parser) in parsers
+ if !isnothing(val)
+ parser(sys, data)
+ end
+ end
+
+ timeseries_metadata_file =
+ get(kwargs, :timeseries_metadata_file, getfield(data, :timeseries_metadata_file))
+
+ if !isnothing(timeseries_metadata_file)
+ PSY.add_time_series!(sys, timeseries_metadata_file; resolution = time_series_resolution)
+ end
+
+ #PSY.check!(sys)
+ return sys
+end
+##############################################
+# PowerSystems2PRAS definition of make_generator()
+##############################################
+"""Creates a generator of any type."""
+function PSY.make_generator(data::PSY.PowerSystemTableData, gen, cost_colnames, bus)
+ generator = nothing
+ gen_type =
+ PSY.get_generator_type(gen.fuel, get(gen, :unit_type, nothing), data.generator_mapping)
+
+ if isnothing(gen_type)
+ @error "Cannot recognize generator type" gen.name
+ elseif gen_type == PSY.ThermalStandard
+ generator = PSY.make_thermal_generator(data, gen, cost_colnames, bus)
+ elseif gen_type == PSY.ThermalMultiStart
+ generator = PSY.make_thermal_generator_multistart(data, gen, cost_colnames, bus)
+ elseif gen_type <: PSY.HydroGen
+ generator = PSY.make_hydro_generator(gen_type, data, gen, cost_colnames, bus)
+ elseif gen_type <: PSY.RenewableGen
+ generator = PSY.make_renewable_generator(gen_type, data, gen, cost_colnames, bus)
+ elseif gen_type == PSY.GenericBattery
+ storage = PSY.get_storage_by_generator(data, gen.name).head
+ generator = PSY.make_storage(data, gen, storage, bus)
+ else
+ @error "Skipping unsupported generator" gen.name gen_type
+ end
+
+ return generator
+end
+##############################################
+# PowerSystems2PRAS function to add outage info
+# to Generator
+##############################################
+function add_outage_info!(component::PSY.StaticInjection, gen)
+ outage_rates = outage_to_rate((gen.fotr, gen.mttr))
+ outage_probability = outage_info(outage_rates.λ, outage_rates.μ)
+
+ ext = PSY.get_ext(component)
+ for fn in fieldnames(outage_info)
+ ext[string(fn)] = getfield(outage_probability, fn)
+ end
+end
+##############################################
+# PowerSystems2PRAS definition of make_thermal_generator()
+##############################################
+function PSY.make_thermal_generator(data::PSY.PowerSystemTableData, gen, cost_colnames, bus)
+ @debug "Making ThermaStandard" gen.name
+ active_power_limits =
+ (min = gen.active_power_limits_min, max = gen.active_power_limits_max)
+ (reactive_power, reactive_power_limits) = PSY.make_reactive_params(gen)
+ rating = PSY.calculate_rating(active_power_limits, reactive_power_limits)
+ ramplimits = PSY.make_ramplimits(gen)
+ timelimits = PSY.make_timelimits(gen, :min_up_time, :min_down_time)
+ primemover = PSY.parse_enum_mapping(PSY.PrimeMovers, gen.unit_type)
+ fuel = PSY.parse_enum_mapping(PSY.ThermalFuels, gen.fuel)
+
+ base_power = gen.base_mva
+ var_cost, fixed, fuel_cost =
+ PSY.calculate_variable_cost(data, gen, cost_colnames, base_power)
+ startup_cost, shutdown_cost = PSY.calculate_uc_cost(data, gen, fuel_cost)
+ op_cost = PSY.ThreePartCost(var_cost, fixed, startup_cost, shutdown_cost)
+
+ component = PSY.ThermalStandard(
+ name = gen.name,
+ available = gen.available,
+ status = gen.status_at_start,
+ bus = bus,
+ active_power = gen.active_power,
+ reactive_power = reactive_power,
+ rating = rating,
+ prime_mover = primemover,
+ fuel = fuel,
+ active_power_limits = active_power_limits,
+ reactive_power_limits = reactive_power_limits,
+ ramp_limits = ramplimits,
+ time_limits = timelimits,
+ operation_cost = op_cost,
+ base_power = base_power,
+ )
+
+ if ((gen.fotr, gen.mttr) != (nothing, nothing))
+ add_outage_info!(component, gen)
+ end
+
+ return component
+end
+##############################################
+# PowerSystems2PRAS definition of make_thermal_generator_multistart()
+##############################################
+function PSY.make_thermal_generator_multistart(
+ data::PSY.PowerSystemTableData,
+ gen,
+ cost_colnames,
+ bus,
+)
+ thermal_gen = PSY.make_thermal_generator(data, gen, cost_colnames, bus)
+
+ @debug "Making ThermalMultiStart" gen.name
+ base_power = PSY.get_base_power(thermal_gen)
+ var_cost, fixed, fuel_cost =
+ PSY.calculate_variable_cost(data, gen, cost_colnames, base_power)
+ if var_cost isa Float64
+ no_load_cost = 0.0
+ var_cost = PSY.VariableCost(var_cost)
+ else
+ no_load_cost = var_cost[1][1]
+ var_cost =
+ PSY.VariableCost([(c - no_load_cost, pp - var_cost[1][2]) for (c, pp) in var_cost])
+ end
+ lag_hot =
+ isnothing(gen.hot_start_time) ? PSY.get_time_limits(thermal_gen).down :
+ gen.hot_start_time
+ lag_warm = isnothing(gen.warm_start_time) ? 0.0 : gen.warm_start_time
+ lag_cold = isnothing(gen.cold_start_time) ? 0.0 : gen.cold_start_time
+ startup_timelimits = (hot = lag_hot, warm = lag_warm, cold = lag_cold)
+ start_types = sum(values(startup_timelimits) .> 0.0)
+ startup_ramp = isnothing(gen.startup_ramp) ? 0.0 : gen.startup_ramp
+ shutdown_ramp = isnothing(gen.shutdown_ramp) ? 0.0 : gen.shutdown_ramp
+ power_trajectory = (startup = startup_ramp, shutdown = shutdown_ramp)
+ hot_start_cost = isnothing(gen.hot_start_cost) ? gen.startup_cost : gen.hot_start_cost
+ if isnothing(hot_start_cost)
+ if hasfield(typeof(gen), :startup_heat_cold_cost)
+ hot_start_cost = gen.startup_heat_cold_cost * fuel_cost * 1000
+ else
+ hot_start_cost = 0.0
+ @warn "No hot_start_cost or startup_cost defined for $(gen.name), setting to $startup_cost" maxlog =
+ 5
+ end
+ end
+ warm_start_cost = isnothing(gen.warm_start_cost) ? PSY.START_COST : gen.hot_start_cost #TODO
+ cold_start_cost = isnothing(gen.cold_start_cost) ? PSY.START_COST : gen.cold_start_cost
+ startup_cost = (hot = hot_start_cost, warm = warm_start_cost, cold = cold_start_cost)
+
+ shutdown_cost = gen.shutdown_cost
+ if isnothing(shutdown_cost)
+ @warn "No shutdown_cost defined for $(gen.name), setting to 0.0" maxlog = 1
+ shutdown_cost = 0.0
+ end
+
+ op_cost = PSY.MultiStartCost(var_cost, no_load_cost, fixed, startup_cost, shutdown_cost)
+
+ component = PSY.ThermalMultiStart(;
+ name = PSY.get_name(thermal_gen),
+ available = PSY.get_available(thermal_gen),
+ status = PSY.get_status(thermal_gen),
+ bus = PSY.get_bus(thermal_gen),
+ active_power = PSY.get_active_power(thermal_gen),
+ reactive_power = PSY.get_reactive_power(thermal_gen),
+ rating = PSY.get_rating(thermal_gen),
+ prime_mover = PSY.get_prime_mover(thermal_gen),
+ fuel = PSY.get_fuel(thermal_gen),
+ active_power_limits = PSY.get_active_power_limits(thermal_gen),
+ reactive_power_limits = PSY.get_reactive_power_limits(thermal_gen),
+ ramp_limits = PSY.get_ramp_limits(thermal_gen),
+ power_trajectory = power_trajectory,
+ time_limits = PSY.get_time_limits(thermal_gen),
+ start_time_limits = startup_timelimits,
+ start_types = start_types,
+ operation_cost = op_cost,
+ base_power = PSY.get_base_power(thermal_gen),
+ time_at_status = PSY.get_time_at_status(thermal_gen),
+ must_run = gen.must_run,
+ )
+
+ if ((gen.fotr, gen.mttr) != (nothing, nothing))
+ add_outage_info!(component, gen)
+ end
+
+ return component
+end
+##############################################
+# PowerSystems2PRAS definition of make_hydro_generator()
+##############################################
+function PSY.make_hydro_generator(gen_type, data::PSY.PowerSystemTableData, gen, cost_colnames, bus)
+ @debug "Making HydroGen" gen.name
+ active_power_limits =
+ (min = gen.active_power_limits_min, max = gen.active_power_limits_max)
+ (reactive_power, reactive_power_limits) = PSY.make_reactive_params(gen)
+ rating = PSY.calculate_rating(active_power_limits, reactive_power_limits)
+ ramp_limits = PSY.make_ramplimits(gen)
+ min_up_time = gen.min_up_time
+ min_down_time = gen.min_down_time
+ time_limits = PSY.make_timelimits(gen, :min_up_time, :min_down_time)
+ base_power = gen.base_mva
+
+ if gen_type == PSY.HydroEnergyReservoir || gen_type == PSY.HydroPumpedStorage
+ if !haskey(data.category_to_df, PSY.InputCategory.STORAGE)
+ throw(PSY.DataFormatError("Storage information must defined in storage.csv"))
+ end
+
+ storage = PSY.get_storage_by_generator(data, gen.name)
+
+ var_cost, fixed, fuel_cost =
+ PSY.calculate_variable_cost(data, gen, cost_colnames, base_power)
+ operation_cost = PSY.TwoPartCost(var_cost, fixed)
+
+ if gen_type == PSY.HydroEnergyReservoir
+ @debug("Creating $(gen.name) as HydroEnergyReservoir")
+
+ hydro_gen = PSY.HydroEnergyReservoir(
+ name = gen.name,
+ available = gen.available,
+ bus = bus,
+ active_power = gen.active_power,
+ reactive_power = reactive_power,
+ prime_mover = PSY.parse_enum_mapping(PSY.PrimeMovers, gen.unit_type),
+ rating = rating,
+ active_power_limits = active_power_limits,
+ reactive_power_limits = reactive_power_limits,
+ ramp_limits = ramp_limits,
+ time_limits = time_limits,
+ operation_cost = operation_cost,
+ base_power = base_power,
+ storage_capacity = storage.head.storage_capacity,
+ inflow = storage.head.input_active_power_limit_max,
+ initial_storage = storage.head.energy_level,
+ )
+
+ elseif gen_type == PSY.HydroPumpedStorage
+ @debug("Creating $(gen.name) as HydroPumpedStorage")
+
+ pump_active_power_limits = (
+ min = gen.pump_active_power_limits_min,
+ max = gen.pump_active_power_limits_max,
+ )
+ (pump_reactive_power, pump_reactive_power_limits) = PSY.make_reactive_params(
+ gen,
+ powerfield = :pump_reactive_power,
+ minfield = :pump_reactive_power_limits_min,
+ maxfield = :pump_reactive_power_limits_max,
+ )
+ pump_rating =
+ PSY.calculate_rating(pump_active_power_limits, pump_reactive_power_limits)
+ pump_ramp_limits = PSY.make_ramplimits(
+ gen;
+ ramplimcol = :pump_ramp_limits,
+ rampupcol = :pump_ramp_up,
+ rampdncol = :pump_ramp_down,
+ )
+ pump_time_limits = PSY.make_timelimits(gen, :pump_min_up_time, :pump_min_down_time)
+ hydro_gen = PSY.HydroPumpedStorage(
+ name = gen.name,
+ available = gen.available,
+ bus = bus,
+ active_power = gen.active_power,
+ reactive_power = reactive_power,
+ rating = rating,
+ base_power = base_power,
+ prime_mover = PSY.parse_enum_mapping(PSY.PrimeMovers, gen.unit_type),
+ active_power_limits = active_power_limits,
+ reactive_power_limits = reactive_power_limits,
+ ramp_limits = ramp_limits,
+ time_limits = time_limits,
+ rating_pump = pump_rating,
+ active_power_limits_pump = pump_active_power_limits,
+ reactive_power_limits_pump = pump_reactive_power_limits,
+ ramp_limits_pump = pump_ramp_limits,
+ time_limits_pump = pump_time_limits,
+ storage_capacity = (
+ up = storage.head.storage_capacity,
+ down = storage.head.storage_capacity,
+ ),
+ inflow = storage.head.input_active_power_limit_max,
+ outflow = storage.tail.input_active_power_limit_max,
+ initial_storage = (
+ up = storage.head.energy_level,
+ down = storage.tail.energy_level,
+ ),
+ storage_target = (
+ up = storage.head.storage_target,
+ down = storage.tail.storage_target,
+ ),
+ operation_cost = operation_cost,
+ pump_efficiency = storage.tail.efficiency,
+ )
+ end
+ elseif gen_type == PSY.HydroDispatch
+ @debug("Creating $(gen.name) as HydroDispatch")
+ hydro_gen = PSY.HydroDispatch(
+ name = gen.name,
+ available = gen.available,
+ bus = bus,
+ active_power = gen.active_power,
+ reactive_power = reactive_power,
+ rating = rating,
+ prime_mover = PSY.parse_enum_mapping(PSY.PrimeMovers, gen.unit_type),
+ active_power_limits = active_power_limits,
+ reactive_power_limits = reactive_power_limits,
+ ramp_limits = ramp_limits,
+ time_limits = time_limits,
+ base_power = base_power,
+ )
+ else
+ error("Tabular data parser does not currently support $gen_type creation")
+ end
+
+ if ((gen.fotr, gen.mttr) != (nothing, nothing))
+ add_outage_info!(hydro_gen, gen)
+ end
+
+ return hydro_gen
+end
+##############################################
+# PowerSystems2PRAS definition of make_renewable_generator()
+##############################################
+function PSY.make_renewable_generator(
+ gen_type,
+ data::PSY.PowerSystemTableData,
+ gen,
+ cost_colnames,
+ bus,
+)
+ @debug "Making RenewableGen" gen.name
+ generator = nothing
+ active_power_limits =
+ (min = gen.active_power_limits_min, max = gen.active_power_limits_max)
+ (reactive_power, reactive_power_limits) = PSY.make_reactive_params(gen)
+ rating = PSY.calculate_rating(active_power_limits, reactive_power_limits)
+ base_power = gen.base_mva
+ var_cost, fixed, fuel_cost =
+ PSY.calculate_variable_cost(data, gen, cost_colnames, base_power)
+ operation_cost = PSY.TwoPartCost(var_cost, fixed)
+
+ if gen_type == PSY.RenewableDispatch
+ @debug("Creating $(gen.name) as RenewableDispatch")
+ generator = PSY.RenewableDispatch(
+ name = gen.name,
+ available = gen.available,
+ bus = bus,
+ active_power = gen.active_power,
+ reactive_power = reactive_power,
+ rating = rating,
+ prime_mover = PSY.parse_enum_mapping(PSY.PrimeMovers, gen.unit_type),
+ reactive_power_limits = reactive_power_limits,
+ power_factor = gen.power_factor,
+ operation_cost = operation_cost,
+ base_power = base_power,
+ )
+ elseif gen_type == PSY.RenewableFix
+ @debug("Creating $(gen.name) as RenewableFix")
+ generator = PSY.RenewableFix(
+ name = gen.name,
+ available = gen.available,
+ bus = bus,
+ active_power = gen.active_power,
+ reactive_power = reactive_power,
+ rating = rating,
+ prime_mover = PSY.parse_enum_mapping(PSY.PrimeMovers, gen.unit_type),
+ power_factor = gen.power_factor,
+ base_power = base_power,
+ )
+ else
+ error("Unsupported type $gen_type")
+ end
+
+ if ((gen.fotr, gen.mttr) != (nothing, nothing))
+ add_outage_info!(generator, gen)
+ end
+
+ return generator
+end
+##############################################
+# PowerSystems2PRAS definition of make_storage()
+##############################################
+function PSY.make_storage(data::PSY.PowerSystemTableData, gen, storage, bus)
+ @debug "Making Storge" storage.name
+ state_of_charge_limits =
+ (min = storage.min_storage_capacity, max = storage.storage_capacity)
+ input_active_power_limits = (
+ min = storage.input_active_power_limit_min,
+ max = storage.input_active_power_limit_max,
+ )
+ output_active_power_limits = (
+ min = storage.output_active_power_limit_min,
+ max = isnothing(storage.output_active_power_limit_max) ?
+ gen.active_power_limits_max : storage.output_active_power_limit_max,
+ )
+ efficiency = (in = storage.input_efficiency, out = storage.output_efficiency)
+ (reactive_power, reactive_power_limits) = PSY.make_reactive_params(storage)
+
+ battery = PSY.GenericBattery(
+ name = gen.name,
+ available = storage.available,
+ bus = bus,
+ prime_mover = PSY.parse_enum_mapping(PSY.PrimeMovers, gen.unit_type),
+ initial_energy = storage.energy_level,
+ state_of_charge_limits = state_of_charge_limits,
+ rating = storage.rating,
+ active_power = storage.active_power,
+ input_active_power_limits = input_active_power_limits,
+ output_active_power_limits = output_active_power_limits,
+ efficiency = efficiency,
+ reactive_power = reactive_power,
+ reactive_power_limits = reactive_power_limits,
+ base_power = storage.base_power,
+ )
+
+ if ((gen.fotr, gen.mttr) != (nothing, nothing))
+ add_outage_info!(battery, gen)
+ end
+
+ return battery
+end
+
diff --git a/src/resource_adequacy/ra_utils.jl b/src/resource_adequacy/ra_utils.jl
new file mode 100644
index 0000000..b8a7afc
--- /dev/null
+++ b/src/resource_adequacy/ra_utils.jl
@@ -0,0 +1,253 @@
+function add_outage_info!(
+ PSY_gen::T,
+ tech::Union{ThermalTech, RenewableTech, HydroTech, BatteryTech}
+ ) where T <: Union{PSY.Generator, PSY.Storage}
+ (λ, μ) = outage_to_rate((get_FOR(tech), get_MTTR(tech)))
+ ext = PSY.get_ext(PSY_gen)
+ ext["outage_probability"] = λ
+ ext["recovery_probability"] = μ
+
+ return
+end
+
+
+function calculate_RA_metrics(sys::PSY.System)
+
+ system_period_of_interest = range(1, length = 8760);
+
+ pras_system = make_pras_system(sys,
+ system_model = "Single-Node",
+ aggregation = "Area",
+ period_of_interest = system_period_of_interest,
+ outage_flag = false);
+
+ total_load = calculate_total_load(sys, 60)
+
+ ra_metrics = Dict{String, Float64}()
+ seed = 3
+ shortfall, = @time PRAS.assess(pras_system, PRAS.SequentialMonteCarlo(samples = 100, seed=seed), PRAS.Shortfall())
+ @info "Finished PRAS simulation... "
+ eue_overall = PRAS.EUE(shortfall)
+ lole_overall = PRAS.LOLE(shortfall)
+
+ ra_metrics["LOLE"] = val(lole_overall)
+ ra_metrics["NEUE"] = val(eue_overall) * 1e6 / total_load
+
+ PSY.set_units_base_system!(sys, PSY.IS.UnitSystem. DEVICE_BASE)
+
+ return ra_metrics
+
+end
+
+"""
+This function does nothing if Device is not of RenewableGen type.
+"""
+function add_capacity_market_device_forecast!(sys_UC::PSY.System,
+ device_UC::D,
+ availability_raw::Vector{Float64},
+ da_resolution::Int64) where D <: Union{PSY.ThermalGen, PSY.HydroGen, PSY.Storage}
+
+ return
+end
+
+"""
+This function adds forecast timeseries to the future capacity market system if Device is of RenewableGen type.
+"""
+function add_capacity_market_device_forecast!(sys_UC::PSY.System,
+ device_UC::D,
+ availability_raw::Vector{Float64},
+ da_resolution::Int64) where D <: PSY.RenewableGen
+
+ ######### Adding to UC##########
+ time_stamps = TS.timestamp(PSY.get_data(PSY.get_time_series(
+ PSY.SingleTimeSeries,
+ first(PSY.get_components(PSY.ElectricLoad, sys_UC)),
+ "max_active_power"
+ )))
+
+ intervals = Int(24 * 60 / da_resolution)
+ append!(availability_raw, availability_raw[(length(availability_raw) - intervals + 1):end])
+ data = Dict(time_stamps[i] => availability_raw[i:(i + intervals - 1)] for i in 1:intervals:length(time_stamps))
+ forecast = PSY.Deterministic("max_active_power", data, Dates.Minute(da_resolution))
+ PSY.add_time_series!(sys_UC, device_UC, forecast)
+
+ return
+end
+
+function add_capacity_market_project!(capacity_market_system::PSY.System,
+ project::Project,
+ simulation_dir::String,
+ da_resolution::Int64)
+ PSY_project = create_PSY_generator(project, capacity_market_system)
+
+ PSY.add_component!(capacity_market_system, PSY_project)
+
+ for product in get_products(project)
+ add_device_services!(capacity_market_system, PSY_project, product)
+ end
+
+ type = get_type(get_tech(project))
+ zone = get_zone(get_tech(project))
+
+ availability_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Availability", "DAY_AHEAD_availability.csv"))
+
+ if in(get_name(project), names(availability_df))
+ availability_raw = availability_df[:, Symbol(get_name(project))]
+ elseif in("$(type)_$(zone)", names(availability_df))
+ availability_raw = availability_df[:, Symbol("$(type)_$(zone)")]
+ end
+
+ add_capacity_market_device_forecast!(capacity_market_system, PSY_project, availability_raw, da_resolution)
+end
+
+function create_capacity_mkt_system(initial_system::PSY.System,
+ active_projects::Vector{Project},
+ capacity_forward_years::Int64,
+ iteration_year::Int64,
+ load_growth::AxisArrays.AxisArray{Float64, 1},
+ simulation_dir::String,
+ da_resolution::Int64)
+
+ println("Creating Forward Capacity Market System")
+ capacity_market_system = deepcopy(initial_system)
+
+ capacity_market_year = iteration_year + capacity_forward_years - 1
+ capacity_market_projects = Project[]
+ option_leaftypes = leaftypes(Project{Option})
+ non_option_projects = filter(project -> !in(typeof(project), option_leaftypes), active_projects)
+
+ for project in non_option_projects
+ end_life_year = get_end_life_year(project)
+ construction_year = get_construction_year(project)
+ if end_life_year >= capacity_market_year && construction_year <= capacity_market_year
+ push!(capacity_market_projects, project)
+ if !(get_name(project) in PSY.get_name.(get_all_techs(capacity_market_system)))
+ add_capacity_market_project!(capacity_market_system, project, simulation_dir, da_resolution)
+ end
+ end
+
+ end
+
+ for device in get_all_techs(capacity_market_system)
+ if !(PSY.get_name(device) in get_name.(capacity_market_projects))
+ PSY.remove_component!(capacity_market_system, device)
+ end
+ end
+
+ nodal_loads = PSY.get_components(PSY.ElectricLoad, capacity_market_system)
+
+ for load in nodal_loads
+ zone = "zone_$(PSY.get_name(PSY.get_area(PSY.get_bus(load))))"
+ scaled_active_power = deepcopy(PSY.get_max_active_power(load)) * (1 + load_growth["load_$(zone)"]) ^ (capacity_forward_years)
+ PSY.set_max_active_power!(load, scaled_active_power)
+
+ end
+
+ return capacity_market_system
+
+end
+
+function check_ra_conditions(ra_targets::Dict{String, Float64}, ra_metrics::Dict{String, Float64})
+
+ metrics = keys(ra_targets)
+
+ adequacy_conditions = falses(length(metrics))
+ scarcity_conditions = falses(length(metrics))
+ for (idx, metric) in enumerate(metrics)
+ if ra_metrics[metric] <= ra_targets[metric]
+ adequacy_conditions[idx] = true
+ else
+ scarcity_conditions[idx] = true
+ end
+ end
+
+ adequacy_conditions_met = prod(adequacy_conditions)
+ scarcity_conditions_met = true
+ if sum(scarcity_conditions) < 1
+ scarcity_conditions_met = false
+ end
+
+ return adequacy_conditions_met, scarcity_conditions_met
+end
+
+function update_delta_irm!(initial_system::PSY.System,
+ active_projects::Vector{Project},
+ capacity_forward_years::Int64,
+ resource_adequacy::ResourceAdequacy,
+ peak_load::Float64,
+ static_capacity_market::Bool,
+ iteration_year::Int64,
+ load_growth::AxisArrays.AxisArray{Float64, 1},
+ simulation_dir::String,
+ da_resolution::Int64)
+
+ if !(static_capacity_market)
+ forward_peak_load = peak_load * (1 + Statistics.mean(load_growth)) ^ (capacity_forward_years)
+ capacity_market_system = create_capacity_mkt_system(initial_system,
+ active_projects,
+ capacity_forward_years,
+ iteration_year,
+ load_growth,
+ simulation_dir,
+ da_resolution)
+
+ ra_targets = get_targets(resource_adequacy)
+ delta_irm = 0.0
+
+ all_capacity_market_projects = get_all_techs(capacity_market_system)
+ removeable_projects = PSY.Generator[]
+
+ CT_generators = sort!(filter(project -> occursin("CT", string(PSY.get_prime_mover(project))), all_capacity_market_projects), by = x -> get_device_size(x))
+ append!(removeable_projects, CT_generators)
+ CC_generators = sort!(filter(project -> occursin("CC", string(PSY.get_prime_mover(project))), all_capacity_market_projects), by = x -> get_device_size(x))
+ append!(removeable_projects, CC_generators)
+
+ @time begin
+ if !isempty(ra_targets)
+ ra_metrics = calculate_RA_metrics(capacity_market_system)
+ println(ra_metrics)
+ adequacy_conditions_met, scarcity_conditions_met = check_ra_conditions(ra_targets, ra_metrics)
+
+ count = 1
+ total_added_capacity = 0.0
+ total_removed_capacity = 0.0
+ removed_capacity = 0.0
+
+ if !(adequacy_conditions_met)
+ while !(adequacy_conditions_met)
+ incremental_project = deepcopy(first(filter(p -> occursin("new_CT", get_name(p)), active_projects)))
+ set_name!(incremental_project, "addition_CT_project_$(count)")
+ total_added_capacity += get_maxcap(incremental_project)
+ add_capacity_market_project!(capacity_market_system, incremental_project, simulation_dir, da_resolution)
+ ra_metrics = calculate_RA_metrics(capacity_market_system)
+ println(ra_metrics)
+ adequacy_conditions_met, scarcity_conditions_met = check_ra_conditions(ra_targets, ra_metrics)
+ count += 1
+ end
+
+ elseif !(scarcity_conditions_met)
+ while !(scarcity_conditions_met) && (total_removed_capacity <= 400)
+ if !(isempty(removeable_projects))
+ removed_project = removeable_projects[1]
+ popfirst!(removeable_projects)
+ removed_capacity = get_device_size(removed_project) * PSY.get_base_power(removed_project)
+ total_removed_capacity += removed_capacity
+ PSY.remove_component!(capacity_market_system, removed_project)
+ ra_metrics = calculate_RA_metrics(capacity_market_system)
+ println(ra_metrics)
+ adequacy_conditions_met, scarcity_conditions_met = check_ra_conditions(ra_targets, ra_metrics)
+ count += 1
+ end
+ end
+ total_removed_capacity -= removed_capacity
+ end
+
+ delta_irm = (total_added_capacity - total_removed_capacity) / forward_peak_load
+ end
+ end
+
+ set_delta_irm!(resource_adequacy, iteration_year, delta_irm)
+ end
+
+ return
+end
diff --git a/src/struct_creators/market_structs/cem_creator.jl b/src/struct_creators/market_structs/cem_creator.jl
index c437d7a..4305fc0 100644
--- a/src/struct_creators/market_structs/cem_creator.jl
+++ b/src/struct_creators/market_structs/cem_creator.jl
@@ -1,233 +1,259 @@
-"""
-This function creates the MarketClearingProblem struct for CEM.
-"""
-function create_cem_mkt_clr_problem(investor_dir::String,
- market_names::Vector{Symbol},
- carbon_tax::Vector{Float64},
- reserve_products::Vector{String},
- ordc_products::Vector{String},
- expected_portfolio::Vector{<: Project{<: BuildPhase}},
- zones::Vector{String},
- lines::Vector{ZonalLine},
- peak_load::Float64,
- rep_hour_weight::Vector{Float64},
- average_capital_cost_multiplier::Float64,
- scenario::Scenario,
- iteration_year::Int64,
- yearly_horizon::Int64)
-
- num_invperiods = yearly_horizon
-
- #Get updated load growth belief
- load_growth = AxisArrays.AxisArray(zeros(length(zones), num_invperiods), zones, collect(1:num_invperiods))
- for (idx, zone) in enumerate(zones)
- load_growth[idx, :] = get_parameter_values(scenario)[iteration_year]["load_$(zone)", iteration_year:iteration_year + num_invperiods - 1]
- end
-
- average_load_growth = [Statistics.mean(load_growth[:, p]) for p in 1:num_invperiods]
-
- carbon_tax_vector = carbon_tax[iteration_year:iteration_year + num_invperiods - 1]
-
- # Gather markets data-------------------------------------------------------------------------------
-
- ######################################### Create Energy Markets ####################################################
- load_data = read_data(joinpath(investor_dir, "timeseries_data_files", "Load", "load_$(iteration_year - 1).csv"))
-
- num_hours = DataFrames.nrow(load_data)
-
- energy_mkt_params = read_data(joinpath(investor_dir, "markets_data", "Energy.csv"))
- price_cap_energy = AxisArrays.AxisArray(energy_mkt_params.price_cap * 1.0, zones)
- zonal_load = AxisArrays.AxisArray(zeros(length(zones), num_hours), zones, (1:num_hours))
-
- for (zone_num, zone) in enumerate(zones)
- for h in 1:num_hours
- zonal_load[zone, h] = load_data[:, Symbol(zone_num)][h]
- end
- end
-
- energy_annual_increment = AxisArrays.AxisArray(ones(length(zones), num_invperiods), zones, collect(1:num_invperiods))
-
- energy_markets = Vector{EnergyMarket}(undef, num_invperiods)
-
- for p in 1:num_invperiods
- for z in zones
- for i in 1:p
- energy_annual_increment[z, p] = energy_annual_increment[z, p] * (1 + load_growth[z, i])
- end
- end
-
- energy_markets[p] = EnergyMarket(AxisArrays.AxisArray(zonal_load .* energy_annual_increment[:, p], zones, (1:num_hours)),
- price_cap_energy)
- end
-
- ######################################### Create Reserve Markets ####################################################
-
- average_annual_increment = ones(num_invperiods)
-
- reserve_up_markets = Vector{Dict{String, ReserveUpMarket{num_hours}}}(undef, num_invperiods)
- reserve_down_markets = Vector{Dict{String, ReserveDownMarket{num_hours}}}(undef, num_invperiods)
- reserve_ordc_markets = Vector{Dict{String, ReserveORDCMarket{num_hours}}}(undef, num_invperiods)
-
- reserve_timeseries_data = Dict(r => read_data(joinpath(investor_dir, "timeseries_data_files", "Reserves", "$(r)_$(iteration_year - 1).csv"))[:, r] for r in reserve_products)
- reserve_parameter_data = Dict(r => read_data(joinpath(investor_dir, "markets_data", "$(r).csv")) for r in reserve_products)
-
- reserve_eligible_projects = Dict(product => String[] for product in reserve_products)
-
- for p in 1:num_invperiods
- reserve_up_market = Dict{String, ReserveUpMarket{num_hours}}()
- reserve_down_market = Dict{String, ReserveDownMarket{num_hours}}()
- reserve_ordc_market = Dict{String, ReserveORDCMarket{num_hours}}()
-
- for i in 1:p
- average_annual_increment[p] = average_annual_increment[p] * (1 + average_load_growth[i])
- end
-
- for product in reserve_products
-
- if Symbol(product) in market_names
- timeseries_data = reserve_timeseries_data[product]
- parameter_data = reserve_parameter_data[product]
-
- if product in ordc_products
- market = create_ordc_market(timeseries_data, parameter_data, reserve_eligible_projects[product])
- reserve_ordc_market[product] = market
- else
- direction = lowercase(parameter_data[1, "direction"])
- price_cap = Float64(parameter_data[1, "price_cap"])
- zones = ["zone_$(n)" for n in split(parameter_data[1, "eligible_zones"], ";")]
- if direction == "up"
- market = ReserveUpMarket(timeseries_data * average_annual_increment[p], price_cap, zones, reserve_eligible_projects[product])
- reserve_up_market[product] = market
- elseif direction == "down"
- market = ReserveDownMarket(timeseries_data * average_annual_increment[p], price_cap, zones, reserve_eligible_projects[product])
- reserve_down_market[product] = market
- end
- end
- end
- end
- reserve_up_markets[p] = reserve_up_market
- reserve_down_markets[p] = reserve_down_market
- reserve_ordc_markets[p] = reserve_ordc_market
- end
-
- ######################################### Create Capacity and REC Markets ####################################################
-
- capacity_market_bool = false
- rec_market_bool = false
-
- if in(:Capacity, market_names)
- capacity_market_bool = true
- end
-
- if in(:REC, market_names)
- rec_market_bool = true
- end
-
- capacity_mkt_param_file = joinpath(investor_dir, "markets_data", "Capacity.csv")
-
- REC_mkt_params = read_data(joinpath(investor_dir, "markets_data", "REC.csv"))
- price_cap_rec = REC_mkt_params[1, "price_cap"]
- rec_req = REC_mkt_params[1, "rec_req"] * rec_market_bool
- rec_annual_increment = REC_mkt_params[1, "annual_increment"] * rec_market_bool
-
- capacity_markets = Vector{CapacityMarket}(undef, num_invperiods)
- rec_markets = Vector{RECMarket}(undef, num_invperiods)
-
- for p in 1:num_invperiods
- system_peak_load = average_annual_increment[p] * peak_load
-
- capacity_markets[p] = create_capacity_demand_curve(capacity_mkt_param_file, system_peak_load, capacity_market_bool)
- rec_markets[p] = RECMarket(min(rec_req + rec_annual_increment * (p + iteration_year - 1), 1), price_cap_rec)
- end
-
- max_peak_loads = AxisArrays.AxisArray([maximum([maximum(market.demand[z, :]) for market in energy_markets]) for z in zones], zones)
-
- markets = MarketCollection.(capacity_markets,
- energy_markets,
- reserve_up_markets,
- reserve_down_markets,
- reserve_ordc_markets,
- rec_markets)
- #-----------------------------------------------------------------------------------------------------------------------------
- availability_df = read_data(joinpath(investor_dir, "timeseries_data_files", "Availability", "DAY_AHEAD_availability.csv"))
-
- invested_portfolio = find_active_invested_projects(expected_portfolio)
- option_portfolio = find_option_projects(expected_portfolio)
-
- cem_projects = MarketProject[]
-
- for project in invested_portfolio
- tech = get_tech(project)
- zone = get_zone(tech)
- cem_project = create_market_project(project,
- price_cap_energy[zone],
- max_peak_loads,
- iteration_year,
- num_hours,
- num_invperiods,
- availability_df)
-
- if !isnothing(cem_project)
- push!(cem_projects, cem_project)
- end
-
- products = get_products(project)
- for product in products
- product_name = String(get_name(product))
- if product_name in reserve_products
- push!(reserve_eligible_projects[product_name], cem_project.name)
- end
- end
- end
-
- aggregated_options = MarketProject[]
- for option in option_portfolio
- tech = get_tech(option)
- zone = get_zone(tech)
- similar_option = filter(p -> (get_type(tech) == p.tech_type && zone == p.zone), aggregated_options)
-
- if length(similar_option) < 1
- aggregated_option = create_market_project(option,
- price_cap_energy[zone],
- max_peak_loads,
- iteration_year,
- num_hours,
- num_invperiods,
- availability_df)
- push!(aggregated_options, aggregated_option)
-
- products = get_products(option)
- for product in products
- product_name = String(get_name(product))
- if product_name in reserve_products
- push!(reserve_eligible_projects[product_name], aggregated_option.name)
- end
- end
- else
- investment_cost = get_investment_cost(get_finance_data(option))[iteration_year:iteration_year + num_invperiods - 1]
- for (i, cost) in enumerate(similar_option[1].expansion_cost)
- if investment_cost[i] > cost
- similar_option[1].expansion_cost[i] = investment_cost[i]
- end
- end
-
- discount_rate = get_discount_rate(get_finance_data(option))
- if discount_rate > similar_option[1].discount_rate
- similar_option[1].discount_rate = discount_rate
- end
-
- owner = get_ownedby(get_finance_data(option))
- if !in(owner, similar_option[1].ownedby)
- similar_option[1].base_cost_units = similar_option[1].base_cost_units + 1
- push!(similar_option[1].ownedby, owner)
- end
- end
-
- end
-
- append!(cem_projects, aggregated_options)
-
- system = MarketClearingProblem(zones, lines, average_capital_cost_multiplier, markets, carbon_tax_vector, cem_projects, rep_hour_weight)
-
- return system
-end
+"""
+This function creates the MarketClearingProblem struct for CEM.
+"""
+function create_cem_mkt_clr_problem(investor_dir::String,
+ market_names::Vector{Symbol},
+ carbon_tax::Vector{Float64},
+ reserve_products::Vector{String},
+ ordc_products::Vector{String},
+ rps_target::String,
+ reserve_penalty::String,
+ resource_adequacy::ResourceAdequacy,
+ irm_scalar::Float64,
+ expected_portfolio::Vector{<: Project{<: BuildPhase}},
+ zones::Vector{String},
+ lines::Vector{ZonalLine},
+ peak_load::Float64,
+ rep_hour_weight::Vector{Float64},
+ average_capital_cost_multiplier::Float64,
+ scenario::Scenario,
+ iteration_year::Int64,
+ yearly_horizon::Int64)
+
+ num_invperiods = yearly_horizon
+
+ #Get updated load growth belief
+ load_growth = AxisArrays.AxisArray(zeros(length(zones), num_invperiods), zones, collect(1:num_invperiods))
+ for (idx, zone) in enumerate(zones)
+ load_growth[idx, :] = get_parameter_values(scenario)[iteration_year]["load_$(zone)", iteration_year:iteration_year + num_invperiods - 1]
+ end
+
+ average_load_growth = [Statistics.mean(load_growth[:, p]) for p in 1:num_invperiods]
+
+ carbon_tax_vector = carbon_tax[iteration_year:iteration_year + num_invperiods - 1]
+
+ # Gather markets data-------------------------------------------------------------------------------
+
+ ######################################### Create Energy Markets ####################################################
+ load_data = read_data(joinpath(investor_dir, "timeseries_data_files", "Load", "load_$(iteration_year - 1).csv"))
+
+ num_hours = DataFrames.nrow(load_data)
+
+ energy_mkt_params = read_data(joinpath(investor_dir, "markets_data", "Energy.csv"))
+ price_cap_energy = AxisArrays.AxisArray(energy_mkt_params.price_cap * 1.0, zones)
+ zonal_load = AxisArrays.AxisArray(zeros(length(zones), num_hours), zones, (1:num_hours))
+
+ for (zone_num, zone) in enumerate(zones)
+ for h in 1:num_hours
+ zonal_load[zone, h] = load_data[:, Symbol(zone_num)][h]
+ end
+ end
+
+ energy_annual_increment = AxisArrays.AxisArray(ones(length(zones), num_invperiods), zones, collect(1:num_invperiods))
+
+ energy_markets = Vector{EnergyMarket}(undef, num_invperiods)
+
+ for p in 1:num_invperiods
+ for z in zones
+ for i in 1:p
+ energy_annual_increment[z, p] = energy_annual_increment[z, p] * (1 + load_growth[z, i])
+ end
+ end
+
+ energy_markets[p] = EnergyMarket(AxisArrays.AxisArray(zonal_load .* energy_annual_increment[:, p], zones, (1:num_hours)),
+ price_cap_energy)
+ end
+
+ ######################################### Create Reserve Markets ####################################################
+
+ average_annual_increment = ones(num_invperiods)
+
+ reserve_up_markets = Vector{Dict{String, ReserveUpMarket{num_hours}}}(undef, num_invperiods)
+ reserve_down_markets = Vector{Dict{String, ReserveDownMarket{num_hours}}}(undef, num_invperiods)
+ reserve_ordc_markets = Vector{Dict{String, ReserveORDCMarket{num_hours}}}(undef, num_invperiods)
+
+ reserve_timeseries_data = Dict(r => read_data(joinpath(investor_dir, "timeseries_data_files", "Reserves", "$(r)_$(iteration_year - 1).csv"))[:, r] for r in reserve_products)
+ reserve_parameter_data = Dict(r => read_data(joinpath(investor_dir, "markets_data", "$(reserve_penalty)_reserve_penalty", "$(r).csv")) for r in reserve_products)
+
+ reserve_eligible_projects = Dict(product => String[] for product in reserve_products)
+
+ for p in 1:num_invperiods
+ reserve_up_market = Dict{String, ReserveUpMarket{num_hours}}()
+ reserve_down_market = Dict{String, ReserveDownMarket{num_hours}}()
+ reserve_ordc_market = Dict{String, ReserveORDCMarket{num_hours}}()
+
+ for i in 1:p
+ average_annual_increment[p] = average_annual_increment[p] * (1 + average_load_growth[i])
+ end
+
+ for product in reserve_products
+
+ if Symbol(product) in market_names
+ timeseries_data = reserve_timeseries_data[product]
+ parameter_data = reserve_parameter_data[product]
+
+ if product in ordc_products
+ market = create_ordc_market(timeseries_data, parameter_data, reserve_eligible_projects[product])
+ reserve_ordc_market[product] = market
+ else
+ direction = lowercase(parameter_data[1, "direction"])
+ price_cap = Float64(parameter_data[1, "price_cap"])
+ zones = ["zone_$(n)" for n in split(parameter_data[1, "eligible_zones"], ";")]
+ if direction == "up"
+ market = ReserveUpMarket(timeseries_data * average_annual_increment[p], price_cap, zones, reserve_eligible_projects[product])
+ reserve_up_market[product] = market
+ elseif direction == "down"
+ market = ReserveDownMarket(timeseries_data * average_annual_increment[p], price_cap, zones, reserve_eligible_projects[product])
+ reserve_down_market[product] = market
+ end
+ end
+ end
+ end
+ reserve_up_markets[p] = reserve_up_market
+ reserve_down_markets[p] = reserve_down_market
+ reserve_ordc_markets[p] = reserve_ordc_market
+ end
+
+ ######################################### Create Capacity and REC Markets ####################################################
+
+ capacity_market_bool = false
+ rec_market_bool = false
+ inertia_market_bool = false
+
+ if in(:Capacity, market_names)
+ capacity_market_bool = true
+ end
+
+ if in(:REC, market_names)
+ rec_market_bool = true
+ end
+
+ delta_irm = get_delta_irm(resource_adequacy, iteration_year)
+
+ capacity_mkt_param_file = joinpath(investor_dir, "markets_data", "Capacity.csv")
+
+ REC_mkt_params = read_data(joinpath(investor_dir, "markets_data", "REC_$(rps_target)_RPS.csv"))
+ price_cap_rec = REC_mkt_params[1, "price_cap"]
+ rec_req = REC_mkt_params[1, "rec_req"] * rec_market_bool
+ rec_annual_increment = REC_mkt_params[1, "annual_increment"] * rec_market_bool
+ rec_non_binding_years = REC_mkt_params[1, "non_binding_years"] * rec_market_bool
+
+ rec_binding_array = Int.(zeros(num_invperiods))
+ binding_years_from_now = max((rec_non_binding_years - iteration_year + 1), 0) + 1
+
+ for j in max(4, binding_years_from_now):num_invperiods
+ rec_binding_array[j] = 1
+ end
+
+ if in(:Inertia, market_names)
+ inertia_market_bool = true
+ end
+
+ inertia_mkt_params = read_data(joinpath(investor_dir, "markets_data", "$(reserve_penalty)_reserve_penalty", "Inertia.csv"))
+ price_cap_inertia = inertia_mkt_params[1, "price_cap"]
+ inertia_req_multiplier = inertia_mkt_params[1, "requirement_multiplier"] * inertia_market_bool
+
+ capacity_markets = Vector{CapacityMarket}(undef, num_invperiods)
+ rec_markets = Vector{RECMarket}(undef, num_invperiods)
+ inertia_markets = Vector{InertiaMarket}(undef, num_invperiods)
+
+ for p in 1:num_invperiods
+ system_peak_load = average_annual_increment[p] * peak_load
+
+ capacity_markets[p] = create_capacity_demand_curve(capacity_mkt_param_file, system_peak_load, irm_scalar, delta_irm, capacity_market_bool)
+ rec_markets[p] = RECMarket(min(rec_req + rec_annual_increment * (p + iteration_year - 1), 1), price_cap_rec, !(iszero(rec_binding_array[p])))
+ inertia_markets[p] = InertiaMarket(system_peak_load * inertia_req_multiplier, price_cap_inertia)
+ end
+
+ max_peak_loads = AxisArrays.AxisArray([maximum([maximum(market.demand[z, :]) for market in energy_markets]) for z in zones], zones)
+
+ markets = MarketCollection.(capacity_markets,
+ energy_markets,
+ reserve_up_markets,
+ reserve_down_markets,
+ reserve_ordc_markets,
+ rec_markets,
+ inertia_markets)
+ #-----------------------------------------------------------------------------------------------------------------------------
+ availability_df = read_data(joinpath(investor_dir, "timeseries_data_files", "Availability", "DAY_AHEAD_availability.csv"))
+
+ invested_portfolio = find_active_invested_projects(expected_portfolio)
+ option_portfolio = find_option_projects(expected_portfolio)
+
+ cem_projects = MarketProject[]
+
+ for project in invested_portfolio
+ tech = get_tech(project)
+ zone = get_zone(tech)
+ cem_project = create_market_project(project,
+ price_cap_energy[zone],
+ max_peak_loads,
+ iteration_year,
+ num_hours,
+ num_invperiods,
+ availability_df)
+
+ if !isnothing(cem_project)
+ push!(cem_projects, cem_project)
+ end
+
+ products = get_products(project)
+ for product in products
+ product_name = String(get_name(product))
+ if product_name in reserve_products
+ push!(reserve_eligible_projects[product_name], cem_project.name)
+ end
+ end
+ end
+
+ aggregated_options = MarketProject[]
+ for option in option_portfolio
+ tech = get_tech(option)
+ zone = get_zone(tech)
+ similar_option = filter(p -> (get_type(tech) == p.tech_type && zone == p.zone), aggregated_options)
+
+ if length(similar_option) < 1
+ aggregated_option = create_market_project(option,
+ price_cap_energy[zone],
+ max_peak_loads,
+ iteration_year,
+ num_hours,
+ num_invperiods,
+ availability_df)
+ push!(aggregated_options, aggregated_option)
+
+ products = get_products(option)
+ for product in products
+ product_name = String(get_name(product))
+ if product_name in reserve_products
+ push!(reserve_eligible_projects[product_name], aggregated_option.name)
+ end
+ end
+ else
+ investment_cost = get_investment_cost(get_finance_data(option))[iteration_year:iteration_year + num_invperiods - 1]
+ for (i, cost) in enumerate(similar_option[1].expansion_cost)
+ if investment_cost[i] > cost
+ similar_option[1].expansion_cost[i] = investment_cost[i]
+ end
+ end
+
+ discount_rate = get_discount_rate(get_finance_data(option))
+ if discount_rate > similar_option[1].discount_rate
+ similar_option[1].discount_rate = discount_rate
+ end
+
+ owner = get_ownedby(get_finance_data(option))
+ if !in(owner, similar_option[1].ownedby)
+ similar_option[1].base_cost_units = similar_option[1].base_cost_units + 1
+ push!(similar_option[1].ownedby, owner)
+ end
+ end
+
+ end
+
+ append!(cem_projects, aggregated_options)
+
+ system = MarketClearingProblem(zones, lines, average_capital_cost_multiplier, markets, carbon_tax_vector, cem_projects, rep_hour_weight)
+
+ return system
+end
diff --git a/src/struct_creators/market_structs/economic_dispatch_creator.jl b/src/struct_creators/market_structs/economic_dispatch_creator.jl
index 3da5d93..39970dc 100644
--- a/src/struct_creators/market_structs/economic_dispatch_creator.jl
+++ b/src/struct_creators/market_structs/economic_dispatch_creator.jl
@@ -1,161 +1,163 @@
-"""
-This function creates the MarketClearingProblem struct for endogenous Economic Dispatch.
-"""
-function create_economic_dispatch_problem(simulation::AgentSimulation,
- sys_UC::PSY.System,
- market_names::Vector{Symbol},
- num_invperiods::Int64,
- load_growth::AxisArrays.AxisArray{Float64, 1},
- existing_projects::Vector{<: Project{<: BuildPhase}},
- iteration_year::Int64)
-
- simulation_dir = get_data_dir(get_case(simulation))
- load_data = read_data(joinpath(simulation_dir, "timeseries_data_files", "Load", "load_$(iteration_year - 1).csv"))
- num_hours = DataFrames.nrow(load_data)
- zones = get_zones(simulation)
- zonal_load = AxisArrays.AxisArray(zeros(length(zones), num_hours), zones, (1:num_hours))
-
- for (zone_num, zone) in enumerate(zones)
- for h in 1:num_hours
- zonal_load[zone, h] = load_data[:, Symbol(zone_num)][h]
- end
- end
-
- if isnothing(sys_UC)
- lines = get_lines(simulation)
-
- reserve_up_market_bool = false
- reserve_down_market_bool = false
- capacity_market_bool = false
- rec_market_bool = false
-
- #Find if the following markets are being simulated:
- if in(:ReserveUp, market_names)
- reserve_up_market_bool = true
- end
-
- if in(:ReserveDown, market_names)
- reserve_down_market_bool = true
- end
-
- if in(:Capacity, market_names)
- capacity_market_bool = true
- end
-
- if in(:REC, market_names)
- rec_market_bool = true
- end
-
- # Gather markets data-------------------------------------------------------------------------------
- reserve_up_demand_data = read_data(joinpath(simulation_dir, "timeseries_data_files", "Reserves", "reserve_up_$(iteration_year - 1).csv"))
- reserve_down_demand_data = read_data(joinpath(simulation_dir, "timeseries_data_files", "Reserves", "reserve_down_$(iteration_year - 1).csv"))
-
- energy_mkt_params = read_data(joinpath(simulation_dir, "markets_data", "energy_mkt_param.csv"))
- price_cap_energy = AxisArrays.AxisArray(energy_mkt_params.price_cap * 1.0, zones)
-
- reserve_up_mkt_params = read_data(joinpath(simulation_dir, "markets_data", "reserve_up_mkt_param.csv"))
- reserve_up_num_points = AxisArrays.AxisArray(zeros(length(zones)), zones)
- reserve_up_break_points = AxisArrays.AxisArray([Float64[] for z in zones], zones)
- reserve_up_price_points = AxisArrays.AxisArray([Float64[] for z in zones], zones)
-
- reserve_down_mkt_params = read_data(joinpath(simulation_dir, "markets_data", "reserve_down_mkt_param.csv"))
- price_cap_reservedown = AxisArrays.AxisArray(reserve_down_mkt_params.price_cap * 1.0, zones)
-
- zonal_reserve_up = AxisArrays.AxisArray(zeros(length(zones), num_hours), zones, (1:num_hours))
- zonal_reserve_down = AxisArrays.AxisArray(zeros(length(zones), num_hours), zones, (1:num_hours))
-
- for (zone_num, zone) in enumerate(zones)
- idx = findfirst(x -> x == "$(zone)", reserve_up_mkt_params.zones)
- if reserve_up_mkt_params.ORDC[idx]
- reserve_up_num_points[zone] = reserve_up_mkt_params.ORDC_points[idx]
- for point in 1:reserve_up_num_points[zone]
- push!(reserve_up_break_points[zone], reserve_up_mkt_params[idx, Symbol("ORDC_x$(1)")]) * reserve_up_market_bool
- push!(reserve_up_price_points[zone], reserve_up_mkt_params[idx, Symbol("ORDC_y$(1)")]) * reserve_up_market_bool
- end
- else
- reserve_up_break_points[zone] = [0.0, 1.0, 1.0, 10.0] * reserve_up_market_bool
- reserve_up_price_points[zone] = [reserve_up_mkt_params.price_cap[idx], reserve_up_mkt_params.price_cap[idx], 0.0, 0.0] * reserve_up_market_bool
- end
-
- price_cap_reservedown[zone] = reserve_down_mkt_params.price_cap[idx]
-
- for h in 1:num_hours
- zonal_reserve_up[zone, h] = reserve_up_demand_data[:, Symbol(zone_num)][h] * reserve_up_market_bool
- zonal_reserve_down[zone, h] = reserve_down_demand_data[:, Symbol(zone_num)][h] * reserve_down_market_bool
- end
- end
-
- energy_annual_increment = load_growth
- reserveup_annual_increment = load_growth
- reservedown_annual_increment = load_growth
-
- capacity_mkt_param_file = joinpath(simulation_dir, "markets_data", "capacity_mkt_param.csv")
- peak_load = get_peak_load(simulation)
- capacity_annual_increment = load_growth
-
- REC_mkt_params = read_data(joinpath(simulation_dir, "markets_data", "REC_mkt_param.csv"))
- price_cap_rec = REC_mkt_params.price_cap[1]
- rec_req = REC_mkt_params.rec_req[1] * rec_market_bool
- rec_annual_increment = REC_mkt_params.annual_increment[1] * rec_market_bool
-
- capacity_markets = Vector{CapacityMarket}(undef, num_invperiods)
- energy_markets = Vector{EnergyMarket}(undef, num_invperiods)
- reserve_up_markets = Vector{ReserveUpMarket}(undef, num_invperiods)
- reserve_down_markets = Vector{ReserveDownMarket}(undef, num_invperiods)
- rec_markets = Vector{RECMarket}(undef, num_invperiods)
-
- average_capacity_growth = Statistics.mean(capacity_annual_increment)
-
- # Create market products data for the horizon
- for p in 1:num_invperiods
- system_peak_load = (1 + average_capacity_growth) ^ (p) * peak_load
- capacity_markets[p] = create_capacity_demand_curve(capacity_mkt_param_file, system_peak_load, capacity_market_bool)
-
- energy_markets[p] = EnergyMarket(AxisArrays.AxisArray(zonal_load .* ((1 .+ energy_annual_increment) .^ p), zones, (1:num_hours)),
- price_cap_energy)
- reserve_up_markets[p] = ReserveUpMarket(reserve_up_break_points,
- reserve_up_price_points,
- AxisArrays.AxisArray(zonal_reserve_up .* ((1 .+ reserveup_annual_increment) .^ p), zones, (1:num_hours)))
- reserve_down_markets[p] = ReserveDownMarket(AxisArrays.AxisArray(zonal_reserve_down .* ((1 .+ reservedown_annual_increment) .^ p), zones, (1:num_hours)),
- price_cap_reservedown)
- rec_markets[p] = RECMarket(min(rec_req + rec_annual_increment * (p + iteration_year - 1), 1),
- price_cap_rec)
- end
-
- max_peak_loads = AxisArrays.AxisArray([maximum([maximum(market.demand[z, :]) for market in energy_markets]) for z in zones], zones)
-
- hour_weight = get_hour_weight(simulation)
-
- markets = MarketCollection.(capacity_markets,
- energy_markets,
- reserve_up_markets,
- reserve_down_markets,
- rec_markets)
- #--------------------------------------------------------------------------------------------------------------
-
- availability_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Availability", "DAY_AHEAD_availability.csv"))
-
- ed_projects = MarketProject[]
-
- for project in existing_projects
- tech = get_tech(project)
- ed_project = create_market_project(project,
- price_cap_energy[get_zone(tech)],
- maximum(reserve_up_break_points[get_zone(tech)]),
- price_cap_reservedown[get_zone(tech)],
- max_peak_loads,
- iteration_year,
- num_hours,
- num_invperiods,
- availability_df)
-
- push!(ed_projects, ed_project)
- end
-
- system = MarketClearingProblem(zones, lines, 0.0, markets, ed_projects, hour_weight)
-
- return zonal_load, system
- else
- return zonal_load, nothing
- end
-end
+"""
+This function creates the MarketClearingProblem struct for endogenous Economic Dispatch.
+"""
+function create_economic_dispatch_problem(simulation::AgentSimulation,
+ sys_UC::PSY.System,
+ market_names::Vector{Symbol},
+ num_invperiods::Int64,
+ load_growth::AxisArrays.AxisArray{Float64, 1},
+ existing_projects::Vector{<: Project{<: BuildPhase}},
+ iteration_year::Int64)
+
+ simulation_dir = get_data_dir(get_case(simulation))
+ delta_irm = get_delta_irm(get_resource_adequacy(simulation), iteration_year)
+ irm_scalar = get_irm_scalar(get_case(simulation))
+ load_data = read_data(joinpath(simulation_dir, "timeseries_data_files", "Load", "load_$(iteration_year - 1).csv"))
+ num_hours = DataFrames.nrow(load_data)
+ zones = get_zones(simulation)
+ zonal_load = AxisArrays.AxisArray(zeros(length(zones), num_hours), zones, (1:num_hours))
+
+ for (zone_num, zone) in enumerate(zones)
+ for h in 1:num_hours
+ zonal_load[zone, h] = load_data[:, Symbol(zone_num)][h]
+ end
+ end
+
+ if isnothing(sys_UC)
+ lines = get_lines(simulation)
+
+ reserve_up_market_bool = false
+ reserve_down_market_bool = false
+ capacity_market_bool = false
+ rec_market_bool = false
+
+ #Find if the following markets are being simulated:
+ if in(:ReserveUp, market_names)
+ reserve_up_market_bool = true
+ end
+
+ if in(:ReserveDown, market_names)
+ reserve_down_market_bool = true
+ end
+
+ if in(:Capacity, market_names)
+ capacity_market_bool = true
+ end
+
+ if in(:REC, market_names)
+ rec_market_bool = true
+ end
+
+ # Gather markets data-------------------------------------------------------------------------------
+ reserve_up_demand_data = read_data(joinpath(simulation_dir, "timeseries_data_files", "Reserves", "reserve_up_$(iteration_year - 1).csv"))
+ reserve_down_demand_data = read_data(joinpath(simulation_dir, "timeseries_data_files", "Reserves", "reserve_down_$(iteration_year - 1).csv"))
+
+ energy_mkt_params = read_data(joinpath(simulation_dir, "markets_data", "energy_mkt_param.csv"))
+ price_cap_energy = AxisArrays.AxisArray(energy_mkt_params.price_cap * 1.0, zones)
+
+ reserve_up_mkt_params = read_data(joinpath(simulation_dir, "markets_data", "reserve_up_mkt_param.csv"))
+ reserve_up_num_points = AxisArrays.AxisArray(zeros(length(zones)), zones)
+ reserve_up_break_points = AxisArrays.AxisArray([Float64[] for z in zones], zones)
+ reserve_up_price_points = AxisArrays.AxisArray([Float64[] for z in zones], zones)
+
+ reserve_down_mkt_params = read_data(joinpath(simulation_dir, "markets_data", "reserve_down_mkt_param.csv"))
+ price_cap_reservedown = AxisArrays.AxisArray(reserve_down_mkt_params.price_cap * 1.0, zones)
+
+ zonal_reserve_up = AxisArrays.AxisArray(zeros(length(zones), num_hours), zones, (1:num_hours))
+ zonal_reserve_down = AxisArrays.AxisArray(zeros(length(zones), num_hours), zones, (1:num_hours))
+
+ for (zone_num, zone) in enumerate(zones)
+ idx = findfirst(x -> x == "$(zone)", reserve_up_mkt_params.zones)
+ if reserve_up_mkt_params.ORDC[idx]
+ reserve_up_num_points[zone] = reserve_up_mkt_params.ORDC_points[idx]
+ for point in 1:reserve_up_num_points[zone]
+ push!(reserve_up_break_points[zone], reserve_up_mkt_params[idx, Symbol("ORDC_x$(1)")]) * reserve_up_market_bool
+ push!(reserve_up_price_points[zone], reserve_up_mkt_params[idx, Symbol("ORDC_y$(1)")]) * reserve_up_market_bool
+ end
+ else
+ reserve_up_break_points[zone] = [0.0, 1.0, 1.0, 10.0] * reserve_up_market_bool
+ reserve_up_price_points[zone] = [reserve_up_mkt_params.price_cap[idx], reserve_up_mkt_params.price_cap[idx], 0.0, 0.0] * reserve_up_market_bool
+ end
+
+ price_cap_reservedown[zone] = reserve_down_mkt_params.price_cap[idx]
+
+ for h in 1:num_hours
+ zonal_reserve_up[zone, h] = reserve_up_demand_data[:, Symbol(zone_num)][h] * reserve_up_market_bool
+ zonal_reserve_down[zone, h] = reserve_down_demand_data[:, Symbol(zone_num)][h] * reserve_down_market_bool
+ end
+ end
+
+ energy_annual_increment = load_growth
+ reserveup_annual_increment = load_growth
+ reservedown_annual_increment = load_growth
+
+ capacity_mkt_param_file = joinpath(simulation_dir, "markets_data", "capacity_mkt_param.csv")
+ peak_load = get_peak_load(simulation)
+ capacity_annual_increment = load_growth
+
+ REC_mkt_params = read_data(joinpath(simulation_dir, "markets_data", "REC_mkt_param.csv"))
+ price_cap_rec = REC_mkt_params.price_cap[1]
+ rec_req = REC_mkt_params.rec_req[1] * rec_market_bool
+ rec_annual_increment = REC_mkt_params.annual_increment[1] * rec_market_bool
+
+ capacity_markets = Vector{CapacityMarket}(undef, num_invperiods)
+ energy_markets = Vector{EnergyMarket}(undef, num_invperiods)
+ reserve_up_markets = Vector{ReserveUpMarket}(undef, num_invperiods)
+ reserve_down_markets = Vector{ReserveDownMarket}(undef, num_invperiods)
+ rec_markets = Vector{RECMarket}(undef, num_invperiods)
+
+ average_capacity_growth = Statistics.mean(capacity_annual_increment)
+
+ # Create market products data for the horizon
+ for p in 1:num_invperiods
+ system_peak_load = (1 + average_capacity_growth) ^ (p) * peak_load
+ capacity_markets[p] = create_capacity_demand_curve(capacity_mkt_param_file, system_peak_load, irms_scalar, delta_irm, capacity_market_bool)
+
+ energy_markets[p] = EnergyMarket(AxisArrays.AxisArray(zonal_load .* ((1 .+ energy_annual_increment) .^ p), zones, (1:num_hours)),
+ price_cap_energy)
+ reserve_up_markets[p] = ReserveUpMarket(reserve_up_break_points,
+ reserve_up_price_points,
+ AxisArrays.AxisArray(zonal_reserve_up .* ((1 .+ reserveup_annual_increment) .^ p), zones, (1:num_hours)))
+ reserve_down_markets[p] = ReserveDownMarket(AxisArrays.AxisArray(zonal_reserve_down .* ((1 .+ reservedown_annual_increment) .^ p), zones, (1:num_hours)),
+ price_cap_reservedown)
+ rec_markets[p] = RECMarket(min(rec_req + rec_annual_increment * (p + iteration_year - 1), 1),
+ price_cap_rec)
+ end
+
+ max_peak_loads = AxisArrays.AxisArray([maximum([maximum(market.demand[z, :]) for market in energy_markets]) for z in zones], zones)
+
+ hour_weight = get_hour_weight(simulation)
+
+ markets = MarketCollection.(capacity_markets,
+ energy_markets,
+ reserve_up_markets,
+ reserve_down_markets,
+ rec_markets)
+ #--------------------------------------------------------------------------------------------------------------
+
+ availability_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Availability", "DAY_AHEAD_availability.csv"))
+
+ ed_projects = MarketProject[]
+
+ for project in existing_projects
+ tech = get_tech(project)
+ ed_project = create_market_project(project,
+ price_cap_energy[get_zone(tech)],
+ maximum(reserve_up_break_points[get_zone(tech)]),
+ price_cap_reservedown[get_zone(tech)],
+ max_peak_loads,
+ iteration_year,
+ num_hours,
+ num_invperiods,
+ availability_df)
+
+ push!(ed_projects, ed_project)
+ end
+
+ system = MarketClearingProblem(zones, lines, 0.0, markets, ed_projects, hour_weight)
+
+ return zonal_load, system
+ else
+ return zonal_load, nothing
+ end
+end
diff --git a/src/struct_creators/market_structs/market_project_creator.jl b/src/struct_creators/market_structs/market_project_creator.jl
index 2e3ff23..fafe730 100644
--- a/src/struct_creators/market_structs/market_project_creator.jl
+++ b/src/struct_creators/market_structs/market_project_creator.jl
@@ -1,535 +1,574 @@
-"""
-This function finds the hourly availability data for projects
-to be passed to expected and actual market clearing modules.
-"""
-function find_project_availability_data(project::P,
- availability_df::DataFrames.DataFrame,
- num_invperiods::Int64,
- num_hours::Int64) where P <: Project{<:BuildPhase}
- type = get_type(get_tech(project))
- zone = get_zone(get_tech(project))
- if in(get_name(project), names(availability_df))
- availability_raw = availability_df[:, Symbol(get_name(project))]
- elseif in("$(type)_$(zone)", names(availability_df))
- availability_raw = availability_df[:, Symbol("$(type)_$(zone)")]
- else
- error("project availiability data not found!")
- end
-
- data_years = unique(availability_df.Year)
-
- data_hours = count(i -> i == data_years[1], availability_df.Year)
-
- @assert data_hours == num_hours
-
- availability_input = zeros(num_invperiods, data_hours)
-
- # Fill raw availability data into yearly and hourly values
- for y in 1:num_invperiods
- availability_input[y, 1:data_hours] = availability_raw[1:data_hours]
- end
-
- return availability_input
-end
-
-"""
-This function extracts the technical details for generators
-to be passed to expected and actual market clearing modules.
-"""
-function get_technical_details(project::P) where P <: GeneratorEMIS{<: BuildPhase}
-
- project_type = "generator"
- min_input = 0.0
- max_input = 0.0
- efficiency_in = 0.0
- efficiency_out = 0.0
- min_storage = 0.0
- max_storage = 0.0
- init_storage = 0.0
-
- return project_type, min_input, max_input, efficiency_in, efficiency_out, min_storage, max_storage, init_storage
-end
-
-"""
-This function extracts the technical details for storage units
-to be passed to expected and actual market clearing modules.
-"""
-function get_technical_details(project::P) where P <: StorageEMIS{<: BuildPhase}
-
- tech = get_tech(project)
-
- project_type = "storage"
-
- input_power_limits = get_input_active_power_limits(tech)
- min_input = input_power_limits[:min]
- max_input = input_power_limits[:max]
-
- efficiency = get_efficiency(tech)
- efficiency_in = efficiency[:in]
- efficiency_out = efficiency[:out]
-
- storage_capacity = get_storage_capacity(tech)
- min_storage = storage_capacity[:min]
- max_storage = storage_capacity[:max]
-
- init_storage = get_soc(tech)
-
- return project_type, min_input, max_input, efficiency_in, efficiency_out, min_storage, max_storage, init_storage
-end
-
-
-function get_marginal_cost_energy(product::T) where T <: Product
- return
-end
-
-function get_marginal_cost_energy(product::Energy)
- marginal_cost_energy = get_marginal_cost(product)
- return marginal_cost_energy
-end
-
-"""
-This function returns the project's marginal cost of energy product to be passed to economic dispatch and CEM.
-"""
-function get_project_energy_cost(project::P) where P <: Project{<: BuildPhase}
- marginal_cost_energy = 0.0
- for product in find_operating_products(get_products(project))
- marginal_cost_temp = get_marginal_cost_energy(product)
- if !isnothing(marginal_cost_temp)
- marginal_cost_energy = marginal_cost_temp
- end
- end
- return marginal_cost_energy
-end
-
-"""
-This function returns the project's marginal cost of reeserve product to be passed to economic dispatch and CEM.
-"""
-function get_project_reserve_cost(product::Product, project_reserve_cost::Dict{String, Float64})
- return project_reserve_cost
-end
-
-function get_project_reserve_cost(product::P, project_reserve_cost::Dict{String, Float64}) where P <: OperatingReserve
- project_reserve_cost[String(get_name(product))] = get_marginal_cost(product)
- return project_reserve_cost
-end
-
-"""
-This function returns the project's maximum participation in reeserve product to be passed to economic dispatch and CEM.
-"""
-function get_project_reserve_limit(product::Product, max_cap::Float64, project_reserve_limit::Dict{String, Float64})
- return project_reserve_limit
-end
-
-function get_project_reserve_limit(product::P, max_cap::Float64, project_reserve_limit::Dict{String, Float64}) where P <: OperatingReserve
- project_reserve_limit[String(get_name(product))] = get_max_limit(product) * max_cap
- return project_reserve_limit
-end
-
-"""
-This function returns the project's derating factor to be passed to CEM and capacity market clearing module.
-Returns 0 if there is no capacity market participation.
-"""
-function get_project_derating(project::P) where P <: Project{<: BuildPhase}
- derating_factor = 0.
- for product in get_products(project)
- derating_temp = get_derating(product)
- if !isnothing(derating_temp)
- derating_factor = derating_temp
- end
- end
- return derating_factor
-end
-
-"""
-This function returns the project's capacity market bid to be passed to the capacity market clearing module.
-Returns 0 if there is no capacity market participation.
-"""
-function get_project_capacity_market_bid(project::P) where P <: Project{<: BuildPhase}
- capacity_bid = 0.
- for product in get_products(project)
- capacity_bid_temp = get_capacity_bid(product)
- if !isnothing(capacity_bid_temp)
- capacity_bid = capacity_bid_temp
- end
- end
- return capacity_bid
-end
-
-
-"""
-This function returns the project's REC energy output to be passed the REC market market clearing module.
-Returns 0 if there is no REC market participation.
-"""
-function get_project_rec_output(project::P) where P <: Project{<: BuildPhase}
- rec_output = 0.
- for product in get_products(project)
- rec_output_temp = get_rec_certificates(product)
- if !isnothing(rec_output_temp)
- rec_output = rec_output_temp
- end
- end
- return rec_output
-end
-
-"""
-This function returns the project's REC market bid to be passed the REC market market clearing module.
-Returns 0 if there is no REC market participation.
-"""
-function get_project_rec_market_bid(project::P) where P <: Project{<: BuildPhase}
- rec_bid = 0.
- for product in get_products(project)
- rec_bid_temp = get_rec_bid(product)
- if !isnothing(rec_bid_temp)
- rec_bid = rec_bid_temp
- end
- end
- return rec_bid
-end
-
-function calculate_carbon_emissions(project_carbon_emissions::Float64, product::Product)
- return project_carbon_emissions
-end
-
-function calculate_carbon_emissions(project_carbon_emissions::Float64, product::CarbonTax)
- project_carbon_emissions = get_emission_intensity(product) * get_avg_heat_rate(product)
- return project_carbon_emissions
-end
-
-function get_emission_intensity(project::Project)
- intensity = 0.0
- for product in get_products(project)
- temp = get_emission_intensity(product)
- if !(isnothing(temp))
- intensity += temp
- end
- end
- return intensity
-end
-
-"""
-This function creates the MarketProject struct to be passed to CEM price projection and endogeneous Economic Dispatch models.
-"""
-function populate_market_project(project::P,
- project_type::String,
- min_input::Float64,
- max_input::Float64,
- efficiency_in::Float64,
- efficiency_out::Float64,
- min_storage::Float64,
- max_storage::Float64,
- init_storage::Float64,
- availability_input::Array{Float64, 2},
- existing_units::Int64,
- units_inqueue::Vector{Float64},
- remaining_lag_time::Int64,
- remaining_life_time::Int64,
- iteration_year::Int64,
- num_invperiods::Int64) where P <: Project{<: BuildPhase}
-
- finance_data = get_finance_data(project)
-
- products = get_products(project)
-
- project_reserve_cost = Dict{String, Float64}()
- project_reserve_limit = Dict{String, Float64}()
- project_carbon_emissions = 0.0
-
- max_cap = get_maxcap(project)
-
- for product in products
- project_reserve_cost = get_project_reserve_cost(product, project_reserve_cost)
- project_reserve_limit = get_project_reserve_limit(product, max_cap, project_reserve_limit)
- project_carbon_emissions = calculate_carbon_emissions(project_carbon_emissions, product)
- end
-
- market_project = MarketProject(
- get_name(project), # name
- project_type, # is project of storage type
- get_type(get_tech(project)), # technology type
- get_fixed_OM_cost(finance_data), # annualfixed O&M costs
- get_queue_cost(finance_data), # Queue cost
- get_project_energy_cost(project), # marginal cost of energy
- project_reserve_cost, # marginal cost of reserves
- project_carbon_emissions, # project emission intensity
- get_investment_cost(finance_data)[iteration_year:iteration_year + num_invperiods - 1], # yearly investment cost
- get_discount_rate(finance_data), # discount rate
- get_mincap(project), # minimum capacity
- max_cap, # maximum capacity
- min_input, # minimum input power
- max_input, # maximum input power
- efficiency_in, # input efficiency
- efficiency_out, # output efficiency
- min_storage, # minimum storage capacity
- max_storage, # maximum storage capacity
- init_storage, # initial storage level
- availability_input, # Hourly availability
- get_project_derating(project), # de-rating factor
- get_ramp_limits(get_tech(project)), # ramp limits
- project_reserve_limit, # reserve participation limits
- existing_units, # existing units
- units_inqueue, # units in queue
- get_lag_time(finance_data), # construction lead time
- remaining_lag_time, # remaining construction time
- 1, # maximum units
- 1, # base cost units
- get_capex_years(finance_data), # capital cost recovery years
- get_life_time(finance_data), # total life_time
- remaining_life_time, # remaining life_time
- in(:Capacity, get_name.(get_products(project))), # eligible for capacity markets
- in(:REC, get_name.(get_products(project))), # eligible for rps compliance
- get_zone(get_tech(project)), # project zone
- [get_ownedby(finance_data)]) # owned by
-
- return market_project
-end
-
-"""
-This function processes data of Existing projects to create the MarketProject struct
-passed to CEM price projection and endogeneous Economic Dispatch models.
-"""
-function create_market_project(project::P,
- pricecap_energy::Float64,
- max_peak_loads::AxisArrays.AxisArray{Float64, 1},
- iteration_year::Int64,
- num_hours::Int64,
- num_invperiods::Int64,
- availability_df::DataFrames.DataFrame) where P <: Project{Existing}
-
- finance_data = get_finance_data(project)
-
- queue_time = length(get_queue_cost(finance_data))
-
- # Get technical characteristics based on project type
- project_type,
- min_input,
- max_input,
- efficiency_in,
- efficiency_out,
- min_storage,
- max_storage,
- init_storage = get_technical_details(project)
-
- availability_input = find_project_availability_data(project, availability_df, num_invperiods, num_hours)
-
- # Calculate remaining life_time
- remaining_life_time = min(get_life_time(finance_data), get_end_life_year(project) - iteration_year + 1)
-
- units_inqueue = zeros(queue_time)
-
- # Existing project have completed their queue time
- units_inqueue[queue_time] = 1.0
-
- existing_units = 1
- remaining_lag_time = 0
-
-
- market_project = populate_market_project(project,
- project_type,
- min_input,
- max_input,
- efficiency_in,
- efficiency_out,
- min_storage,
- max_storage,
- init_storage,
- availability_input,
- existing_units,
- units_inqueue,
- remaining_lag_time,
- remaining_life_time,
- iteration_year,
- num_invperiods)
- return market_project
-
-end
-
-"""
-This function processes data of Option projects to create the MarketProject struct
-passed to CEM price projection and endogeneous Economic Dispatch models.
-"""
-function create_market_project(project::P,
- pricecap_energy::Float64,
- max_peak_loads::AxisArrays.AxisArray{Float64, 1},
- iteration_year::Int64,
- num_hours::Int64,
- num_invperiods::Int64,
- availability_df::DataFrames.DataFrame) where P <: Project{Option}
-
- # Get technical characteristics based on project type
- project_type,
- min_input,
- max_input,
- efficiency_in,
- efficiency_out,
- min_storage,
- max_storage,
- init_storage = get_technical_details(project)
-
- availability_input = find_project_availability_data(project, availability_df, num_invperiods, num_hours)
-
- finance_data = get_finance_data(project)
-
- queue_time = length(get_queue_cost(finance_data))
-
- # End of life_time for option projects can be up to the end of horizon.
- remaining_life_time = num_invperiods
- units_inqueue = zeros(queue_time)
- existing_units = 0
- remaining_lag_time = get_lag_time(finance_data) + queue_time
-
- market_project = populate_market_project(project,
- project_type,
- min_input,
- max_input,
- efficiency_in,
- efficiency_out,
- min_storage,
- max_storage,
- init_storage,
- availability_input,
- existing_units,
- units_inqueue,
- remaining_lag_time,
- remaining_life_time,
- iteration_year,
- num_invperiods)
-
- market_project.name = "option_$(market_project.tech_type)_$(market_project.zone)"
-
- if market_project.derating_factor == 0.0
- derating = 0.5 # Dummy derating for maximum capacity if capacity product doesn't exist
- else
- derating = market_project.derating_factor
- end
-
- modified_max_gen = max_peak_loads[market_project.zone] / derating
- market_project.max_new_options = round(modified_max_gen / market_project.max_gen)
-
- return market_project
-
-end
-
-"""
-This function processes data of Planned projects to create the MarketProject struct
-passed to CEM price projection and endogeneous Economic Dispatch models.
-"""
-function create_market_project(project::P,
- pricecap_energy::Float64,
- max_peak_loads::AxisArrays.AxisArray{Float64, 1},
- iteration_year::Int64,
- num_hours::Int64,
- num_invperiods::Int64,
- availability_df::DataFrames.DataFrame) where P <: Project{Planned}
-
- # Get technical characteristics based on project type
- project_type,
- min_input,
- max_input,
- efficiency_in,
- efficiency_out,
- min_storage,
- max_storage,
- init_storage = get_technical_details(project)
-
- availability_input = find_project_availability_data(project, availability_df, num_invperiods, num_hours)
-
- finance_data = get_finance_data(project)
-
- queue_time = length(get_queue_cost(finance_data))
-
- # Number of construction years left
- remaining_lag_time = get_construction_year(project) - iteration_year
-
- units_inqueue = zeros(queue_time)
-
- # Planned units have completed their queue time
- units_inqueue[queue_time] = 1.0
-
- # Calculate remaining life_time
- remaining_life_time = get_life_time(finance_data) + remaining_lag_time
-
- existing_units = 0
-
- market_project = populate_market_project(project,
- project_type,
- min_input,
- max_input,
- efficiency_in,
- efficiency_out,
- min_storage,
- max_storage,
- init_storage,
- availability_input,
- existing_units,
- units_inqueue,
- remaining_lag_time,
- remaining_life_time,
- iteration_year,
- num_invperiods)
- return market_project
-
-end
-
-"""
-This function processes data of Queue projects to create the MarketProject struct
-passed to CEM price projection and endogeneous Economic Dispatch models.
-"""
-function create_market_project(project::P,
- pricecap_energy::Float64,
- max_peak_loads::AxisArrays.AxisArray{Float64, 1},
- iteration_year::Int64,
- num_hours::Int64,
- num_invperiods::Int64,
- availability_df::DataFrames.DataFrame) where P <: Project{Queue}
-
- # Get technical characteristics based on project type
- project_type,
- min_input,
- max_input,
- efficiency_in,
- efficiency_out,
- min_storage,
- max_storage,
- init_storage = get_technical_details(project)
-
- availability_input = find_project_availability_data(project, availability_df, num_invperiods, num_hours)
-
- finance_data = get_finance_data(project)
-
- queue_time = length(get_queue_cost(finance_data))
-
- units_inqueue = zeros(queue_time)
-
- queue_year = iteration_year - get_decision_year(project)
-
- # Calculate number of years left in queue
- if queue_year <= queue_time
- units_inqueue[queue_year] = 1.0
- end
-
- remaining_lag_time = get_lag_time(finance_data) + queue_time - queue_year
-
- # Calculate remaining life_time
- remaining_life_time = get_life_time(finance_data) + get_lag_time(finance_data) + queue_time - queue_year
-
- existing_units = 0
-
- market_project = populate_market_project(project,
- project_type,
- min_input,
- max_input,
- efficiency_in,
- efficiency_out,
- min_storage,
- max_storage,
- init_storage,
- availability_input,
- existing_units,
- units_inqueue,
- remaining_lag_time,
- remaining_life_time,
- iteration_year,
- num_invperiods)
- return market_project
-
-end
+"""
+This function finds the hourly availability data for projects
+to be passed to expected and actual market clearing modules.
+"""
+function find_project_availability_data(project::P,
+ availability_df::DataFrames.DataFrame,
+ num_invperiods::Int64,
+ num_hours::Int64) where P <: Project{<:BuildPhase}
+ type = get_type(get_tech(project))
+ zone = get_zone(get_tech(project))
+ if in(get_name(project), names(availability_df))
+ availability_raw = availability_df[:, Symbol(get_name(project))]
+ elseif in("$(type)_$(zone)", names(availability_df))
+ availability_raw = availability_df[:, Symbol("$(type)_$(zone)")]
+ else
+ error("project availiability data not found!")
+ end
+
+ data_years = unique(availability_df.Year)
+
+ data_hours = count(i -> i == data_years[1], availability_df.Year)
+
+ @assert data_hours == num_hours
+
+ availability_input = zeros(num_invperiods, data_hours)
+
+ # Fill raw availability data into yearly and hourly values
+ for y in 1:num_invperiods
+ availability_input[y, 1:data_hours] = availability_raw[1:data_hours]
+ end
+
+ return availability_input
+end
+
+"""
+This function extracts the technical details for generators
+to be passed to expected and actual market clearing modules.
+"""
+function get_technical_details(project::P) where P <: GeneratorEMIS{<: BuildPhase}
+
+ project_type = "generator"
+ min_input = 0.0
+ max_input = 0.0
+ efficiency_in = 0.0
+ efficiency_out = 0.0
+ min_storage = 0.0
+ max_storage = 0.0
+ init_storage = 0.0
+
+ return project_type, min_input, max_input, efficiency_in, efficiency_out, min_storage, max_storage, init_storage
+end
+
+"""
+This function extracts the technical details for storage units
+to be passed to expected and actual market clearing modules.
+"""
+function get_technical_details(project::P) where P <: StorageEMIS{<: BuildPhase}
+
+ tech = get_tech(project)
+
+ project_type = "storage"
+
+ input_power_limits = get_input_active_power_limits(tech)
+ min_input = input_power_limits[:min]
+ max_input = input_power_limits[:max]
+
+ efficiency = get_efficiency(tech)
+ efficiency_in = efficiency[:in]
+ efficiency_out = efficiency[:out]
+
+ storage_capacity = get_storage_capacity(tech)
+ min_storage = storage_capacity[:min]
+ max_storage = storage_capacity[:max]
+
+ init_storage = get_soc(tech)
+
+ return project_type, min_input, max_input, efficiency_in, efficiency_out, min_storage, max_storage, init_storage
+end
+
+
+function get_marginal_cost_energy(product::T) where T <: Product
+ return
+end
+
+function get_marginal_cost_energy(product::Energy)
+ marginal_cost_energy = get_marginal_cost(product)
+ return marginal_cost_energy
+end
+
+"""
+This function returns the project's marginal cost of energy product to be passed to economic dispatch and CEM.
+"""
+function get_project_energy_cost(project::P) where P <: Project{<: BuildPhase}
+ marginal_cost_energy = 0.0
+ for product in find_operating_products(get_products(project))
+ marginal_cost_temp = get_marginal_cost_energy(product)
+ if !isnothing(marginal_cost_temp)
+ marginal_cost_energy = marginal_cost_temp
+ end
+ end
+ return marginal_cost_energy
+end
+
+"""
+This function returns the project's marginal cost of reeserve product to be passed to economic dispatch and CEM.
+"""
+function get_project_reserve_cost(product::Product, project_reserve_cost::Dict{String, Float64})
+ return project_reserve_cost
+end
+
+function get_project_reserve_cost(product::P, project_reserve_cost::Dict{String, Float64}) where P <: OperatingReserve
+ project_reserve_cost[String(get_name(product))] = get_marginal_cost(product)
+ return project_reserve_cost
+end
+
+"""
+This function returns the project's maximum participation in reeserve product to be passed to economic dispatch and CEM.
+"""
+function get_project_reserve_limit(product::Product, max_cap::Float64, project_reserve_limit::Dict{String, Float64})
+ return project_reserve_limit
+end
+
+function get_project_reserve_limit(product::P, max_cap::Float64, project_reserve_limit::Dict{String, Float64}) where P <: OperatingReserve
+ project_reserve_limit[String(get_name(product))] = get_max_limit(product) * max_cap
+ return project_reserve_limit
+end
+
+"""
+This function returns the project's derating factor to be passed to CEM and capacity market clearing module.
+Returns 0 if there is no capacity market participation.
+"""
+function get_project_derating(project::P) where P <: Project{<: BuildPhase}
+ derating_factor = 0.
+ for product in get_products(project)
+ derating_temp = get_derating(product)
+ if !isnothing(derating_temp)
+ derating_factor = derating_temp
+ end
+ end
+ return derating_factor
+end
+
+"""
+This function returns the project's capacity market bid to be passed to the capacity market clearing module.
+Returns 0 if there is no capacity market participation.
+"""
+function get_project_capacity_market_bid(project::P) where P <: Project{<: BuildPhase}
+ capacity_bid = 0.
+ for product in get_products(project)
+ capacity_bid_temp = get_capacity_bid(product)
+ if !isnothing(capacity_bid_temp)
+ capacity_bid = capacity_bid_temp
+ end
+ end
+ return capacity_bid
+end
+
+
+"""
+This function returns the project's REC energy output to be passed the REC market market clearing module.
+Returns 0 if there is no REC market participation.
+"""
+function get_project_expected_rec_output(project::P) where P <: Project{<: BuildPhase}
+ rec_output = 0.
+ for product in get_products(project)
+ rec_output_temp = get_expected_rec_certificates(product)
+ if !isnothing(rec_output_temp)
+ rec_output = rec_output_temp
+ end
+ end
+ return rec_output
+end
+
+"""
+This function returns the project's REC market bid to be passed the REC market market clearing module.
+Returns 0 if there is no REC market participation.
+"""
+function get_project_rec_market_bid(project::P) where P <: Project{<: BuildPhase}
+ rec_bid = 0.
+ for product in get_products(project)
+ rec_bid_temp = get_rec_bid(product)
+ if !isnothing(rec_bid_temp)
+ rec_bid = rec_bid_temp
+ end
+ end
+ return rec_bid
+end
+
+function calculate_carbon_emissions(project_carbon_emissions::Float64, product::Product)
+ return project_carbon_emissions
+end
+
+function calculate_carbon_emissions(project_carbon_emissions::Float64, product::CarbonTax)
+ project_carbon_emissions = get_emission_intensity(product) * get_avg_heat_rate(product)
+ return project_carbon_emissions
+end
+
+function get_emission_intensity(project::Project)
+ intensity = 0.0
+ for product in get_products(project)
+ temp = get_emission_intensity(product)
+ if !(isnothing(temp))
+ intensity += temp
+ end
+ end
+ return intensity
+end
+
+function get_inertia_constant(project::Project)
+ constant = 0.0
+ for product in get_products(project)
+ temp = get_h_constant(product)
+ if !(isnothing(temp))
+ constant += temp
+ end
+ end
+ return constant
+end
+
+function get_synchronous_inertia(project::Project)
+ synchronous_inertia = true
+ for product in get_products(project)
+ temp = get_synchronous(product)
+ if !(isnothing(temp))
+ synchronous_inertia = temp
+ end
+ end
+ return synchronous_inertia
+end
+
+function get_rec_correction_factor(project::Project, iteration_year::Int64)
+ value = 1.0
+ for product in get_products(project)
+ temp = get_rec_correction_factor(product, iteration_year)
+ if !(isnothing(temp))
+ value = temp
+ end
+ end
+ return value
+end
+
+"""
+This function creates the MarketProject struct to be passed to CEM price projection and endogeneous Economic Dispatch models.
+"""
+function populate_market_project(project::P,
+ project_type::String,
+ min_input::Float64,
+ max_input::Float64,
+ efficiency_in::Float64,
+ efficiency_out::Float64,
+ min_storage::Float64,
+ max_storage::Float64,
+ init_storage::Float64,
+ availability_input::Array{Float64, 2},
+ existing_units::Int64,
+ units_inqueue::Vector{Float64},
+ remaining_lag_time::Int64,
+ remaining_life_time::Int64,
+ iteration_year::Int64,
+ num_invperiods::Int64) where P <: Project{<: BuildPhase}
+
+ finance_data = get_finance_data(project)
+
+ products = get_products(project)
+
+ project_reserve_cost = Dict{String, Float64}()
+ project_reserve_limit = Dict{String, Float64}()
+ project_carbon_emissions = 0.0
+
+ max_cap = get_maxcap(project)
+
+ for product in products
+ project_reserve_cost = get_project_reserve_cost(product, project_reserve_cost)
+ project_reserve_limit = get_project_reserve_limit(product, max_cap, project_reserve_limit)
+ project_carbon_emissions = calculate_carbon_emissions(project_carbon_emissions, product)
+ end
+
+ inertia_constant = get_inertia_constant(project)
+ synchronous_inertia = get_synchronous_inertia(project)
+
+ market_project = MarketProject(
+ get_name(project), # name
+ project_type, # is project of storage type
+ get_type(get_tech(project)), # technology type
+ get_fixed_OM_cost(finance_data), # annualfixed O&M costs
+ get_queue_cost(finance_data), # Queue cost
+ get_project_energy_cost(project), # marginal cost of energy
+ project_reserve_cost, # marginal cost of reserves
+ project_carbon_emissions, # project emission intensity
+ get_investment_cost(finance_data)[iteration_year:iteration_year + num_invperiods - 1], # yearly investment cost
+ get_discount_rate(finance_data), # discount rate
+ get_mincap(project), # minimum capacity
+ max_cap, # maximum capacity
+ min_input, # minimum input power
+ max_input, # maximum input power
+ efficiency_in, # input efficiency
+ efficiency_out, # output efficiency
+ min_storage, # minimum storage capacity
+ max_storage, # maximum storage capacity
+ init_storage, # initial storage level
+ availability_input, # Hourly availability
+ get_project_derating(project), # de-rating factor
+ get_ramp_limits(get_tech(project)), # ramp limits
+ project_reserve_limit, # reserve participation limits
+ existing_units, # existing units
+ units_inqueue, # units in queue
+ get_lag_time(finance_data), # construction lead time
+ remaining_lag_time, # remaining construction time
+ 1, # maximum units
+ 1, # base cost units
+ get_capex_years(finance_data), # capital cost recovery years
+ get_life_time(finance_data), # total life_time
+ remaining_life_time, # remaining life_time
+ in(:Capacity, get_name.(get_products(project))), # eligible for capacity markets
+ in(:REC, get_name.(get_products(project))), # eligible for rps compliance
+ get_rec_correction_factor(project, iteration_year), # rec correction factor
+ inertia_constant, # inertia H-constant,
+ synchronous_inertia, # whether inertia is synchronous
+ get_zone(get_tech(project)), # project zone
+ [get_ownedby(finance_data)]) # owned by
+
+ return market_project
+end
+
+"""
+This function processes data of Existing projects to create the MarketProject struct
+passed to CEM price projection and endogeneous Economic Dispatch models.
+"""
+function create_market_project(project::P,
+ pricecap_energy::Float64,
+ max_peak_loads::AxisArrays.AxisArray{Float64, 1},
+ iteration_year::Int64,
+ num_hours::Int64,
+ num_invperiods::Int64,
+ availability_df::DataFrames.DataFrame) where P <: Project{Existing}
+
+ finance_data = get_finance_data(project)
+
+ queue_time = length(get_queue_cost(finance_data))
+
+ # Get technical characteristics based on project type
+ project_type,
+ min_input,
+ max_input,
+ efficiency_in,
+ efficiency_out,
+ min_storage,
+ max_storage,
+ init_storage = get_technical_details(project)
+
+ availability_input = find_project_availability_data(project, availability_df, num_invperiods, num_hours)
+
+ # Calculate remaining life_time
+ remaining_life_time = min(get_life_time(finance_data), get_end_life_year(project) - iteration_year + 1)
+
+ units_inqueue = zeros(queue_time)
+
+ # Existing project have completed their queue time
+ units_inqueue[queue_time] = 1.0
+
+ existing_units = 1
+ remaining_lag_time = 0
+
+
+ market_project = populate_market_project(project,
+ project_type,
+ min_input,
+ max_input,
+ efficiency_in,
+ efficiency_out,
+ min_storage,
+ max_storage,
+ init_storage,
+ availability_input,
+ existing_units,
+ units_inqueue,
+ remaining_lag_time,
+ remaining_life_time,
+ iteration_year,
+ num_invperiods)
+ return market_project
+
+end
+
+"""
+This function processes data of Option projects to create the MarketProject struct
+passed to CEM price projection and endogeneous Economic Dispatch models.
+"""
+function create_market_project(project::P,
+ pricecap_energy::Float64,
+ max_peak_loads::AxisArrays.AxisArray{Float64, 1},
+ iteration_year::Int64,
+ num_hours::Int64,
+ num_invperiods::Int64,
+ availability_df::DataFrames.DataFrame) where P <: Project{Option}
+
+ # Get technical characteristics based on project type
+ project_type,
+ min_input,
+ max_input,
+ efficiency_in,
+ efficiency_out,
+ min_storage,
+ max_storage,
+ init_storage = get_technical_details(project)
+
+ availability_input = find_project_availability_data(project, availability_df, num_invperiods, num_hours)
+
+ finance_data = get_finance_data(project)
+
+ queue_time = length(get_queue_cost(finance_data))
+
+ # End of life_time for option projects can be up to the end of horizon.
+ remaining_life_time = num_invperiods
+ units_inqueue = zeros(queue_time)
+ existing_units = 0
+ remaining_lag_time = get_lag_time(finance_data) + queue_time
+
+ market_project = populate_market_project(project,
+ project_type,
+ min_input,
+ max_input,
+ efficiency_in,
+ efficiency_out,
+ min_storage,
+ max_storage,
+ init_storage,
+ availability_input,
+ existing_units,
+ units_inqueue,
+ remaining_lag_time,
+ remaining_life_time,
+ iteration_year,
+ num_invperiods)
+
+ market_project.name = "option_$(market_project.tech_type)_$(market_project.zone)"
+
+ if market_project.derating_factor == 0.0
+ derating = 0.5 # Dummy derating for maximum capacity if capacity product doesn't exist
+ else
+ derating = market_project.derating_factor
+ end
+
+ modified_max_gen = max_peak_loads[market_project.zone] / derating
+ market_project.max_new_options = round(modified_max_gen / market_project.max_gen)
+
+ return market_project
+
+end
+
+"""
+This function processes data of Planned projects to create the MarketProject struct
+passed to CEM price projection and endogeneous Economic Dispatch models.
+"""
+function create_market_project(project::P,
+ pricecap_energy::Float64,
+ max_peak_loads::AxisArrays.AxisArray{Float64, 1},
+ iteration_year::Int64,
+ num_hours::Int64,
+ num_invperiods::Int64,
+ availability_df::DataFrames.DataFrame) where P <: Project{Planned}
+
+ # Get technical characteristics based on project type
+ project_type,
+ min_input,
+ max_input,
+ efficiency_in,
+ efficiency_out,
+ min_storage,
+ max_storage,
+ init_storage = get_technical_details(project)
+
+ availability_input = find_project_availability_data(project, availability_df, num_invperiods, num_hours)
+
+ finance_data = get_finance_data(project)
+
+ queue_time = length(get_queue_cost(finance_data))
+
+ # Number of construction years left
+ remaining_lag_time = get_construction_year(project) - iteration_year
+
+ units_inqueue = zeros(queue_time)
+
+ # Planned units have completed their queue time
+ units_inqueue[queue_time] = 1.0
+
+ # Calculate remaining life_time
+ remaining_life_time = get_life_time(finance_data) + remaining_lag_time
+
+ existing_units = 0
+
+ market_project = populate_market_project(project,
+ project_type,
+ min_input,
+ max_input,
+ efficiency_in,
+ efficiency_out,
+ min_storage,
+ max_storage,
+ init_storage,
+ availability_input,
+ existing_units,
+ units_inqueue,
+ remaining_lag_time,
+ remaining_life_time,
+ iteration_year,
+ num_invperiods)
+ return market_project
+
+end
+
+"""
+This function processes data of Queue projects to create the MarketProject struct
+passed to CEM price projection and endogeneous Economic Dispatch models.
+"""
+function create_market_project(project::P,
+ pricecap_energy::Float64,
+ max_peak_loads::AxisArrays.AxisArray{Float64, 1},
+ iteration_year::Int64,
+ num_hours::Int64,
+ num_invperiods::Int64,
+ availability_df::DataFrames.DataFrame) where P <: Project{Queue}
+
+ # Get technical characteristics based on project type
+ project_type,
+ min_input,
+ max_input,
+ efficiency_in,
+ efficiency_out,
+ min_storage,
+ max_storage,
+ init_storage = get_technical_details(project)
+
+ availability_input = find_project_availability_data(project, availability_df, num_invperiods, num_hours)
+
+ finance_data = get_finance_data(project)
+
+ queue_time = length(get_queue_cost(finance_data))
+
+ units_inqueue = zeros(queue_time)
+
+ queue_year = iteration_year - get_decision_year(project)
+
+ # Calculate number of years left in queue
+ if queue_year <= queue_time
+ units_inqueue[queue_year] = 1.0
+ end
+
+ remaining_lag_time = get_lag_time(finance_data) + queue_time - queue_year
+
+ # Calculate remaining life_time
+ remaining_life_time = get_life_time(finance_data) + get_lag_time(finance_data) + queue_time - queue_year
+
+ existing_units = 0
+
+ market_project = populate_market_project(project,
+ project_type,
+ min_input,
+ max_input,
+ efficiency_in,
+ efficiency_out,
+ min_storage,
+ max_storage,
+ init_storage,
+ availability_input,
+ existing_units,
+ units_inqueue,
+ remaining_lag_time,
+ remaining_life_time,
+ iteration_year,
+ num_invperiods)
+ return market_project
+
+end
diff --git a/src/struct_creators/simulation_structs/agent_simulation_creator.jl b/src/struct_creators/simulation_structs/agent_simulation_creator.jl
index e3d361a..24ee838 100644
--- a/src/struct_creators/simulation_structs/agent_simulation_creator.jl
+++ b/src/struct_creators/simulation_structs/agent_simulation_creator.jl
@@ -1,151 +1,189 @@
-"""
-This function populates and returns the AgentSimulationData struct.
-"""
-function gather_data(case::CaseDefinition)
-
- data_dir = get_data_dir(case)
- test_system_dir = get_sys_dir(case)
- start_year = get_start_year(case)
- n_rep_days = get_num_rep_days(case)
-
- annual_growth_data = read_data(joinpath(data_dir, "markets_data", "annual_growth.csv"))
- annual_growth = AxisArrays.AxisArray(collect(transpose(convert(Matrix, annual_growth_data[:, 2:end]))),
- names(annual_growth_data)[2:end],
- 1:DataFrames.nrow(annual_growth_data))
-
- zones,
- representative_days,
- rep_hour_weight,
- system_peak_load,
- test_sys_hour_weight,
- zonal_lines = read_test_system(data_dir, test_system_dir, get_base_dir(case), annual_growth, start_year, n_rep_days)
-
- if isnothing(zones)
- zones = ["zone_1"]
- end
-
- if isnothing(zonal_lines)
- zonal_lines = [ZonalLine("line_1", zones[1], zones[1], 0.0)]
- end
-
- markets_df = read_data(joinpath(data_dir, "markets_data", "markets.csv"))
- markets_dict = Dict(Symbol(names(markets_df)[i]) => markets_df[1, i] for i in 1:length(names(markets_df)))
-
- if get_siip_market_clearing(case)
- base_power = 100.0
- sys_UC, sys_ED = create_rts_sys(test_system_dir, base_power, data_dir, get_da_resolution(case), get_rt_resolution(case))
- else
- sys_UC = nothing
- sys_ED = nothing
- end
-
- simulation_years = get_simulation_years(case)
-
- carbon_tax = zeros(simulation_years)
-
- if markets_dict[:CarbonTax]
- carbon_tax_data = read_data(joinpath(data_dir, "markets_data", "CarbonTax.csv"))
- for y in 1:simulation_years
- carbon_tax[y] = carbon_tax_data[findfirst(x -> x == start_year + y - 1, carbon_tax_data[:, "Year"]), "\$/ton"]
- end
- end
-
- queue_cost_df = read_data(joinpath(data_dir, "queue_cost_data.csv"))
-
- deratingdata = read_data(joinpath(data_dir, "markets_data", "derating_dict.csv"))
-
- simulation_data = AgentSimulationData(case,
- sys_UC,
- sys_ED,
- zones,
- zonal_lines,
- representative_days,
- test_sys_hour_weight,
- rep_hour_weight,
- system_peak_load,
- markets_dict,
- carbon_tax,
- queue_cost_df,
- deratingdata,
- annual_growth)
-
- investors = create_investors(simulation_data)
- set_investors!(simulation_data, investors)
-
- #construct_ordc(data_dir, investors, 0, representative_days)
- add_psy_ordc!(data_dir, markets_dict, sys_UC, "UC")
- add_psy_ordc!(data_dir, markets_dict, sys_ED, "ED")
-
- transform_psy_timeseries!(sys_UC, sys_ED, get_da_resolution(case), get_rt_resolution(case))
-
- # Adding representative days availability data to investor folders
- system_availability_data = DataFrames.DataFrame(CSV.File(joinpath(data_dir, "timeseries_data_files", "Availability", "DAY_AHEAD_availability.csv")))
-
- rep_projects_availability = filter(row -> in(Dates.Date(row[:Year], row[:Month], row[:Day]), keys(representative_days)), system_availability_data)
-
- for dir in get_data_dir.(investors)
- write_data(joinpath(dir, "timeseries_data_files", "Availability"), "DAY_AHEAD_availability.csv", rep_projects_availability)
- end
-
- update_simulation_derating_data!(simulation_data)
-
- return simulation_data
-end
-
-"""
-This function creates the data directory for the simulated case.
-"""
-function make_case_data_dir(case::CaseDefinition)
- base_dir = get_base_dir(case)
- if get_heterogeneity(case)
- sys_data_dir = joinpath(base_dir, "Heterogeneous")
-
- else
- sys_data_dir = joinpath(base_dir, "Homogeneous")
- end
-
- case_dir = get_data_dir(case)
- dir_exists(case_dir)
- cp(sys_data_dir, case_dir, force = true)
-
- return
-end
-
-"""
-This function creates the results directory for the simulated case.
-"""
-function make_results_dir(case::CaseDefinition)
-
- case_name = get_name(case)
-
- results_dir = joinpath(".", "Results", case_name)
- dir_exists(results_dir)
-
- return results_dir
-end
-
-"""
-This function returns the AgentSimulation struct which contains all the required data for running the simulation.
-"""
-function create_agent_simulation(case::CaseDefinition)
- make_case_data_dir(case)
- results_dir = make_results_dir(case)
- simulation_data = gather_data(case)
- simulation = AgentSimulation(case,
- results_dir,
- 1,
- get_system_UC(simulation_data),
- get_system_ED(simulation_data),
- get_zones(simulation_data),
- get_lines(simulation_data),
- get_rep_days(simulation_data),
- get_hour_weight(simulation_data),
- get_peak_load(simulation_data),
- get_markets(simulation_data),
- get_carbon_tax(simulation_data),
- get_investors(simulation_data),
- get_derating_data(simulation_data),
- get_annual_growth(simulation_data))
-
- return simulation
-end
+"""
+This function populates and returns the AgentSimulationData struct.
+"""
+function gather_data(case::CaseDefinition)
+
+ data_dir = get_data_dir(case)
+ test_system_dir = get_sys_dir(case)
+ start_year = get_start_year(case)
+ n_rep_days = get_num_rep_days(case)
+
+ annual_growth_data = read_data(joinpath(data_dir, "markets_data", "annual_growth.csv"))
+ annual_growth = AxisArrays.AxisArray(collect(transpose(Matrix(annual_growth_data[:, 2:end]))),
+ names(annual_growth_data)[2:end],
+ 1:DataFrames.nrow(annual_growth_data))
+
+ zones,
+ representative_days,
+ rep_hour_weight,
+ system_peak_load,
+ test_sys_hour_weight,
+ zonal_lines = read_test_system(data_dir, test_system_dir, get_base_dir(case), annual_growth, start_year, n_rep_days)
+
+ if isnothing(zones)
+ zones = ["zone_1"]
+ end
+
+ if isnothing(zonal_lines)
+ zonal_lines = [ZonalLine("line_1", zones[1], zones[1], 0.0)]
+ end
+
+ markets_dict = get_markets(case)
+
+ if get_siip_market_clearing(case)
+ base_power = 100.0
+ sys_UC, sys_ED = create_rts_sys(test_system_dir, base_power, data_dir, get_da_resolution(case), get_rt_resolution(case))
+ else
+ sys_UC = nothing
+ sys_ED = nothing
+ end
+
+ simulation_years = get_total_horizon(case)
+
+ carbon_tax = zeros(simulation_years)
+
+ if markets_dict[:CarbonTax]
+ carbon_tax_data = read_data(joinpath(data_dir, "markets_data", "CarbonTax.csv"))
+ for y in 1:simulation_years
+ carbon_tax[y] = carbon_tax_data[findfirst(x -> x == start_year + y - 1, carbon_tax_data[:, "Year"]), "\$/ton"]
+ end
+ end
+
+ rec_requirement = zeros(simulation_years)
+ initial_rec_requirement = 0.0
+ if markets_dict[:REC]
+ rec_data = read_data(joinpath(data_dir, "markets_data", "REC_$(get_rps_target(case))_RPS.csv"))
+ initial_rec_requirement = rec_data.rec_req[1]
+ rec_increment = rec_data.annual_increment[1]
+ rec_requirement = [initial_rec_requirement + y * rec_increment for y in 1:simulation_years]
+ end
+
+ queue_cost_df = read_data(joinpath(data_dir, "queue_cost_data.csv"))
+
+ deratingdata = read_data(joinpath(data_dir, "markets_data", "derating_dict.csv"))
+
+ ra_target_file = joinpath(data_dir, "markets_data", "resource_adequacy_targets.csv")
+ ra_targets = Dict{String, Float64}()
+ ra_metrics = Dict{String, Float64}()
+
+
+ if isfile(ra_target_file)
+ for row in eachrow(read_data(ra_target_file))
+ ra_targets[row["Metric"]] = row["Target"]
+ end
+ end
+
+ resource_adequacy = ResourceAdequacy(ra_targets, zeros(simulation_years), [ra_metrics for i in 1:simulation_years])
+
+ simulation_data = AgentSimulationData(case,
+ sys_UC,
+ sys_ED,
+ zones,
+ zonal_lines,
+ representative_days,
+ test_sys_hour_weight,
+ rep_hour_weight,
+ system_peak_load,
+ markets_dict,
+ carbon_tax,
+ rec_requirement,
+ queue_cost_df,
+ deratingdata,
+ annual_growth,
+ resource_adequacy)
+
+ investors = create_investors(simulation_data)
+ set_investors!(simulation_data, investors)
+
+ # convert_thermal_clean_energy!(sys_UC)
+ # convert_thermal_clean_energy!(sys_ED)
+
+ convert_thermal_fast_start!(sys_UC)
+ convert_thermal_fast_start!(sys_ED)
+
+ construct_ordc(deepcopy(sys_UC), data_dir, investors, 0, representative_days, get_ordc_curved(case), get_ordc_unavailability_method(case), get_reserve_penalty(case))
+ add_psy_ordc!(data_dir, markets_dict, sys_UC, "UC", 1, get_da_resolution(case), get_rt_resolution(case), get_reserve_penalty(case))
+ add_psy_ordc!(data_dir, markets_dict, sys_ED, "ED", 1, get_da_resolution(case), get_rt_resolution(case), get_reserve_penalty(case))
+
+ if markets_dict[:Inertia]
+ add_psy_inertia!(data_dir, sys_UC, "UC", get_reserve_penalty(case), system_peak_load)
+ add_psy_inertia!(data_dir, sys_ED, "ED", get_reserve_penalty(case), system_peak_load)
+ end
+
+ add_psy_clean_energy_constraint!(sys_UC, initial_rec_requirement)
+
+ transform_psy_timeseries!(sys_UC, sys_ED, get_da_resolution(case), get_rt_resolution(case), 36, 2)
+
+ # Adding representative days availability data to investor folders
+ system_availability_data = DataFrames.DataFrame(CSV.File(joinpath(data_dir, "timeseries_data_files", "Availability", "DAY_AHEAD_availability.csv")))
+
+ rep_projects_availability = filter(row -> in(Dates.Date(row[:Year], row[:Month], row[:Day]), keys(representative_days)), system_availability_data)
+
+ for dir in get_data_dir.(investors)
+ write_data(joinpath(dir, "timeseries_data_files", "Availability"), "DAY_AHEAD_availability.csv", rep_projects_availability)
+ end
+
+ update_simulation_derating_data!(simulation_data, get_derating_scale(case))
+
+ return simulation_data
+end
+
+"""
+This function creates the data directory for the simulated case.
+"""
+function make_case_data_dir(case::CaseDefinition)
+ base_dir = get_base_dir(case)
+ if get_heterogeneity(case)
+ sys_data_dir = joinpath(base_dir, "Heterogeneous")
+
+ else
+ sys_data_dir = joinpath(base_dir, "Homogeneous")
+ end
+
+ case_dir = get_data_dir(case)
+ dir_exists(case_dir)
+ cp(sys_data_dir, case_dir, force = true)
+
+ return
+end
+
+"""
+This function creates the results directory for the simulated case.
+"""
+function make_results_dir(case::CaseDefinition)
+
+ case_name = get_name(case)
+
+ results_dir = joinpath(".", "Results", case_name)
+ dir_exists(results_dir)
+
+ return results_dir
+end
+
+"""
+This function returns the AgentSimulation struct which contains all the required data for running the simulation.
+"""
+function create_agent_simulation(case::CaseDefinition)
+ make_case_data_dir(case)
+ results_dir = make_results_dir(case)
+ simulation_data = gather_data(case)
+ simulation = AgentSimulation(case,
+ results_dir,
+ 1,
+ get_system_UC(simulation_data),
+ get_system_ED(simulation_data),
+ get_zones(simulation_data),
+ get_lines(simulation_data),
+ get_rep_days(simulation_data),
+ get_hour_weight(simulation_data),
+ get_peak_load(simulation_data),
+ get_markets(simulation_data),
+ get_carbon_tax(simulation_data),
+ get_rec_requirement(simulation_data),
+ get_investors(simulation_data),
+ get_derating_data(simulation_data),
+ get_annual_growth(simulation_data),
+ get_resource_adequacy(simulation_data))
+
+ return simulation
+end
diff --git a/src/struct_creators/simulation_structs/investor_creator.jl b/src/struct_creators/simulation_structs/investor_creator.jl
index 72dc38b..6d3651d 100644
--- a/src/struct_creators/simulation_structs/investor_creator.jl
+++ b/src/struct_creators/simulation_structs/investor_creator.jl
@@ -1,192 +1,212 @@
-"""
-This function returns a vector of investors in the simulation.
-"""
-function create_investors(simulation_data::AgentSimulationData)
- horizon = get_total_horizon(get_case(simulation_data))
- simulation_data_dir = get_data_dir(get_case(simulation_data))
- dir_name = joinpath(get_data_dir(get_case(simulation_data)), "investors")
- investor_names = readdir(dir_name)
- investors = Vector{Investor}(undef, length(investor_names))
- for i = 1:length(investor_names)
- investor_dir = joinpath(dir_name, "$(investor_names[i])")
-
- rep_hour_weight = get_rep_hour_weight(simulation_data)
-
- # Read Investor Characteristics
- characteristics = read_data(joinpath(investor_dir, "characteristics.csv"))
-
- forecast_type = get_forecast_type(get_case(simulation_data))
-
- if forecast_type == "perfect"
- parameter_values = [get_annual_growth(simulation_data) for i in 1:horizon]
- scenario_data = [Scenario("perfect", 1.0, nothing, parameter_values)]
- forecast = Perfect(scenario_data)
-
- elseif forecast_type == "imperfect"
-
- # Read belief data file
- if get_info_symmetry(get_case(simulation_data))
- belief_filename = joinpath(simulation_data_dir, "markets_data", "symmetric_belief.csv")
- else
- belief_filename = joinpath(investor_dir, "markets_data", "investor_belief.csv")
- end
-
- @assert isfile(belief_filename)
-
- investor_belief_data = read_data(belief_filename)
-
- param_names= investor_belief_data.parameters
-
- @assert in("initial_estimate", names(investor_belief_data))
- initial_estimate = AxisArrays.AxisArray(investor_belief_data.initial_estimate, param_names) # initial state initial_estimate
-
- # Create Kalman Filter for Belief Update
- if get_belief_update(get_case(simulation_data))
- @assert in("initial_error_cov", names(investor_belief_data))
- @assert in("process_cov", names(investor_belief_data))
- @assert in("measurement_cov", names(investor_belief_data))
-
- initial_error_covariance = AxisArrays.AxisArray(collect(LinearAlgebra.Diagonal(investor_belief_data.initial_error_cov)), param_names, param_names) # initial error covariance
-
- Q = AxisArrays.AxisArray(investor_belief_data.process_cov, param_names) # process covariance
- R = AxisArrays.AxisArray(investor_belief_data.measurement_cov, param_names) # measurement covariance
-
- belief = InvestorBelief(Q, R)
- kf = KalmanFilter(belief, initial_estimate, initial_error_covariance)
-
- else
- kf = nothing
- end
-
- # Populate scenario data
- scenario_data = Scenario[]
- # If user has provided parameter values for each scenario
-
- if get_uncertainty(get_case(simulation_data))
- # Assert that user has provided parameter multiplier values for each scenario
- if get_info_symmetry(get_case(simulation_data))
- scenario_file_name = joinpath(simulation_data_dir, "markets_data", "symmetric_scenario_multiplier_data.csv")
- else
- scenario_file_name = joinpath(investor_dir, "markets_data", "scenario_multiplier_data.csv")
- end
-
- @assert isfile(scenario_file_name)
- scenario_df = read_data(scenario_file_name)
- num_scenarios = DataFrames.nrow(scenario_df)
- for s in 1:num_scenarios
- name = scenario_df[s, :scenario]
- probability = scenario_df[s, :probability]
- parameter_multipliers = Dict{String, Float64}()
- parameter_values = [AxisArrays.AxisArray(zeros(length(param_names), horizon), param_names, collect(1:horizon)) for i in 1:horizon]
- for param in param_names
- if in(param, names(scenario_df))
- parameter_multipliers[param] = scenario_df[s, Symbol(param)]
- for i in 1:horizon
- parameter_values[1][param, i] = parameter_multipliers[param] * initial_estimate[param]
- end
- else
- parameter_multipliers[param] = 1.0
- for i in 1:horizon
- parameter_values[1][param, i] = initial_estimate[param]
- end
- end
- end
- push!(scenario_data, Scenario(name, probability, parameter_multipliers, parameter_values))
- end
- else
- name = "scenario_1"
- probability = 1.0
- parameter_multipliers = Dict(param => 1.0 for param in param_names)
- parameter_values = [AxisArrays.AxisArray(zeros(length(param_names), horizon), param_names, collect(1:horizon)) for i in 1:horizon]
- for param in param_names
- for i in 1:horizon
- parameter_values[1][param, i] = initial_estimate[param]
- end
- end
-
- push!(scenario_data, Scenario(name, probability, parameter_multipliers, parameter_values))
- end
-
- forecast = Imperfect(kf, scenario_data)
-
- end
- #Empty vector of projects.
- projects = Project{<:BuildPhase}[]
-
- projectdata_existing = extract_projectdata(investor_dir, "projectexisting.csv")
- projectdata_options = extract_projectdata(investor_dir, "projectoptions.csv")
-
- sys_UC = get_system_UC(simulation_data)
-
- #Append existing and option projects.
- append!(projects, create_project_existing(projectdata_existing,
- simulation_data,
- sys_UC,
- investor_names[i],
- investor_dir,
- get_name.(scenario_data)))
-
- append!(projects, create_project_options(projectdata_options,
- simulation_data,
- investor_names[i],
- investor_dir,
- get_name.(scenario_data)))
-
- add_investor_project_availability!(simulation_data_dir, projects)
-
- #Names of the markets in which the investor is participating.
- markets = Symbol[]
- simulation_markets = get_markets(simulation_data)
-
- for m in keys(simulation_markets)
- if simulation_markets[m]
- push!(markets, m)
- end
- end
-
- #Carbon Tax Data
- simulation_years = get_simulation_years(get_case(simulation_data))
- start_year = get_start_year(get_case(simulation_data))
-
- carbon_tax = zeros(simulation_years)
-
- if in(:CarbonTax, markets)
- carbon_tax_data = read_data(joinpath(investor_dir, "markets_data", "CarbonTax.csv"))
- for y in 1:simulation_years
- carbon_tax[y] = carbon_tax_data[findfirst(x -> x == start_year + y - 1, carbon_tax_data[:, "Year"]), "\$/ton"]
- end
- end
-
- # Empty market prices struct
- market_prices = MarketPrices()
-
- capital_cost_multiplier = characteristics.capital_cost_multiplier[1]
- max_annual_projects = characteristics.max_annual_projects[1]
-
- # Risk Preference
- if get_risk_aversion(get_case(simulation_data))
- risk_preference_type = lowercase(characteristics.risk_preference[1])
- if risk_preference_type == "neutral"
- risk_preference = RiskNeutral()
- elseif risk_preference_type == "averse"
- risk_preference = RiskAverse(characteristics.uf_constant[1], characteristics.uf_multiplier[1], characteristics.uf_risk_coefficient[1])
- end
- else
- risk_preference = RiskNeutral()
- end
-
- investors[i] = Investor(investor_names[i],
- investor_dir,
- projects,
- markets,
- carbon_tax,
- market_prices,
- rep_hour_weight,
- forecast,
- capital_cost_multiplier,
- max_annual_projects,
- risk_preference)
- end
-
- return investors
-end
+"""
+This function returns a vector of investors in the simulation.
+"""
+function create_investors(simulation_data::AgentSimulationData)
+ horizon = get_total_horizon(get_case(simulation_data))
+ simulation_data_dir = get_data_dir(get_case(simulation_data))
+ dir_name = joinpath(get_data_dir(get_case(simulation_data)), "investors")
+ investor_names = readdir(dir_name)
+ investors = Vector{Investor}(undef, length(investor_names))
+ for i = 1:length(investor_names)
+ investor_dir = joinpath(dir_name, "$(investor_names[i])")
+
+ rep_hour_weight = get_rep_hour_weight(simulation_data)
+
+ # Read Investor Characteristics
+ characteristics = read_data(joinpath(investor_dir, "characteristics.csv"))
+
+ forecast_type = get_forecast_type(get_case(simulation_data))
+
+ if forecast_type == "perfect"
+ parameter_values = [get_annual_growth(simulation_data) for i in 1:horizon]
+ scenario_data = [Scenario("perfect", 1.0, nothing, parameter_values)]
+ forecast = Perfect(scenario_data)
+
+ elseif forecast_type == "imperfect"
+
+ # Read belief data file
+ if get_info_symmetry(get_case(simulation_data))
+ belief_filename = joinpath(simulation_data_dir, "markets_data", "symmetric_belief.csv")
+ else
+ belief_filename = joinpath(investor_dir, "markets_data", "investor_belief.csv")
+ end
+
+ @assert isfile(belief_filename)
+
+ investor_belief_data = read_data(belief_filename)
+
+ param_names= investor_belief_data.parameters
+
+ @assert in("initial_estimate", names(investor_belief_data))
+ initial_estimate = AxisArrays.AxisArray(investor_belief_data.initial_estimate, param_names) # initial state initial_estimate
+
+ # Create Kalman Filter for Belief Update
+ if get_belief_update(get_case(simulation_data))
+ @assert in("initial_error_cov", names(investor_belief_data))
+ @assert in("process_cov", names(investor_belief_data))
+ @assert in("measurement_cov", names(investor_belief_data))
+
+ initial_error_covariance = AxisArrays.AxisArray(collect(LinearAlgebra.Diagonal(investor_belief_data.initial_error_cov)), param_names, param_names) # initial error covariance
+
+ Q = AxisArrays.AxisArray(investor_belief_data.process_cov, param_names) # process covariance
+ R = AxisArrays.AxisArray(investor_belief_data.measurement_cov, param_names) # measurement covariance
+
+ belief = InvestorBelief(Q, R)
+ kf = KalmanFilter(belief, initial_estimate, initial_error_covariance)
+
+ else
+ kf = nothing
+ end
+
+ # Populate scenario data
+ scenario_data = Scenario[]
+ # If user has provided parameter values for each scenario
+
+ if get_uncertainty(get_case(simulation_data))
+ # Assert that user has provided parameter multiplier values for each scenario
+ if get_info_symmetry(get_case(simulation_data))
+ scenario_file_name = joinpath(simulation_data_dir, "markets_data", "symmetric_scenario_multiplier_data.csv")
+ else
+ scenario_file_name = joinpath(investor_dir, "markets_data", "scenario_multiplier_data.csv")
+ end
+
+ @assert isfile(scenario_file_name)
+ scenario_df = read_data(scenario_file_name)
+ num_scenarios = DataFrames.nrow(scenario_df)
+ for s in 1:num_scenarios
+ name = scenario_df[s, :scenario]
+ probability = scenario_df[s, :probability]
+ parameter_multipliers = Dict{String, Float64}()
+ parameter_values = [AxisArrays.AxisArray(zeros(length(param_names), horizon), param_names, collect(1:horizon)) for i in 1:horizon]
+ for param in param_names
+ if in(param, names(scenario_df))
+ parameter_multipliers[param] = scenario_df[s, Symbol(param)]
+ for i in 1:horizon
+ parameter_values[1][param, i] = parameter_multipliers[param] * initial_estimate[param]
+ end
+ else
+ parameter_multipliers[param] = 1.0
+ for i in 1:horizon
+ parameter_values[1][param, i] = initial_estimate[param]
+ end
+ end
+ end
+ push!(scenario_data, Scenario(name, probability, parameter_multipliers, parameter_values))
+ end
+ else
+ name = "scenario_1"
+ probability = 1.0
+ parameter_multipliers = Dict(param => 1.0 for param in param_names)
+ parameter_values = [AxisArrays.AxisArray(zeros(length(param_names), horizon), param_names, collect(1:horizon)) for i in 1:horizon]
+ for param in param_names
+ for i in 1:horizon
+ parameter_values[1][param, i] = initial_estimate[param]
+ end
+ end
+
+ push!(scenario_data, Scenario(name, probability, parameter_multipliers, parameter_values))
+ end
+
+ forecast = Imperfect(kf, scenario_data)
+
+ end
+ #Empty vector of projects.
+ projects = Project{<:BuildPhase}[]
+
+ projectdata_existing = extract_projectdata(investor_dir, "projectexisting.csv")
+ projectdata_options = extract_projectdata(investor_dir, "projectoptions.csv")
+
+ sys_UC = get_system_UC(simulation_data)
+
+ #Append existing and option projects.
+ append!(projects, create_project_existing(projectdata_existing,
+ simulation_data,
+ sys_UC,
+ investor_names[i],
+ investor_dir,
+ get_name.(scenario_data)))
+
+ append!(projects, create_project_options(projectdata_options,
+ simulation_data,
+ investor_names[i],
+ investor_dir,
+ get_name.(scenario_data)))
+
+ add_investor_project_availability!(simulation_data_dir, projects, sys_UC)
+
+ option_leaftypes = leaftypes(Project{Option})
+
+ option_projects = filter(project -> in(typeof(project), option_leaftypes), projects)
+
+ portfolio_preference_multipliers = Dict{Tuple{String, String}, Vector{Float64}}()
+
+ for project in option_projects
+ project_tech_specs = get_tech(project)
+ tech = get_type(project_tech_specs)
+ zone = get_zone(project_tech_specs)
+ portfolio_preference_multipliers[(tech, zone)] = get_project_preference_multiplier(project)
+ end
+
+ preference_multiplier_range = (min = characteristics[1, "min_pref_multiplier"], max = characteristics[1, "max_pref_multiplier"])
+
+ #Names of the markets in which the investor is participating.
+ markets = Symbol[]
+ simulation_markets = get_markets(simulation_data)
+
+ for m in keys(simulation_markets)
+ if simulation_markets[m]
+ push!(markets, m)
+ end
+ end
+
+ #Carbon Tax Data
+ simulation_years = get_total_horizon(get_case(simulation_data))
+ start_year = get_start_year(get_case(simulation_data))
+
+ carbon_tax = zeros(simulation_years)
+
+ if in(:CarbonTax, markets)
+ carbon_tax_data = read_data(joinpath(investor_dir, "markets_data", "CarbonTax.csv"))
+ for y in 1:simulation_years
+ carbon_tax[y] = carbon_tax_data[findfirst(x -> x == start_year + y - 1, carbon_tax_data[:, "Year"]), "\$/ton"]
+ end
+ end
+
+ # Empty market prices struct
+ market_prices = MarketPrices()
+
+ capital_cost_multiplier = characteristics.capital_cost_multiplier[1]
+ max_annual_projects = characteristics.max_annual_projects[1]
+
+ # Risk Preference
+ if get_risk_aversion(get_case(simulation_data))
+ risk_preference_type = lowercase(characteristics.risk_preference[1])
+ if risk_preference_type == "neutral"
+ risk_preference = RiskNeutral()
+ elseif risk_preference_type == "averse"
+ risk_preference = RiskAverse(characteristics.uf_constant[1], characteristics.uf_multiplier[1], characteristics.uf_risk_coefficient[1])
+ end
+ else
+ risk_preference = RiskNeutral()
+ end
+
+ retirement_lookback = characteristics.retire_lookback[1]
+
+ investors[i] = Investor(investor_names[i],
+ investor_dir,
+ projects,
+ markets,
+ carbon_tax,
+ market_prices,
+ rep_hour_weight,
+ forecast,
+ capital_cost_multiplier,
+ preference_multiplier_range,
+ portfolio_preference_multipliers,
+ max_annual_projects,
+ risk_preference,
+ retirement_lookback)
+ end
+
+ return investors
+end
diff --git a/src/struct_creators/simulation_structs/product_creator.jl b/src/struct_creators/simulation_structs/product_creator.jl
index 04b8e9c..2105650 100644
--- a/src/struct_creators/simulation_structs/product_creator.jl
+++ b/src/struct_creators/simulation_structs/product_creator.jl
@@ -1,61 +1,85 @@
-"""
-This function returns a vector of the products provided by the project
-based on the data provided in the input files.
-"""
-function create_products(simulation_data::AgentSimulationData,
- projectdata::DataFrames.DataFrameRow)
- products = Product[]
-
- markets = get_markets(simulation_data)
-
- simulation_horizon = get_simulation_years(get_case(simulation_data))
-
- # Energy market product
- variable_cost = projectdata["Fuel Price \$/MMBTU"] * projectdata["HR_avg_0"] / 1000
- push!(products, Energy(:Energy, Dict{String, Array{Float64, 2}}(), variable_cost))
-
- reserve_definition = read_data(joinpath(get_data_dir(get_case(simulation_data)), "markets_data", "reserve_products.csv"))
-
- reserve_products = split(reserve_definition[1, "all_products"], "; ")
-
- for product in reserve_products
- product_data = read_data(joinpath(get_data_dir(get_case(simulation_data)), "markets_data", "$(product).csv"))
- eligible_zones = ["zone_$(n)" for n in split(product_data[1, "eligible_zones"], ";")]
-
- if markets[Symbol(product)] && (projectdata["Zone"] in eligible_zones) && occursin(projectdata["Category"], product_data[1, "eligible categories"])
- time_scale = product_data[1, "timescale (min)"]
- direction = product_data[1, "direction"]
- if lowercase(direction) == "up"
- push!(products, OperatingReserve{ReserveUpEMIS}(Symbol(product),
- projectdata["Ramp Rate pu/Hr"] * time_scale / 60,
- 0.0))
- elseif lowercase(direction) == "down"
- push!(products, OperatingReserve{ReserveDownEMIS}(Symbol(product),
- projectdata["Ramp Rate pu/Hr"] * time_scale / 60,
- 0.0))
- end
- end
- end
-
- if markets[:Capacity] && projectdata["Capacity Eligible"]
- push!(products, Capacity(:Capacity,
- 0.0,
- Dict{String, Array{Float64, 1}}(),
- 0.0))
- end
-
- if markets[:REC] && projectdata["REC Eligible"]
- push!(products, REC(:REC,
- 0.0,
- 0.0))
- end
-
- push!(products, CarbonTax(:CarbonTax,
- projectdata["CO2_Emissions ton/MMBTU"],
- projectdata["HR_avg_0"] / 1000,
- projectdata["Fuel Price \$/MMBTU"],
- zeros(length(simulation_horizon))))
-
- return products
-end
-
+"""
+This function returns a vector of the products provided by the project
+based on the data provided in the input files.
+"""
+function create_products(simulation_data::AgentSimulationData,
+ projectdata::DataFrames.DataFrameRow)
+ products = Product[]
+
+ markets = get_markets(simulation_data)
+
+ reserve_penalty = get_reserve_penalty(get_case(simulation_data))
+ mopr = get_mopr(get_case(simulation_data))
+ bat_cap = get_battery_cap_mkt(get_case(simulation_data))
+
+ simulation_horizon = get_simulation_years(get_case(simulation_data))
+
+ # Energy market product
+ variable_cost = projectdata["Fuel Price \$/MMBTU"] * projectdata["HR_avg_0"] / 1000
+ push!(products, Energy(:Energy, Dict{String, Array{Float64, 2}}(), variable_cost, 0.0))
+
+ reserve_definition = read_data(joinpath(get_data_dir(get_case(simulation_data)), "markets_data", "reserve_products.csv"))
+
+ reserve_products = split(reserve_definition[1, "all_products"], "; ")
+
+ for product in reserve_products
+ product_data = read_data(joinpath(get_data_dir(get_case(simulation_data)), "markets_data", "$(reserve_penalty)_reserve_penalty", "$(product).csv"))
+ eligible_zones = ["zone_$(n)" for n in split(product_data[1, "eligible_zones"], ";")]
+
+ if markets[Symbol(product)] && (projectdata["Zone"] in eligible_zones) && occursin(projectdata["Category"], product_data[1, "eligible categories"])
+ time_scale = product_data[1, "timescale (min)"]
+ direction = product_data[1, "direction"]
+ if lowercase(direction) == "up"
+ push!(products, OperatingReserve{ReserveUpEMIS}(Symbol(product),
+ projectdata["Ramp Rate pu/Hr"] * time_scale / 60,
+ 0.0))
+ elseif lowercase(direction) == "down"
+ push!(products, OperatingReserve{ReserveDownEMIS}(Symbol(product),
+ projectdata["Ramp Rate pu/Hr"] * time_scale / 60,
+ 0.0))
+ end
+ end
+ end
+
+ capacity_eligible = projectdata["Capacity Eligible"]
+ if mopr
+ if projectdata["Category"] == "Wind" || projectdata["Category"] == "Solar PV"
+ capacity_eligible = false
+ end
+ end
+
+ if !(bat_cap)
+ if projectdata["Category"] == "Battery"
+ capacity_eligible = false
+ end
+ end
+
+ if markets[:Capacity] && capacity_eligible
+ push!(products, Capacity(:Capacity,
+ 0.0,
+ Dict{String, Array{Float64, 1}}(),
+ 0.0))
+ end
+
+ if markets[:REC] && projectdata["REC Eligible"]
+ push!(products, REC(:REC,
+ 0.0,
+ ones(simulation_horizon),
+ 0.0))
+ end
+
+ push!(products, CarbonTax(:CarbonTax,
+ projectdata["CO2_Emissions ton/MMBTU"],
+ projectdata["HR_avg_0"] / 1000,
+ projectdata["Fuel Price \$/MMBTU"],
+ zeros(simulation_horizon)))
+
+ if markets[:Inertia]
+ push!(products, Inertia(:Inertia,
+ projectdata["Synchronous_Inertia"],
+ projectdata["Inertia MJ/MW"],
+ 0.0))
+ end
+ return products
+end
+
diff --git a/src/struct_creators/simulation_structs/project_creator.jl b/src/struct_creators/simulation_structs/project_creator.jl
index b8c8f4e..7d72b72 100644
--- a/src/struct_creators/simulation_structs/project_creator.jl
+++ b/src/struct_creators/simulation_structs/project_creator.jl
@@ -1,792 +1,834 @@
-# This file contains functions which help creating a Project.
-
-"""
-This function returns a dataframe containing project data if the data file exists,
-otherwise it returns nothing.
-"""
-function extract_projectdata(inv_dir::String,
- file_name::String)
-
- file_path = joinpath(inv_dir, file_name)
- if isfile(file_path)
- projectdata = read_data(file_path)
- return projectdata
- else
- return nothing
- end
-
-end
-
-
-"""
-This function returns the queue cost data for a project
-based on its size.
-"""
-function create_project_queuecost(size::Float64,
- queue_cost_data::DataFrames.DataFrame,
- lag_bool::Bool)
- if lag_bool
- D1_row = findfirst(x -> x == "D1", queue_cost_data[:, "Ref"])
- D2_row = findfirst(x -> x == "D2", queue_cost_data[:, "Ref"])
- M2_row = findfirst(x -> x == "M2 (per MW)", queue_cost_data[:, "Ref"])
-
- if size < 6
- D1 = queue_cost_data[D1_row, "less than 6 MW"]
- D2 = queue_cost_data[D2_row, "less than 6 MW"]
- M2 = queue_cost_data[M2_row, "less than 6 MW"] * size
- elseif 6 <= size <= 20
- D1 = queue_cost_data[D1_row, "6 to 20 MW"]
- D2 = queue_cost_data[D2_row, "6 to 20 MW"]
- M2 = queue_cost_data[M2_row, "6 to 20 MW"] * size
- elseif 20 < size <= 50
- D1 = queue_cost_data[D1_row, "20 to 50 MW"]
- D2 = queue_cost_data[D2_row, "20 to 50 MW"]
- M2 = queue_cost_data[M2_row, "20 to 50 MW"] * size
- elseif 50 < size <= 100
- D1 = queue_cost_data[D1_row, "50 to 100 MW"]
- D2 = queue_cost_data[D2_row, "50 to 100 MW"]
- M2 = queue_cost_data[M2_row, "50 to 100 MW"] * size
- elseif 100 < size <= 200
- D1 = queue_cost_data[D1_row, "100 to 200 MW"]
- D2 = queue_cost_data[D2_row, "100 to 200 MW"]
- M2 = queue_cost_data[M2_row, "100 to 200 MW"] * size
- elseif 200 < size <= 500
- D1 = queue_cost_data[D1_row, "200 to 500 MW"]
- D2 = queue_cost_data[D2_row, "200 to 500 MW"]
- M2 = queue_cost_data[M2_row, "200 to 500 MW"] * size
- elseif 500 < size < 1000
- D1 = queue_cost_data[D1_row, "500 to 1000 MW"]
- D2 = queue_cost_data[D2_row, "500 to 1000 MW"]
- M2 = queue_cost_data[M2_row, "500 to 1000 MW"] * size
- elseif size >= 1000
- D1 = queue_cost_data[D1_row, "greater than 1000 MW"]
- D2 = queue_cost_data[D2_row, "greater than 1000 MW"]
- M2 = queue_cost_data[M2_row, "greater than 1000 MW"] * size
- end
- else
- D1 = 0; D2 = 0; M2 = 0
- end
- queue_cost = [D1 + D2, M2]
- return queue_cost
-end
-
-"""
-This function writes the project's hourly availability data
-to the "DAY_AHEAD_availability.csv" file stored in
-simulation_dir/timeseries_data_files/Availability
-"""
-function add_investor_project_availability!(simulation_dir::String,
- projects::Vector{Project})
- system_availability_data = DataFrames.DataFrame(CSV.File(joinpath(simulation_dir, "timeseries_data_files", "Availability", "DAY_AHEAD_availability.csv")))
-
- for project in projects
- project_name = get_name(project)
- tech = get_tech(project)
- type_zone_id = "$(get_type(tech))_$(get_zone(tech))"
-
- if (!in(project_name, names(system_availability_data)) && !in(type_zone_id, names(system_availability_data)))
- if typeof(project) == ThermalGenEMIS{Existing} || typeof(project) == BatteryEMIS{Existing}
- system_availability_data[:, project_name] = ones(DataFrames.nrow(system_availability_data))
- elseif typeof(project) == RenewableGenEMIS{Existing}
- error("Timeseries for existing renewable generator not provided")
- elseif typeof(project) == ThermalGenEMIS{Option} || typeof(project) == BatteryEMIS{Option}
- system_availability_data[:, type_zone_id] = ones(DataFrames.nrow(system_availability_data))
- elseif typeof(project) == RenewableGenEMIS{Option}
- availability = zeros(DataFrames.nrow(system_availability_data))
- bus = get_bus(tech)
- if get_type(tech) == "WT"
- gens_in_zone = filter(g -> (first("$(bus)", 1) == first(g, 1)) && (occursin("wind", lowercase(g)) || occursin("wt", lowercase(g))),
- names(system_availability_data))
- elseif get_type(tech) == "PVe"
- gens_in_zone = filter(g -> (first("$(bus)", 1)== first(g, 1)) && occursin("pv", lowercase(g)),
- names(system_availability_data))
- elseif get_type(tech) == "HY"
- gens_in_zone = filter(g -> (first("$(bus)", 1) == first(g, 1)) && occursin("hy", lowercase(g)),
- names(system_availability_data))
- end
-
- if length(gens_in_zone) < 1
- error("Similar renewable generator in zone not found")
- else
- for g in gens_in_zone
- availability += system_availability_data[:, g]
- end
- system_availability_data[:, type_zone_id] = availability / length(gens_in_zone)
- end
- end
- end
- end
-
- write_data(joinpath(simulation_dir, "timeseries_data_files", "Availability"), "DAY_AHEAD_availability.csv", system_availability_data)
-
- return
-end
-
-
-
-"""
-This function creates a piece-wise linear operation cost for the project according to the structure used in PSY.
-This allows smooth conversion of local Project structs to PSY Device structs.
-Returns PSY.TwoPartCost or PSY.ThreePartCost based on the type of the Project.
-"""
-function create_operation_cost(projectdata::DataFrames.DataFrameRow, size::Float64, base_power::Float64, sys_base_power::Float64, products::Vector{Product}, carbon_tax::Vector{Float64})
-
- output_point_fields = String[]
- heat_rate_fields = String[]
-
- fields = names(projectdata)
-
- for field in fields
- if occursin("Output_pct_", field)
- push!(output_point_fields, field)
- elseif occursin("HR_", field)
- push!(heat_rate_fields, field)
- end
- end
-
- @assert length(output_point_fields) > 0
- cost_colnames = zip(heat_rate_fields, output_point_fields)
-
- fuel_cost = projectdata["Fuel Price \$/MMBTU"] / 1000.0
- var_cost = [(projectdata[hr], projectdata[mw]) for (hr, mw) in cost_colnames]
-
- var_cost = unique([
- (tryparse(Float64, string(c[1])), tryparse(Float64, string(c[2])))
- for c in var_cost if !in("NA", c)
- ])
-
- if length(var_cost) > 1
- var_cost[2:end] = [
- (
- var_cost[i][1] *
- (var_cost[i][2] - var_cost[i - 1][2]) *
- fuel_cost,
- var_cost[i][2] / base_power,
- ) .* size for i in 2:length(var_cost)
- ]
- var_cost[1] =
- (var_cost[1][1] * var_cost[1][2] * fuel_cost, var_cost[1][2] / base_power) .* size
-
- fixed = max(
- 0.0,
- var_cost[1][1] -
- (var_cost[2][1] / (var_cost[2][2] - var_cost[1][2]) * var_cost[1][2]),
- )
- var_cost[1] = (var_cost[1][1] - fixed, var_cost[1][2] * size / sys_base_power)
- for i in 2:length(var_cost)
- var_cost[i] = (var_cost[i - 1][1] + var_cost[i][1], var_cost[i][2] * size / sys_base_power)
- end
- elseif length(var_cost) == 1
- # if there is only one point, use it to determine the constant $/MW cost
- var_cost = var_cost[1][1] * var_cost[1][2] * fuel_cost * base_power
- fixed = 0.0
- else
- var_cost = 0.0
- fixed = 0.0
- end
-
- start_up_cost = fuel_cost * projectdata["Start Heat Cold MBTU"] * 1000.0
- shut_down_cost = 0.0
-
- if occursin("CC", projectdata["Unit Type"]) || occursin("CT", projectdata["Unit Type"]) || occursin("ST", projectdata["Unit Type"]) || occursin("NUCLEAR", projectdata["Unit Type"])
- operation_cost = PSY.ThreePartCost(var_cost, fixed, start_up_cost, shut_down_cost)
- else
- operation_cost = PSY.TwoPartCost(var_cost, fixed)
- end
-
- return operation_cost
-end
-
-"""
-This function populates project investment parameters and creates Finance struct.
-"""
-function create_investment_data(size::Float64,
- projectdata::DataFrames.DataFrameRow,
- investor_dir::String,
- simulation_data::AgentSimulationData,
- products::Vector{<: Product},
- investor_name::String,
- scenario_names::Vector{String},
- lag_bool::Bool
- )
- start_year = get_start_year(get_case(simulation_data))
- max_horizon = get_total_horizon(get_case(simulation_data))
-
- decision_year = 1
- queue_cost_data = get_queue_cost_data(simulation_data)
- queue_cost = create_project_queuecost(size, queue_cost_data, lag_bool)
- queue_time = length(queue_cost)
- construction_year = decision_year + (queue_time + projectdata["Lagtime"]) * lag_bool
-
- capex_years = projectdata["Capex Years"]
-
- capex_data = read_data(joinpath(investor_dir, "project_capex.csv"))
-
- category = projectdata["Category"]
- capex_row = findfirst(x-> x == category, capex_data[:, "Category"])
-
- if lag_bool
- online_year = start_year + decision_year + queue_time - 1
- age = 0
- remaining_lifetime = projectdata["Lifetime"]
- project_capex = [capex_data[capex_row, "$(start_year + y - 1)"] for y in 1:max_horizon]
- else
- online_year = projectdata["Online Year"]
- age = start_year - online_year
- remaining_lifetime = projectdata["Lifetime"] - age
- project_capex = zeros(max_horizon)
-
- for y = 1:min(max_horizon, (capex_years - age))
- project_capex[y] = capex_data[capex_row, "pre $(start_year)"]
- end
-
- end
-
- investment_cost, discount_rate = calculate_capcost_and_wacc(project_capex, category, investor_dir, online_year)
-
- investment_cost = investment_cost * size * 1000.0
-
- effective_investment_cost = investment_cost[queue_time + 1]
- fixedOM_cost = projectdata["Fixed OM Cost per MW"] * size
-
- end_life_year = (queue_time + projectdata["Lagtime"]) * lag_bool + remaining_lifetime
-
- retirement_year = (queue_time + projectdata["Lagtime"]) * lag_bool + remaining_lifetime
-
- num_profit_years = max(max_horizon, end_life_year)
-
- scenario_total_utilization = Dict{String, Array{Float64, 2}}()
-
- yearly_profit = [AxisArrays.AxisArray(zeros(length(products), num_profit_years),
- AxisArrays.Axis{:prod}(get_name.(products)),
- AxisArrays.Axis{:year}(1:1:num_profit_years))
- for y in 1:num_profit_years]
-
- scenario_profit = Dict(scenario_name => [AxisArrays.AxisArray(zeros(length(products), num_profit_years),
- AxisArrays.Axis{:prod}(get_name.(products)),
- AxisArrays.Axis{:year}(1:1:num_profit_years))
- for y in 1:num_profit_years] for scenario_name in scenario_names)
-
- realized_profit = AxisArrays.AxisArray(zeros(length(products), num_profit_years),
- AxisArrays.Axis{:prod}(get_name.(products)),
- AxisArrays.Axis{:year}(1:1:num_profit_years))
-
- scenario_npv = Dict(scenario_name => zeros(num_profit_years) for scenario_name in scenario_names)
- expected_npv = zeros(num_profit_years)
- scenario_utility = Dict(scenario_name => zeros(num_profit_years) for scenario_name in scenario_names)
- expected_utility = zeros(num_profit_years)
-
- annual_cashflow = zeros(num_profit_years)
-
- finance_data = Finance(investment_cost,
- effective_investment_cost,
- projectdata["Lagtime"],
- remaining_lifetime,
- capex_years,
- fixedOM_cost,
- queue_cost,
- scenario_total_utilization,
- scenario_profit,
- realized_profit,
- discount_rate,
- scenario_npv,
- expected_npv,
- scenario_utility,
- expected_utility,
- annual_cashflow,
- investor_name)
-
- return decision_year, construction_year, retirement_year, end_life_year, finance_data
-end
-
-"""
-This function creates the Tech struct for projects
-based only on the CSV data.
-"""
-function create_tech_type(name::String,
- projectdata::DataFrames.DataFrameRow,
- size::Float64,
- base_power::Float64,
- decision_year::Int64,
- construction_year::Int64,
- retirement_year::Int64,
- end_life_year::Int64,
- products::Vector{Product},
- finance_data::Finance,
- sys_UC::Union{PSY.System, Nothing},
- carbon_tax::Vector{Float64}
- )
-
- type = projectdata["Unit Type"]
- min_cap = projectdata["Min Gen pu"] * size
-
- active_power_limits = (min = min_cap, max = size)
- ramp_limits = (up = projectdata["Ramp Rate pu/Hr"] * size, down = projectdata["Ramp Rate pu/Hr"] * size)
- fuel_cost = projectdata["Fuel Price \$/MMBTU"]
- zone = projectdata["Zone"]
- bus = projectdata["Bus ID"]
- FOR = projectdata["FOR"]
-
- if !isnothing(sys_UC)
- sys_base_power = PSY.get_base_power(sys_UC)
- operation_cost = create_operation_cost(projectdata, size, base_power, sys_base_power, products, carbon_tax)
- up_down_time = (up = projectdata["Min Up Time Hr"], down = projectdata["Min Down Time Hr"])
- else
- operation_cost = nothing
- up_down_time = nothing
- end
-
- if type == "ST" || type == "CT" || type == "CC" || type == "NU_ST"
-
- output_point_fields = String[]
- heat_rate_fields = String[]
- fields = names(projectdata)
-
- for field in fields
- if occursin("Output_pct_", field)
- push!(output_point_fields, field)
- elseif occursin("HR_", field)
- push!(heat_rate_fields, field)
- end
- end
-
- @assert length(output_point_fields) > 0
- cost_colnames = zip(heat_rate_fields, output_point_fields)
-
- heat_rate = [(projectdata[hr], projectdata[mw]) for (hr, mw) in cost_colnames]
-
- heat_rate = unique([
- (tryparse(Float64, string(c[1])), tryparse(Float64, string(c[2])))
- for c in heat_rate if !in("NA", c)
- ])
-
- if length(heat_rate) > 1
- heat_rate[2:end] = [
- (
- heat_rate[i][1] *
- (heat_rate[i][2] - heat_rate[i - 1][2]),
- heat_rate[i][2]
- ) for i in 2:length(heat_rate)
- ]
- heat_rate[1] =
- (heat_rate[1][1] * heat_rate[1][2], heat_rate[1][2])
-
- fixed = max(
- 0.0,
- heat_rate[1][1] -
- (heat_rate[2][1] / (heat_rate[2][2] - heat_rate[1][2]) * heat_rate[1][2]),
- )
- heat_rate[1] = (heat_rate[1][1] / 1000.0, heat_rate[1][2] * size)
- for i in 2:length(heat_rate)
- heat_rate[i] = (heat_rate[i - 1][1] + heat_rate[i][1] / 1000.0, heat_rate[i][2] * size)
- end
- pushfirst!(heat_rate, (fixed / 1000.0, 0.0))
- elseif length(heat_rate) == 1
- # if there is only one point, use it to determine the constant $/MW cost
- heat_rate = (heat_rate[1][1] * heat_rate[1][2] / 1000.0, heat_rate[1][2] * size)
- fixed = 0.0
- else
- heat_rate = [(0.0, 0.0)]
- fixed = 0.0
- end
- tech = ThermalTech(type,
- projectdata["Fuel"],
- active_power_limits,
- ramp_limits,
- up_down_time,
- operation_cost,
- fuel_cost,
- heat_rate,
- bus,
- zone,
- FOR)
-
- return ThermalGenEMIS{BuildPhase}(name,
- tech,
- decision_year,
- construction_year,
- retirement_year,
- end_life_year,
- products,
- finance_data)
- elseif type == "WT" || type == "PVe"
- tech = RenewableTech(type,
- active_power_limits,
- ramp_limits,
- operation_cost,
- bus,
- zone,
- FOR)
-
- return RenewableGenEMIS{BuildPhase}(name,
- tech,
- decision_year,
- construction_year,
- retirement_year,
- end_life_year,
- products,
- finance_data)
-
- elseif type == "HY"
- tech = HydroTech(type,
- active_power_limits,
- ramp_limits,
- up_down_time,
- operation_cost,
- bus,
- zone,
- FOR)
-
- return HydroGenEMIS{BuildPhase}(name,
- tech,
- decision_year,
- construction_year,
- retirement_year,
- end_life_year,
- products,
- finance_data)
-
- elseif type == "BA"
- storage_capacity = parse(Float64, projectdata["Duration Hr"]) * size
- tech = BatteryTech(type,
- (min = 0.0, max = parse(Float64, projectdata["Input Power Rating pu"]) * size),
- (min = 0.0, max = parse(Float64, projectdata["Output Power Rating pu"]) * size),
- ramp_limits,
- (min = parse(Float64, projectdata["Min Storgae pu"]) * storage_capacity, max = storage_capacity),
- 0.0,
- (in = parse(Float64, projectdata["Round Trip Efficiency pu"]), out = 1.0),
- bus,
- zone,
- FOR)
- return BatteryEMIS{BuildPhase}(name,
- tech,
- decision_year,
- construction_year,
- retirement_year,
- end_life_year,
- products,
- finance_data)
-
- end
-
-end
-
-"""
-This function creates the Tech struct for ThermalGen projects
-based on the CSV and PSY Device data.
-"""
-function create_tech_type(name::String,
- device::T,
- projectdata::DataFrames.DataFrameRow,
- size::Float64,
- base_power::Float64,
- decision_year::Int64,
- construction_year::Int64,
- retirement_year::Int64,
- end_life_year::Int64,
- products::Vector{Product},
- finance_data::Finance
- ) where T <: PSY.ThermalGen
-
- min_cap = deepcopy(PSY.get_active_power_limits(device)[:min]) * base_power
-
- fuel_cost = projectdata["Fuel Price \$/MMBTU"]
- bus = deepcopy(PSY.get_bus(device))
- device_ramp_limits = deepcopy(PSY.get_ramp_limits(device))
-
- if isnothing(device_ramp_limits)
- ramp_limits = nothing
- else
- ramp_limits = (up = device_ramp_limits[:up] * base_power * 60, down = device_ramp_limits[:down] * base_power * 60)
- end
-
- prime_mover = string(deepcopy(PSY.get_prime_mover(device)))
- fuel = string(deepcopy(PSY.get_fuel(device)))
-
- if fuel == "NUCLEAR"
- prime_mover = "NU_ST"
- end
-
- FOR = projectdata["FOR"]
-
- output_point_fields = String[]
- heat_rate_fields = String[]
- fields = names(projectdata)
-
- for field in fields
- if occursin("Output_pct_", field)
- push!(output_point_fields, field)
- elseif occursin("HR_", field)
- push!(heat_rate_fields, field)
- end
- end
-
- @assert length(output_point_fields) > 0
- cost_colnames = zip(heat_rate_fields, output_point_fields)
-
- heat_rate = [(projectdata[hr], projectdata[mw]) for (hr, mw) in cost_colnames]
-
- heat_rate = unique([
- (tryparse(Float64, string(c[1])), tryparse(Float64, string(c[2])))
- for c in heat_rate if !in("NA", c)
- ])
- if length(heat_rate) > 1
- heat_rate[2:end] = [
- (
- heat_rate[i][1] *
- (heat_rate[i][2] - heat_rate[i - 1][2]),
- heat_rate[i][2]
- ) for i in 2:length(heat_rate)
- ]
- heat_rate[1] =
- (heat_rate[1][1] * heat_rate[1][2], heat_rate[1][2])
-
- fixed = max(
- 0.0,
- heat_rate[1][1] -
- (heat_rate[2][1] / (heat_rate[2][2] - heat_rate[1][2]) * heat_rate[1][2]),
- )
- heat_rate[1] = (heat_rate[1][1] / 1000.0, heat_rate[1][2] * size)
- for i in 2:length(heat_rate)
- heat_rate[i] = (heat_rate[i - 1][1] + heat_rate[i][1] / 1000.0, heat_rate[i][2] * size)
- end
- pushfirst!(heat_rate, (fixed / 1000.0, 0.0))
- elseif length(heat_rate) == 1
- # if there is only one point, use it to determine the constant $/MW cost
- heat_rate = (heat_rate[1][1] * heat_rate[1][2] / 1000.0, heat_rate[1][2] * size)
- fixed = 0.0
- else
- heat_rate = [(0.0, 0.0)]
- fixed = 0.0
- end
-
-
- tech = ThermalTech(prime_mover,
- fuel,
- (min = min_cap, max = size),
- ramp_limits,
- deepcopy(PSY.get_time_limits(device)),
- deepcopy(PSY.get_operation_cost(device)),
- fuel_cost,
- heat_rate,
- deepcopy(PSY.get_number(bus)),
- projectdata["Zone"],
- FOR)
-
- return ThermalGenEMIS{BuildPhase}(name,
- tech,
- decision_year,
- construction_year,
- retirement_year,
- end_life_year,
- products,
- finance_data)
-
-end
-
-"""
-This function creates the Tech struct for RenewableGen projects
-based on the CSV and PSY Device data.
-"""
-function create_tech_type(name::String,
- device::T,
- projectdata::DataFrames.DataFrameRow,
- size::Float64,
- base_power::Float64,
- decision_year::Int64,
- construction_year::Int64,
- retirement_year::Int64,
- end_life_year::Int64,
- products::Vector{Product},
- finance_data::Finance
- ) where T <: PSY.RenewableGen
-
- bus = deepcopy(PSY.get_bus(device))
-
- type = string(deepcopy(PSY.get_prime_mover(device)))
-
- FOR = projectdata["FOR"]
-
- tech = RenewableTech(type,
- (min = 0.0, max = size),
- (up = size, down = size),
- deepcopy(PSY.get_operation_cost(device)),
- deepcopy(PSY.get_number(bus)),
- projectdata["Zone"],
- FOR)
-
- return RenewableGenEMIS{BuildPhase}(name,
- tech,
- decision_year,
- construction_year,
- retirement_year,
- end_life_year,
- products,
- finance_data)
-
-end
-
-"""
-This function creates the Tech struct for HydroGen projects
-based on the CSV and PSY Device data.
-"""
-function create_tech_type(name::String,
- device::P,
- projectdata::DataFrames.DataFrameRow,
- size::Float64,
- base_power::Float64,
- decision_year::Int64,
- construction_year::Int64,
- retirement_year::Int64,
- end_life_year::Int64,
- products::Vector{Product},
- finance_data::Finance
- ) where P <: PSY.HydroGen
-
- min_cap = deepcopy(PSY.get_active_power_limits(device)[:min]) * base_power
- bus = deepcopy(PSY.get_bus(device))
-
- device_ramp_limits = deepcopy(PSY.get_ramp_limits(device))
-
- if isnothing(device_ramp_limits)
- ramp_limits = nothing
- else
- ramp_limits = (up = device_ramp_limits[:up] * base_power * 60, down = device_ramp_limits[:down] * base_power * 60)
- end
-
- FOR = projectdata["FOR"]
-
- tech = HydroTech("HY",
- (min = min_cap, max = size),
- ramp_limits,
- deepcopy(PSY.get_time_limits(device)),
- deepcopy(PSY.get_operation_cost(device)),
- deepcopy( PSY.get_number(bus)),
- projectdata["Zone"],
- FOR)
-
- return HydroGenEMIS{BuildPhase}(name,
- tech,
- decision_year,
- construction_year,
- retirement_year,
- end_life_year,
- products,
- finance_data)
-
-end
-
-"""
-This function creates the Tech struct for Battery projects
-based on the CSV and PSY Device data.
-"""
-function create_tech_type(name::String,
- device::PSY.GenericBattery,
- projectdata::DataFrames.DataFrameRow,
- size::Float64,
- base_power::Float64,
- decision_year::Int64,
- construction_year::Int64,
- retirement_year::Int64,
- end_life_year::Int64,
- products::Vector{Product},
- finance_data::Finance
- )
-
- bus = deepcopy(PSY.get_bus(device))
-
- type = string(deepcopy(PSY.get_prime_mover(device)))
- input_active_power_limits = deepcopy(PSY.get_input_active_power_limits(device))
- output_active_power_limits = deepcopy(PSY.get_output_active_power_limits(device))
- storage_capacity = deepcopy(PSY.get_state_of_charge_limits(device))
-
- FOR = projectdata["FOR"]
-
- tech = BatteryTech(type,
- (min = input_active_power_limits[:min] * base_power, down = input_active_power_limits[:max] * base_power),
- (min = output_active_power_limits[:min] * base_power, down = output_active_power_limits[:max] * base_power),
- (up = size, down = size),
- (min = storage_capacity[:min] * size * base_power, down = storage_capacity[:max] * size * base_power),
- PSY.get_initial_energy(device) * size * base_power,
- PSY.get_efficiency(device),
- PSY.get_number(bus),
- projectdata["Zone"],
- FOR)
-
- return BatteryEMIS{BuildPhase}(name,
- tech,
- decision_year,
- construction_year,
- retirement_year,
- end_life_year,
- products,
- finance_data)
-
-end
-
-"""
-This function returns a Project struct based only on project's CSV data.
-"""
-function create_project(projectdata::DataFrames.DataFrameRow,
- simulation_data::AgentSimulationData,
- investor_name::String,
- investor_dir::String,
- scenario_names::Vector{String},
- lag_bool::Bool)
-
- name = projectdata["GEN_UID"]
- size = Float64(size_in_MW(investor_dir,
- projectdata["Unit Type"],
- projectdata["Size"]))
-
- base_power = size
- carbon_tax = get_carbon_tax(simulation_data)
-
- products = create_products(simulation_data,
- projectdata)
-
- decision_year,
- construction_year,
- retirement_year,
- end_life_year,
- finance_data = create_investment_data(size, projectdata, investor_dir, simulation_data, products, investor_name, scenario_names, lag_bool)
-
- sys_UC = get_system_UC(simulation_data)
-
- project = create_tech_type(name, projectdata, size, base_power, decision_year, construction_year, retirement_year, end_life_year, products, finance_data, sys_UC, carbon_tax)
-
- return project
-end
-
-
-
-"""
-This function returns a Project struct based on CSV data and PSY data.
-"""
-function create_project(projectdata::DataFrames.DataFrameRow,
- device::T,
- simulation_data::AgentSimulationData,
- investor_name::String,
- investor_dir::String,
- scenario_names::Vector{String},
- lag_bool::Bool) where T <: Union{PSY.Generator, PSY.Storage}
-
- name = get_name(device)
-
- base_power = PSY.get_base_power(device)
-
- size = get_device_size(device) * base_power
-
- products = create_products(simulation_data,
- projectdata)
-
- decision_year,
- construction_year,
- retirement_year,
- end_life_year,
- finance_data = create_investment_data(size, projectdata, investor_dir, simulation_data, products, investor_name, scenario_names, lag_bool)
-
- project = create_tech_type(name, device, projectdata, size, base_power, decision_year, construction_year, retirement_year, end_life_year, products, finance_data)
-
- return project
-end
+# This file contains functions which help creating a Project.
+
+"""
+This function returns a dataframe containing project data if the data file exists,
+otherwise it returns nothing.
+"""
+function extract_projectdata(inv_dir::String,
+ file_name::String)
+
+ file_path = joinpath(inv_dir, file_name)
+ if isfile(file_path)
+ projectdata = read_data(file_path)
+ return projectdata
+ else
+ return nothing
+ end
+
+end
+
+
+"""
+This function returns the queue cost data for a project
+based on its size.
+"""
+function create_project_queuecost(size::Float64,
+ queue_cost_data::DataFrames.DataFrame,
+ lag_bool::Bool)
+ if lag_bool
+ D1_row = findfirst(x -> x == "D1", queue_cost_data[:, "Ref"])
+ D2_row = findfirst(x -> x == "D2", queue_cost_data[:, "Ref"])
+ M2_row = findfirst(x -> x == "M2 (per MW)", queue_cost_data[:, "Ref"])
+
+ if size < 6
+ D1 = queue_cost_data[D1_row, "less than 6 MW"]
+ D2 = queue_cost_data[D2_row, "less than 6 MW"]
+ M2 = queue_cost_data[M2_row, "less than 6 MW"] * size
+ elseif 6 <= size <= 20
+ D1 = queue_cost_data[D1_row, "6 to 20 MW"]
+ D2 = queue_cost_data[D2_row, "6 to 20 MW"]
+ M2 = queue_cost_data[M2_row, "6 to 20 MW"] * size
+ elseif 20 < size <= 50
+ D1 = queue_cost_data[D1_row, "20 to 50 MW"]
+ D2 = queue_cost_data[D2_row, "20 to 50 MW"]
+ M2 = queue_cost_data[M2_row, "20 to 50 MW"] * size
+ elseif 50 < size <= 100
+ D1 = queue_cost_data[D1_row, "50 to 100 MW"]
+ D2 = queue_cost_data[D2_row, "50 to 100 MW"]
+ M2 = queue_cost_data[M2_row, "50 to 100 MW"] * size
+ elseif 100 < size <= 200
+ D1 = queue_cost_data[D1_row, "100 to 200 MW"]
+ D2 = queue_cost_data[D2_row, "100 to 200 MW"]
+ M2 = queue_cost_data[M2_row, "100 to 200 MW"] * size
+ elseif 200 < size <= 500
+ D1 = queue_cost_data[D1_row, "200 to 500 MW"]
+ D2 = queue_cost_data[D2_row, "200 to 500 MW"]
+ M2 = queue_cost_data[M2_row, "200 to 500 MW"] * size
+ elseif 500 < size < 1000
+ D1 = queue_cost_data[D1_row, "500 to 1000 MW"]
+ D2 = queue_cost_data[D2_row, "500 to 1000 MW"]
+ M2 = queue_cost_data[M2_row, "500 to 1000 MW"] * size
+ elseif size >= 1000
+ D1 = queue_cost_data[D1_row, "greater than 1000 MW"]
+ D2 = queue_cost_data[D2_row, "greater than 1000 MW"]
+ M2 = queue_cost_data[M2_row, "greater than 1000 MW"] * size
+ end
+ else
+ D1 = 0; D2 = 0; M2 = 0
+ end
+ queue_cost = [D1 + D2, M2]
+ return queue_cost
+end
+
+"""
+This function writes the project's hourly availability data
+to the "DAY_AHEAD_availability.csv" file stored in
+simulation_dir/timeseries_data_files/Availability
+"""
+function add_investor_project_availability!(simulation_dir::String,
+ projects::Vector{Project},
+ sys_UC::Union{Nothing, PSY.System})
+ system_availability_data = DataFrames.DataFrame(CSV.File(joinpath(simulation_dir, "timeseries_data_files", "Availability", "DAY_AHEAD_availability.csv")))
+ system_availability_data_rt = DataFrames.DataFrame(CSV.File(joinpath(simulation_dir, "timeseries_data_files", "Availability", "REAL_TIME_availability.csv")))
+ gennames = names(system_availability_data)[5:length(names(system_availability_data))] #################
+
+ for project in projects
+ # println("this is $(get_name(project))")
+ project_name = get_name(project)
+ tech = get_tech(project)
+ type_zone_id = "$(get_type(tech))_$(get_zone(tech))"
+
+ if (!in(project_name, names(system_availability_data)) && !in(type_zone_id, names(system_availability_data)))
+ if typeof(project) == ThermalGenEMIS{Existing} || typeof(project) == BatteryEMIS{Existing}
+ system_availability_data[:, project_name] = ones(DataFrames.nrow(system_availability_data))
+ system_availability_data_rt[:, project_name] = ones(DataFrames.nrow(system_availability_data_rt))
+ elseif typeof(project) == RenewableGenEMIS{Existing}
+ error("Timeseries for existing renewable generator not provided")
+ elseif typeof(project) == ThermalGenEMIS{Option} || typeof(project) == BatteryEMIS{Option}
+ system_availability_data[:, type_zone_id] = ones(DataFrames.nrow(system_availability_data))
+ system_availability_data_rt[:, type_zone_id] = ones(DataFrames.nrow(system_availability_data_rt))
+ elseif typeof(project) == RenewableGenEMIS{Option}
+ availability = zeros(DataFrames.nrow(system_availability_data))
+ availability_rt = zeros(DataFrames.nrow(system_availability_data_rt))
+ bus = get_bus(tech)
+ if get_type(tech) == "WT"
+ # gens_in_zone = filter(g -> (first("$(bus)", 1) == first(g, 1)) && (occursin("wind", lowercase(g)) || occursin("wt", lowercase(g))),
+ # names(system_availability_data))
+ gens_in_zone = filter(g -> (first("$(bus)", 1) == first(string(PSY.get_number(PSY.get_bus(PSY.get_component(PSY.StaticInjection, sys_UC,g)))), 1)) &&
+ occursin("WT", string(PSY.get_prime_mover(PSY.get_component(PSY.StaticInjection, sys_UC,g)))),
+ gennames)
+ elseif get_type(tech) == "PVe"
+ # gens_in_zone = filter(g -> (first("$(bus)", 1)== first(g, 1)) && occursin("pv", lowercase(g)),
+ # names(system_availability_data))
+ gens_in_zone = filter(g -> (first("$(bus)", 1) == first(string(PSY.get_number(PSY.get_bus(PSY.get_component(PSY.StaticInjection, sys_UC,g)))), 1)) &&
+ occursin("PVe", string(PSY.get_prime_mover(PSY.get_component(PSY.StaticInjection, sys_UC,g)))),
+ gennames)
+ elseif get_type(tech) == "HY"
+ # gens_in_zone = filter(g -> (first("$(bus)", 1) == first(g, 1)) && occursin("hy", lowercase(g)),
+ # names(system_availability_data))
+ gens_in_zone = filter(g -> (first("$(bus)", 1) == first(string(PSY.get_number(PSY.get_bus(PSY.get_component(PSY.StaticInjection, sys_UC,g)))), 1)) &&
+ occursin("HY", string(PSY.get_prime_mover(PSY.get_component(PSY.StaticInjection, sys_UC,g)))),
+ gennames)
+ end
+
+ if length(gens_in_zone) < 1
+ error("Similar renewable generator in zone not found")
+ else
+ for g in gens_in_zone
+ availability += system_availability_data[:, g]
+ availability_rt += system_availability_data_rt[:, g]
+ end
+ system_availability_data[:, type_zone_id] = availability / length(gens_in_zone)
+ system_availability_data_rt[:, type_zone_id] = availability_rt / length(gens_in_zone)
+ end
+ end
+ end
+ end
+
+ write_data(joinpath(simulation_dir, "timeseries_data_files", "Availability"), "DAY_AHEAD_availability.csv", system_availability_data)
+ write_data(joinpath(simulation_dir, "timeseries_data_files", "Availability"), "REAL_TIME_availability.csv", system_availability_data_rt)
+
+ return
+end
+
+
+
+"""
+This function creates a piece-wise linear operation cost for the project according to the structure used in PSY.
+This allows smooth conversion of local Project structs to PSY Device structs.
+Returns PSY.TwoPartCost or PSY.ThreePartCost based on the type of the Project.
+"""
+function create_operation_cost(projectdata::DataFrames.DataFrameRow, size::Float64, base_power::Float64, sys_base_power::Float64, products::Vector{Product}, carbon_tax::Vector{Float64})
+
+ output_point_fields = String[]
+ heat_rate_fields = String[]
+
+ fields = names(projectdata)
+
+ for field in fields
+ if occursin("Output_pct_", field)
+ push!(output_point_fields, field)
+ elseif occursin("HR_", field)
+ push!(heat_rate_fields, field)
+ end
+ end
+
+ @assert length(output_point_fields) > 0
+ cost_colnames = zip(heat_rate_fields, output_point_fields)
+
+ fuel_cost = projectdata["Fuel Price \$/MMBTU"] / 1000.0
+ var_cost = [(projectdata[hr], projectdata[mw]) for (hr, mw) in cost_colnames]
+
+ var_cost = unique([
+ (tryparse(Float64, string(c[1])), tryparse(Float64, string(c[2])))
+ for c in var_cost if !in("NA", c)
+ ])
+
+ if length(var_cost) > 1
+ var_cost[2:end] = [
+ (
+ var_cost[i][1] *
+ (var_cost[i][2] - var_cost[i - 1][2]) *
+ fuel_cost,
+ var_cost[i][2] / base_power,
+ ) .* size for i in 2:length(var_cost)
+ ]
+ var_cost[1] =
+ (var_cost[1][1] * var_cost[1][2] * fuel_cost, var_cost[1][2] / base_power) .* size
+
+ fixed = max(
+ 0.0,
+ var_cost[1][1] -
+ (var_cost[2][1] / (var_cost[2][2] - var_cost[1][2]) * var_cost[1][2]),
+ )
+ var_cost[1] = (var_cost[1][1] - fixed, var_cost[1][2] * size)
+ for i in 2:length(var_cost)
+ var_cost[i] = (var_cost[i - 1][1] + var_cost[i][1], var_cost[i][2] * size)
+ end
+ elseif length(var_cost) == 1
+ # if there is only one point, use it to determine the constant $/MW cost
+ var_cost = var_cost[1][1] * var_cost[1][2] * fuel_cost * base_power
+ fixed = 0.0
+ else
+ var_cost = 0.0
+ fixed = 0.0
+ end
+
+ start_up_cost = fuel_cost * projectdata["Start Heat Cold MBTU"] * 1000.0
+ shut_down_cost = 0.0
+
+ if occursin("CC", projectdata["Unit Type"]) || occursin("CT", projectdata["Unit Type"]) || occursin("GT", projectdata["Unit Type"]) || occursin("ST", projectdata["Unit Type"]) || occursin("NUCLEAR", projectdata["Unit Type"])
+ operation_cost = PSY.ThreePartCost(var_cost, fixed, start_up_cost, shut_down_cost)
+ else
+ operation_cost = PSY.TwoPartCost(var_cost, fixed)
+ end
+
+ return operation_cost
+end
+
+"""
+This function populates project investment parameters and creates Finance struct.
+"""
+function create_investment_data(size::Float64,
+ projectdata::DataFrames.DataFrameRow,
+ investor_dir::String,
+ simulation_data::AgentSimulationData,
+ products::Vector{<: Product},
+ investor_name::String,
+ scenario_names::Vector{String},
+ lag_bool::Bool
+ )
+ start_year = get_start_year(get_case(simulation_data))
+ max_horizon = get_total_horizon(get_case(simulation_data))
+
+ decision_year = 1
+ queue_cost_data = get_queue_cost_data(simulation_data)
+ queue_cost = create_project_queuecost(size, queue_cost_data, lag_bool)
+ queue_time = length(queue_cost)
+ construction_year = decision_year + (queue_time + projectdata["Lagtime"]) * lag_bool
+
+ capex_years = projectdata["Capex Years"]
+
+ capex_data = read_data(joinpath(investor_dir, "project_capex.csv"))
+
+ category = String(projectdata["Category"])
+ capex_row = findfirst(x-> x == category, capex_data[:, "Category"])
+
+ if lag_bool
+ online_year = start_year + decision_year + queue_time - 1
+ age = 0
+ remaining_lifetime = projectdata["Lifetime"]
+ project_capex = [capex_data[capex_row, "$(start_year + y - 1)"] for y in 1:max_horizon]
+ preference_multiplier = projectdata["Preference Multiplier"] * ones(max_horizon)
+ else
+ online_year = projectdata["Online Year"]
+ age = start_year - online_year
+ remaining_lifetime = projectdata["Lifetime"] - age
+ project_capex = zeros(max_horizon)
+
+ for y = 1:min(max_horizon, (capex_years - age))
+ project_capex[y] = capex_data[capex_row, "pre $(start_year)"]
+ end
+
+ preference_multiplier = ones(max_horizon)
+ end
+
+ investment_cost, discount_rate = calculate_capcost_and_wacc(project_capex, category, investor_dir, online_year)
+
+ investment_cost = investment_cost * size * 1000.0
+
+ effective_investment_cost = investment_cost[queue_time + 1]
+ fixedOM_cost = projectdata["Fixed OM Cost per MW"] * size
+
+ end_life_year = (queue_time + projectdata["Lagtime"]) * lag_bool + remaining_lifetime
+
+ retirement_year = (queue_time + projectdata["Lagtime"]) * lag_bool + remaining_lifetime
+
+ num_profit_years = max(max_horizon, end_life_year)
+
+ scenario_total_utilization = Dict{String, Array{Float64, 2}}()
+
+ yearly_profit = [AxisArrays.AxisArray(zeros(length(products), num_profit_years),
+ AxisArrays.Axis{:prod}(get_name.(products)),
+ AxisArrays.Axis{:year}(1:1:num_profit_years))
+ for y in 1:num_profit_years]
+
+ scenario_profit = Dict(scenario_name => [AxisArrays.AxisArray(zeros(length(products), num_profit_years),
+ AxisArrays.Axis{:prod}(get_name.(products)),
+ AxisArrays.Axis{:year}(1:1:num_profit_years))
+ for y in 1:num_profit_years] for scenario_name in scenario_names)
+
+ realized_profit = AxisArrays.AxisArray(zeros(length(products), num_profit_years),
+ AxisArrays.Axis{:prod}(get_name.(products)),
+ AxisArrays.Axis{:year}(1:1:num_profit_years))
+
+ scenario_npv = Dict(scenario_name => zeros(num_profit_years) for scenario_name in scenario_names)
+ expected_npv = zeros(num_profit_years)
+ scenario_utility = Dict(scenario_name => zeros(num_profit_years) for scenario_name in scenario_names)
+ expected_utility = zeros(num_profit_years)
+
+ annual_cashflow = zeros(num_profit_years)
+
+ finance_data = Finance(investment_cost,
+ effective_investment_cost,
+ preference_multiplier,
+ projectdata["Lagtime"],
+ remaining_lifetime,
+ capex_years,
+ fixedOM_cost,
+ queue_cost,
+ scenario_total_utilization,
+ scenario_profit,
+ realized_profit,
+ discount_rate,
+ scenario_npv,
+ expected_npv,
+ scenario_utility,
+ expected_utility,
+ annual_cashflow,
+ investor_name)
+
+ return decision_year, construction_year, retirement_year, end_life_year, finance_data
+end
+
+"""
+This function creates the Tech struct for projects
+based only on the CSV data.
+"""
+function create_tech_type(name::String,
+ projectdata::DataFrames.DataFrameRow,
+ size::Float64,
+ base_power::Float64,
+ decision_year::Int64,
+ construction_year::Int64,
+ retirement_year::Int64,
+ end_life_year::Int64,
+ products::Vector{Product},
+ finance_data::Finance,
+ sys_UC::Union{PSY.System, Nothing},
+ carbon_tax::Vector{Float64}
+ )
+
+ type = projectdata["Unit Type"]
+ min_cap = projectdata["Min Gen pu"] * size
+
+ active_power_limits = (min = min_cap, max = size)
+ ramp_limits = (up = projectdata["Ramp Rate pu/Hr"] * size, down = projectdata["Ramp Rate pu/Hr"] * size)
+ fuel_cost = projectdata["Fuel Price \$/MMBTU"]
+ zone = projectdata["Zone"]
+ bus = projectdata["Bus ID"]
+ FOR = projectdata["FOR"]
+ MTTR = projectdata["MTTR Hr"]
+
+ if !isnothing(sys_UC)
+ sys_base_power = PSY.get_base_power(sys_UC)
+ operation_cost = create_operation_cost(projectdata, size, base_power, sys_base_power, products, carbon_tax)
+ up_down_time = (up = projectdata["Min Up Time Hr"], down = projectdata["Min Down Time Hr"])
+ else
+ operation_cost = nothing
+ up_down_time = nothing
+ end
+
+ if type == "ST" || type == "CT" || type == "CC" || type == "NU_ST" || type == "GT" || type == "RE_CT"
+
+ output_point_fields = String[]
+ heat_rate_fields = String[]
+ fields = names(projectdata)
+
+ for field in fields
+ if occursin("Output_pct_", field)
+ push!(output_point_fields, field)
+ elseif occursin("HR_", field)
+ push!(heat_rate_fields, field)
+ end
+ end
+
+ @assert length(output_point_fields) > 0
+ cost_colnames = zip(heat_rate_fields, output_point_fields)
+
+ heat_rate = [(projectdata[hr], projectdata[mw]) for (hr, mw) in cost_colnames]
+
+ heat_rate = unique([
+ (tryparse(Float64, string(c[1])), tryparse(Float64, string(c[2])))
+ for c in heat_rate if !in("NA", c)
+ ])
+
+ if length(heat_rate) > 1
+ heat_rate[2:end] = [
+ (
+ heat_rate[i][1] *
+ (heat_rate[i][2] - heat_rate[i - 1][2]),
+ heat_rate[i][2]
+ ) for i in 2:length(heat_rate)
+ ]
+ heat_rate[1] =
+ (heat_rate[1][1] * heat_rate[1][2], heat_rate[1][2])
+
+ fixed = max(
+ 0.0,
+ heat_rate[1][1] -
+ (heat_rate[2][1] / (heat_rate[2][2] - heat_rate[1][2]) * heat_rate[1][2]),
+ )
+ heat_rate[1] = (heat_rate[1][1] / 1000.0, heat_rate[1][2] * size)
+ for i in 2:length(heat_rate)
+ heat_rate[i] = (heat_rate[i - 1][1] + heat_rate[i][1] / 1000.0, heat_rate[i][2] * size)
+ end
+ pushfirst!(heat_rate, (fixed / 1000.0, 0.0))
+ elseif length(heat_rate) == 1
+ # if there is only one point, use it to determine the constant $/MW cost
+ heat_rate = (heat_rate[1][1] * heat_rate[1][2] / 1000.0, heat_rate[1][2] * size)
+ fixed = 0.0
+ else
+ heat_rate = [(0.0, 0.0)]
+ fixed = 0.0
+ end
+ tech = ThermalTech(type,
+ projectdata["Fuel"],
+ active_power_limits,
+ ramp_limits,
+ up_down_time,
+ operation_cost,
+ fuel_cost,
+ heat_rate,
+ bus,
+ zone,
+ FOR,
+ MTTR)
+
+ return ThermalGenEMIS{BuildPhase}(name,
+ tech,
+ decision_year,
+ construction_year,
+ retirement_year,
+ end_life_year,
+ products,
+ finance_data)
+ elseif type == "WT" || type == "PVe"
+ tech = RenewableTech(type,
+ active_power_limits,
+ ramp_limits,
+ operation_cost,
+ bus,
+ zone,
+ FOR,
+ MTTR)
+
+ return RenewableGenEMIS{BuildPhase}(name,
+ tech,
+ decision_year,
+ construction_year,
+ retirement_year,
+ end_life_year,
+ products,
+ finance_data)
+
+ elseif type == "HY"
+ tech = HydroTech(type,
+ active_power_limits,
+ ramp_limits,
+ up_down_time,
+ operation_cost,
+ bus,
+ zone,
+ FOR,
+ MTTR)
+
+ return HydroGenEMIS{BuildPhase}(name,
+ tech,
+ decision_year,
+ construction_year,
+ retirement_year,
+ end_life_year,
+ products,
+ finance_data)
+
+ elseif type == "BA"
+ storage_capacity = parse(Float64, projectdata["Duration Hr"]) * size
+ tech = BatteryTech(type,
+ (min = 0.0, max = parse(Float64, projectdata["Input Power Rating pu"]) * size),
+ (min = 0.0, max = parse(Float64, projectdata["Output Power Rating pu"]) * size),
+ ramp_limits,
+ (min = parse(Float64, projectdata["Min Storgae pu"]) * storage_capacity, max = storage_capacity),
+ 0.0,
+ (in = parse(Float64, projectdata["Round Trip Efficiency pu"]), out = 1.0),
+ bus,
+ zone,
+ FOR,
+ MTTR)
+ return BatteryEMIS{BuildPhase}(name,
+ tech,
+ decision_year,
+ construction_year,
+ retirement_year,
+ end_life_year,
+ products,
+ finance_data)
+
+ end
+
+end
+
+"""
+This function creates the Tech struct for ThermalGen projects
+based on the CSV and PSY Device data.
+"""
+function create_tech_type(name::String,
+ device::T,
+ projectdata::DataFrames.DataFrameRow,
+ size::Float64,
+ base_power::Float64,
+ decision_year::Int64,
+ construction_year::Int64,
+ retirement_year::Int64,
+ end_life_year::Int64,
+ products::Vector{Product},
+ finance_data::Finance
+ ) where T <: PSY.ThermalGen
+
+ min_cap = deepcopy(PSY.get_active_power_limits(device)[:min]) * base_power
+
+ fuel_cost = projectdata["Fuel Price \$/MMBTU"]
+ bus = deepcopy(PSY.get_bus(device))
+ device_ramp_limits = deepcopy(PSY.get_ramp_limits(device))
+
+ if isnothing(device_ramp_limits)
+ ramp_limits = nothing
+ else
+ ramp_limits = (up = device_ramp_limits[:up] * base_power * 60, down = device_ramp_limits[:down] * base_power * 60)
+ end
+
+ prime_mover = string(deepcopy(PSY.get_prime_mover(device)))
+ fuel = string(deepcopy(PSY.get_fuel(device)))
+
+ if fuel == "NUCLEAR"
+ prime_mover = "NU_ST"
+ end
+
+ FOR = projectdata["FOR"]
+ MTTR = projectdata["MTTR Hr"]
+
+ output_point_fields = String[]
+ heat_rate_fields = String[]
+ fields = names(projectdata)
+
+ for field in fields
+ if occursin("Output_pct_", field)
+ push!(output_point_fields, field)
+ elseif occursin("HR_", field)
+ push!(heat_rate_fields, field)
+ end
+ end
+
+ @assert length(output_point_fields) > 0
+ cost_colnames = zip(heat_rate_fields, output_point_fields)
+
+ heat_rate = [(projectdata[hr], projectdata[mw]) for (hr, mw) in cost_colnames]
+
+ heat_rate = unique([
+ (tryparse(Float64, string(c[1])), tryparse(Float64, string(c[2])))
+ for c in heat_rate if !in("NA", c)
+ ])
+ if length(heat_rate) > 1
+ heat_rate[2:end] = [
+ (
+ heat_rate[i][1] *
+ (heat_rate[i][2] - heat_rate[i - 1][2]),
+ heat_rate[i][2]
+ ) for i in 2:length(heat_rate)
+ ]
+ heat_rate[1] =
+ (heat_rate[1][1] * heat_rate[1][2], heat_rate[1][2])
+
+ fixed = max(
+ 0.0,
+ heat_rate[1][1] -
+ (heat_rate[2][1] / (heat_rate[2][2] - heat_rate[1][2]) * heat_rate[1][2]),
+ )
+ heat_rate[1] = (heat_rate[1][1] / 1000.0, heat_rate[1][2] * size)
+ for i in 2:length(heat_rate)
+ heat_rate[i] = (heat_rate[i - 1][1] + heat_rate[i][1] / 1000.0, heat_rate[i][2] * size)
+ end
+ pushfirst!(heat_rate, (fixed / 1000.0, 0.0))
+ elseif length(heat_rate) == 1
+ # if there is only one point, use it to determine the constant $/MW cost
+ heat_rate = (heat_rate[1][1] * heat_rate[1][2] / 1000.0, heat_rate[1][2] * size)
+ fixed = 0.0
+ else
+ heat_rate = [(0.0, 0.0)]
+ fixed = 0.0
+ end
+
+
+ tech = ThermalTech(prime_mover,
+ fuel,
+ (min = min_cap, max = size),
+ ramp_limits,
+ deepcopy(PSY.get_time_limits(device)),
+ deepcopy(PSY.get_operation_cost(device)),
+ fuel_cost,
+ heat_rate,
+ deepcopy(PSY.get_number(bus)),
+ projectdata["Zone"],
+ FOR,
+ MTTR)
+
+ return ThermalGenEMIS{BuildPhase}(name,
+ tech,
+ decision_year,
+ construction_year,
+ retirement_year,
+ end_life_year,
+ products,
+ finance_data)
+
+end
+
+"""
+This function creates the Tech struct for RenewableGen projects
+based on the CSV and PSY Device data.
+"""
+function create_tech_type(name::String,
+ device::T,
+ projectdata::DataFrames.DataFrameRow,
+ size::Float64,
+ base_power::Float64,
+ decision_year::Int64,
+ construction_year::Int64,
+ retirement_year::Int64,
+ end_life_year::Int64,
+ products::Vector{Product},
+ finance_data::Finance
+ ) where T <: PSY.RenewableGen
+
+ bus = deepcopy(PSY.get_bus(device))
+
+ type = string(deepcopy(PSY.get_prime_mover(device)))
+
+ FOR = projectdata["FOR"]
+ MTTR = projectdata["MTTR Hr"]
+
+ tech = RenewableTech(type,
+ (min = 0.0, max = size),
+ (up = size, down = size),
+ deepcopy(PSY.get_operation_cost(device)),
+ deepcopy(PSY.get_number(bus)),
+ projectdata["Zone"],
+ FOR,
+ MTTR)
+
+ return RenewableGenEMIS{BuildPhase}(name,
+ tech,
+ decision_year,
+ construction_year,
+ retirement_year,
+ end_life_year,
+ products,
+ finance_data)
+
+end
+
+"""
+This function creates the Tech struct for HydroGen projects
+based on the CSV and PSY Device data.
+"""
+function create_tech_type(name::String,
+ device::P,
+ projectdata::DataFrames.DataFrameRow,
+ size::Float64,
+ base_power::Float64,
+ decision_year::Int64,
+ construction_year::Int64,
+ retirement_year::Int64,
+ end_life_year::Int64,
+ products::Vector{Product},
+ finance_data::Finance
+ ) where P <: PSY.HydroGen
+
+ min_cap = deepcopy(PSY.get_active_power_limits(device)[:min]) * base_power
+ bus = deepcopy(PSY.get_bus(device))
+
+ device_ramp_limits = deepcopy(PSY.get_ramp_limits(device))
+
+ if isnothing(device_ramp_limits)
+ ramp_limits = nothing
+ else
+ ramp_limits = (up = device_ramp_limits[:up] * base_power * 60, down = device_ramp_limits[:down] * base_power * 60)
+ end
+
+ FOR = projectdata["FOR"]
+ MTTR = projectdata["MTTR Hr"]
+
+ tech = HydroTech("HY",
+ (min = min_cap, max = size),
+ ramp_limits,
+ deepcopy(PSY.get_time_limits(device)),
+ deepcopy(PSY.get_operation_cost(device)),
+ deepcopy( PSY.get_number(bus)),
+ projectdata["Zone"],
+ FOR,
+ MTTR)
+
+ return HydroGenEMIS{BuildPhase}(name,
+ tech,
+ decision_year,
+ construction_year,
+ retirement_year,
+ end_life_year,
+ products,
+ finance_data)
+
+end
+
+"""
+This function creates the Tech struct for Battery projects
+based on the CSV and PSY Device data.
+"""
+function create_tech_type(name::String,
+ device::PSY.GenericBattery,
+ projectdata::DataFrames.DataFrameRow,
+ size::Float64,
+ base_power::Float64,
+ decision_year::Int64,
+ construction_year::Int64,
+ retirement_year::Int64,
+ end_life_year::Int64,
+ products::Vector{Product},
+ finance_data::Finance
+ )
+
+ bus = deepcopy(PSY.get_bus(device))
+
+ type = string(deepcopy(PSY.get_prime_mover(device)))
+ input_active_power_limits = deepcopy(PSY.get_input_active_power_limits(device))
+ output_active_power_limits = deepcopy(PSY.get_output_active_power_limits(device))
+ storage_capacity = deepcopy(PSY.get_state_of_charge_limits(device))
+
+ FOR = projectdata["FOR"]
+ MTTR = projectdata["MTTR Hr"]
+
+ tech = BatteryTech(type,
+ (min = input_active_power_limits[:min] * base_power, max = input_active_power_limits[:max] * base_power),
+ (min = output_active_power_limits[:min] * base_power, max = output_active_power_limits[:max] * base_power),
+ (up = size, down = size),
+ (min = storage_capacity[:min] * base_power, max = storage_capacity[:max] * base_power), #originally has *size
+ PSY.get_initial_energy(device) * size * base_power,
+ PSY.get_efficiency(device),
+ PSY.get_number(bus),
+ projectdata["Zone"],
+ FOR,
+ MTTR)
+
+ return BatteryEMIS{BuildPhase}(name,
+ tech,
+ decision_year,
+ construction_year,
+ retirement_year,
+ end_life_year,
+ products,
+ finance_data)
+
+end
+
+"""
+This function returns a Project struct based only on project's CSV data.
+"""
+function create_project(projectdata::DataFrames.DataFrameRow,
+ simulation_data::AgentSimulationData,
+ investor_name::String,
+ investor_dir::String,
+ scenario_names::Vector{String},
+ lag_bool::Bool)
+
+ name = String(projectdata["GEN_UID"])
+
+ unit_type = String(projectdata["Unit Type"])
+ size_raw = projectdata["Size"]
+ if typeof(size_raw) !== Float64
+ size_raw = String(size_raw)
+ end
+
+ size = Float64(size_in_MW(investor_dir,
+ unit_type,
+ size_raw))
+
+ base_power = size
+ carbon_tax = get_carbon_tax(simulation_data)
+
+ products = create_products(simulation_data,
+ projectdata)
+
+ decision_year,
+ construction_year,
+ retirement_year,
+ end_life_year,
+ finance_data = create_investment_data(size, projectdata, investor_dir, simulation_data, products, investor_name, scenario_names, lag_bool)
+
+ sys_UC = get_system_UC(simulation_data)
+
+ project = create_tech_type(name, projectdata, size, base_power, decision_year, construction_year, retirement_year, end_life_year, products, finance_data, sys_UC, carbon_tax)
+
+ return project
+end
+
+
+
+"""
+This function returns a Project struct based on CSV data and PSY data.
+"""
+function create_project(projectdata::DataFrames.DataFrameRow,
+ device::T,
+ simulation_data::AgentSimulationData,
+ investor_name::String,
+ investor_dir::String,
+ scenario_names::Vector{String},
+ lag_bool::Bool) where T <: Union{PSY.Generator, PSY.Storage}
+
+ name = get_name(device)
+
+ base_power = PSY.get_base_power(device)
+
+ size = get_device_size(device) * base_power
+
+ products = create_products(simulation_data,
+ projectdata)
+
+ decision_year,
+ construction_year,
+ retirement_year,
+ end_life_year,
+ finance_data = create_investment_data(size, projectdata, investor_dir, simulation_data, products, investor_name, scenario_names, lag_bool)
+
+ project = create_tech_type(name, device, projectdata, size, base_power, decision_year, construction_year, retirement_year, end_life_year, products, finance_data)
+
+ return project
+end
diff --git a/src/struct_creators/simulation_structs/project_vector_creator.jl b/src/struct_creators/simulation_structs/project_vector_creator.jl
index 2f1e2ec..c28edaa 100644
--- a/src/struct_creators/simulation_structs/project_vector_creator.jl
+++ b/src/struct_creators/simulation_structs/project_vector_creator.jl
@@ -1,106 +1,106 @@
-"""
-This function returns an empty vector of existing projects,
-if data for exsting projects does not exist.
-"""
-function create_project_existing(projectdata::Nothing,
- simulation_data::AgentSimulationData,
- sys_UC::Union{Nothing, PSY.System},
- investor_name::String,
- investor_dir::String,
- scenario_names::Vector{String})
- return Project{Existing}[]
-end
-
-"""
-This function returns a vector of existing projects for the investor.
-"""
-function create_project_existing(projectdata::DataFrames.DataFrame,
- simulation_data::AgentSimulationData,
- sys_UC::Nothing,
- investor_name::String,
- investor_dir::String,
- scenario_names::Vector{String})
-
- project_existing = Vector{Project{Existing}}(undef, size(projectdata,1))
-
- for i in 1:size(projectdata,1)
- project_existing[i] = create_project(projectdata[i,:],
- simulation_data,
- investor_name,
- investor_dir,
- scenario_names,
- false)
- println("Created project $(get_name(project_existing[i])) for $(investor_name)")
- end
-
- return project_existing
-end
-
-"""
-This function returns a vector of existing projects for the investor if PSY data exists.
-"""
-function create_project_existing(projectdata::DataFrames.DataFrame,
- simulation_data::AgentSimulationData,
- sys_UC::PSY.System,
- investor_name::String,
- investor_dir::String,
- scenario_names::Vector{String})
-
- existing_gens = PSY.get_components(PSY.Generator, sys_UC)
- existing_storage = PSY.get_components(PSY.Storage, sys_UC)
-
- existing_techs = union(existing_gens, existing_storage)
-
- project_existing = Vector{Project{Existing}}(undef, DataFrames.nrow(projectdata))
-
- for i in 1:DataFrames.nrow(projectdata)
-
- device = PSY.get_components_by_name(PSY.Device, sys_UC, projectdata[i, "GEN_UID"])
-
- project_existing[i] = create_project(projectdata[i, :],
- device[1],
- simulation_data,
- investor_name,
- investor_dir,
- scenario_names,
- false)
-
- println("Created project $(get_name(project_existing[i])) for $(investor_name)")
- end
-
- return project_existing
-end
-
-"""
-This function returns an empty vector of option projects,
-if data for option projects does not exist.
-"""
-function create_project_options(projectdata::Nothing,
- simulation_data::AgentSimulationData,
- investor_name::String,
- investor_dir::String,
- scenario_names::Vector{String})
- return Project{Option}[]
-end
-
-"""
-This function returns a vector of project options for the investor.
-"""
-function create_project_options(projectdata::DataFrames.DataFrame,
- simulation_data::AgentSimulationData,
- investor_name::String,
- investor_dir::String,
- scenario_names::Vector{String})
- project_option = Vector{Project{Option}}(undef, size(projectdata,1))
- for i in 1:size(projectdata,1)
- project_option[i] = create_project(projectdata[i,:],
- simulation_data,
- investor_name,
- investor_dir,
- scenario_names,
- true)
-
- println("Created project $(get_name(project_option[i])) for $(investor_name)")
- end
- return project_option
-end
+"""
+This function returns an empty vector of existing projects,
+if data for exsting projects does not exist.
+"""
+function create_project_existing(projectdata::Nothing,
+ simulation_data::AgentSimulationData,
+ sys_UC::Union{Nothing, PSY.System},
+ investor_name::String,
+ investor_dir::String,
+ scenario_names::Vector{String})
+ return Project{Existing}[]
+end
+
+"""
+This function returns a vector of existing projects for the investor.
+"""
+function create_project_existing(projectdata::DataFrames.DataFrame,
+ simulation_data::AgentSimulationData,
+ sys_UC::Nothing,
+ investor_name::String,
+ investor_dir::String,
+ scenario_names::Vector{String})
+
+ project_existing = Vector{Project{Existing}}(undef, size(projectdata,1))
+
+ for i in 1:size(projectdata,1)
+ project_existing[i] = create_project(projectdata[i,:],
+ simulation_data,
+ investor_name,
+ investor_dir,
+ scenario_names,
+ false)
+ println("Created project $(get_name(project_existing[i])) for $(investor_name)")
+ end
+
+ return project_existing
+end
+
+"""
+This function returns a vector of existing projects for the investor if PSY data exists.
+"""
+function create_project_existing(projectdata::DataFrames.DataFrame,
+ simulation_data::AgentSimulationData,
+ sys_UC::PSY.System,
+ investor_name::String,
+ investor_dir::String,
+ scenario_names::Vector{String})
+
+ existing_gens = PSY.get_components(PSY.Generator, sys_UC)
+ existing_storage = PSY.get_components(PSY.Storage, sys_UC)
+
+ existing_techs = union(existing_gens, existing_storage)
+
+ project_existing = Vector{Project{Existing}}(undef, DataFrames.nrow(projectdata))
+
+ for i in 1:DataFrames.nrow(projectdata)
+
+ device = PSY.get_components_by_name(PSY.Device, sys_UC, projectdata[i, "GEN_UID"])
+
+ project_existing[i] = create_project(projectdata[i, :],
+ device[1],
+ simulation_data,
+ investor_name,
+ investor_dir,
+ scenario_names,
+ false)
+
+ @info "Created project $(get_name(project_existing[i])) for $(investor_name)"
+ end
+
+ return project_existing
+end
+
+"""
+This function returns an empty vector of option projects,
+if data for option projects does not exist.
+"""
+function create_project_options(projectdata::Nothing,
+ simulation_data::AgentSimulationData,
+ investor_name::String,
+ investor_dir::String,
+ scenario_names::Vector{String})
+ return Project{Option}[]
+end
+
+"""
+This function returns a vector of project options for the investor.
+"""
+function create_project_options(projectdata::DataFrames.DataFrame,
+ simulation_data::AgentSimulationData,
+ investor_name::String,
+ investor_dir::String,
+ scenario_names::Vector{String})
+ project_option = Vector{Project{Option}}(undef, size(projectdata,1))
+ for i in 1:size(projectdata,1)
+ project_option[i] = create_project(projectdata[i,:],
+ simulation_data,
+ investor_name,
+ investor_dir,
+ scenario_names,
+ true)
+
+ @info "Created project $(get_name(project_option[i])) for $(investor_name)"
+ end
+ return project_option
+end
diff --git a/src/struct_creators/simulation_structs/rts_psy_creator.jl b/src/struct_creators/simulation_structs/rts_psy_creator.jl
index 70fab20..89e83ea 100644
--- a/src/struct_creators/simulation_structs/rts_psy_creator.jl
+++ b/src/struct_creators/simulation_structs/rts_psy_creator.jl
@@ -1,80 +1,123 @@
-function prune_system_devices!(sys::PSY.System, prune_dict::Dict{Type{<:PSY.Component}, Array{AbstractString}})
- for (type, device_names) in prune_dict
- for name in device_names
- device = PSY.get_component(type, sys, name)
- PSY.remove_component!(sys, device)
- end
- end
- return
-end
-
-"""
-Use this function to specify which devices to remove from the RTS at the start of the simulation.
-"""
-function specify_pruned_units()
- pruned_unit = Dict{Type{<:PSY.Component}, Array{AbstractString}}()
- pruned_unit[PSY.ThermalStandard] = ["115_STEAM_1", "115_STEAM_2", "315_STEAM_1", "315_STEAM_2", "315_STEAM_3", "315_STEAM_4", "315_STEAM_5",
- "101_CT_1", "101_CT_2", "102_CT_1", "102_CT_2",
- "201_CT_1", "201_CT_2", "202_CT_1", "202_CT_2",
- "301_CT_1", "301_CT_2", "302_CT_1", "302_CT_2",
- "207_CT_1", "307_CT_1", "101_STEAM_4"]
- pruned_unit[PSY.RenewableFix] = ["308_RTPV_1", "313_RTPV_1", "313_RTPV_2", "313_RTPV_3", "313_RTPV_4", "313_RTPV_5", "313_RTPV_6", "313_RTPV_7",
- "313_RTPV_8", "313_RTPV_9", "313_RTPV_10", "313_RTPV_11", "313_RTPV_12", "320_RTPV_1", "320_RTPV_2", "320_RTPV_3",
- "313_RTPV_13", "320_RTPV_4", "320_RTPV_5", "118_RTPV_1", "118_RTPV_2", "118_RTPV_3", "118_RTPV_4", "118_RTPV_5",
- "118_RTPV_6", "320_RTPV_6", "118_RTPV_7", "118_RTPV_8", "118_RTPV_9", "118_RTPV_10", "213_RTPV_1"]
- pruned_unit[PSY.RenewableDispatch] = ["309_WIND_1", "212_CSP_1"]
- pruned_unit[PSY.Generator] = ["114_SYNC_COND_1", "314_SYNC_COND_1", "214_SYNC_COND_1"]
- pruned_unit[PSY.GenericBattery] = ["313_STORAGE_1"]
-
- return pruned_unit
- end
-
-
-function create_rts_sys(rts_dir::String,
- base_power::Float64,
- simulation_dir::String,
- da_resolution::Int64,
- rt_resolution::Int64)
-
- da_products = split(read_data(joinpath(simulation_dir, "markets_data", "reserve_products.csv"))[1,"da_products"], "; ")
-
- rts_src_dir = joinpath(rts_dir, "RTS_Data", "SourceData")
- rts_siip_dir = joinpath(rts_dir, "RTS_Data", "FormattedData", "SIIP");
-
- rawsys = PSY.PowerSystemTableData(
- rts_src_dir,
- base_power,
- joinpath(rts_siip_dir, "user_descriptors.yaml"),
- timeseries_metadata_file = joinpath(rts_siip_dir, "timeseries_pointers.json"),
- );
-
- sys_UC = PSY.System(rawsys; time_series_resolution = Dates.Minute(da_resolution));
-
- services_UC = get_system_services(sys_UC)
-
- for service in services_UC
- if !(PSY.get_name(service) in da_products)
- PSY.remove_component!(sys_UC, service)
- end
- end
-
- rt_products = split(read_data(joinpath(simulation_dir, "markets_data", "reserve_products.csv"))[1,"rt_products"], "; ")
-
- sys_ED = PSY.System(rawsys; time_series_resolution = Dates.Minute(rt_resolution));
-
- services_ED = get_system_services(sys_ED)
-
- for service in services_ED
- if !(PSY.get_name(service) in rt_products)
- PSY.remove_component!(sys_ED, service)
- end
- end
-
- pruned_unit = specify_pruned_units()
- device_list = Dict{Type{<:PSY.Component}, Array{AbstractString}}()
- prune_system_devices!(sys_UC, pruned_unit)
- prune_system_devices!(sys_ED, pruned_unit)
-
- return sys_UC, sys_ED
-end
-
+function prune_system_devices!(sys::PSY.System, prune_dict::Dict{Type{<:PSY.Component}, Array{AbstractString}})
+ for (type, device_names) in prune_dict
+ for name in device_names
+ device = PSY.get_component(type, sys, name)
+ PSY.remove_component!(sys, device)
+ end
+ end
+ return
+end
+
+"""
+Use this function to specify which devices to remove from the RTS at the start of the simulation.
+"""
+function specify_pruned_units()
+ pruned_unit = Dict{Type{<:PSY.Component}, Array{AbstractString}}()
+ pruned_unit[PSY.ThermalStandard] = ["115_STEAM_1", "115_STEAM_2", "315_STEAM_1", "315_STEAM_2", "315_STEAM_3", "315_STEAM_4", "315_STEAM_5",
+ "101_CT_1", "101_CT_2", "102_CT_1", "102_CT_2",
+ "201_CT_1", "201_CT_2", "202_CT_1", "202_CT_2",
+ "301_CT_1", "301_CT_2", "302_CT_1", "302_CT_2",
+ "207_CT_1", "307_CT_1", "101_STEAM_4",
+ "123_STEAM_3", "223_STEAM_1", "223_STEAM_3"]
+ pruned_unit[PSY.RenewableFix] = ["308_RTPV_1", "313_RTPV_1", "313_RTPV_2", "313_RTPV_3", "313_RTPV_4", "313_RTPV_5", "313_RTPV_6", "313_RTPV_7",
+ "313_RTPV_8", "313_RTPV_9", "313_RTPV_10", "313_RTPV_11", "313_RTPV_12", "320_RTPV_1", "320_RTPV_2", "320_RTPV_3",
+ "313_RTPV_13", "320_RTPV_4", "320_RTPV_5", "118_RTPV_1", "118_RTPV_2", "118_RTPV_3", "118_RTPV_4", "118_RTPV_5",
+ "118_RTPV_6", "320_RTPV_6", "118_RTPV_7", "118_RTPV_8", "118_RTPV_9", "118_RTPV_10", "213_RTPV_1"]
+ pruned_unit[PSY.RenewableDispatch] = ["309_WIND_1", "212_CSP_1"]
+ pruned_unit[PSY.Generator] = ["114_SYNC_COND_1", "314_SYNC_COND_1", "214_SYNC_COND_1"]
+ pruned_unit[PSY.GenericBattery] = ["313_STORAGE_1"]
+
+ return pruned_unit
+ end
+
+
+function create_rts_sys(rts_dir::String,
+ base_power::Float64,
+ simulation_dir::String,
+ da_resolution::Int64,
+ rt_resolution::Int64)
+
+ # da_products = split(read_data(joinpath(simulation_dir, "markets_data", "reserve_products.csv"))[1,"da_products"], "; ")
+ #
+ # rts_src_dir = joinpath(rts_dir, "RTS_Data", "SourceData")
+ # rts_siip_dir = joinpath(rts_dir, "RTS_Data", "FormattedData", "SIIP");
+ #
+ # rawsys = PSY.PowerSystemTableData(
+ # rts_src_dir,
+ # base_power,
+ # joinpath(rts_siip_dir, "user_descriptors.yaml"),
+ # timeseries_metadata_file = joinpath(rts_siip_dir, "timeseries_pointers.json"),
+ # );
+ #
+ # sys_UC = PSY.System(rawsys; time_series_resolution = Dates.Minute(da_resolution));
+ #
+ # services_UC = get_system_services(sys_UC)
+ #
+ # for service in services_UC
+ # if !(PSY.get_name(service) in da_products)
+ # PSY.remove_component!(sys_UC, service)
+ # end
+ # end
+ #
+ # rt_products = split(read_data(joinpath(simulation_dir, "markets_data", "reserve_products.csv"))[1,"rt_products"], "; ")
+ #
+ # sys_ED = PSY.System(rawsys; time_series_resolution = Dates.Minute(rt_resolution));
+ #
+ # services_ED = get_system_services(sys_ED)
+ #
+ # for service in services_ED
+ # if !(PSY.get_name(service) in rt_products)
+ # PSY.remove_component!(sys_ED, service)
+ # end
+ # end
+ #
+ # pruned_unit = specify_pruned_units()
+ # prune_system_devices!(sys_UC, pruned_unit)
+ # prune_system_devices!(sys_ED, pruned_unit)
+
+ sys_UC = PSY.System(joinpath(rts_dir,"DA_sys_EMIS_v0811_sampleoutage.json"), time_series_directory = "/tmp/scratch");
+ sys_ED = PSY.System(joinpath(rts_dir,"RT_sys_EMIS_v0811_sampleoutage.json"), time_series_directory = "/tmp/scratch");
+
+ removegen_name = ["AUSTIN_1","AUSTIN_2"]
+ for sys in [sys_UC, sys_ED]
+ for d in PSY.get_components(PSY.Generator, sys, x -> x.name ∈ removegen_name)
+ # println("Now removing $(get_name(d))")
+ PSY.remove_component!(sys, d)
+ end
+ end
+
+ for sys in [sys_UC, sys_ED]
+ d= PSY.get_component(PSY.VariableReserve,sys,"SPIN")
+ PSY.remove_component!(sys,d)
+ end
+
+ for sys in [sys_UC, sys_ED]
+ d= PSY.get_component(PSY.VariableReserveNonSpinning,sys,"NONSPIN")
+ PSY.remove_component!(sys,d)
+ end
+
+ for sys in [sys_UC, sys_ED]
+ d= PSY.get_component(PSY.VariableReserve,sys,"REG_DN")
+ PSY.set_name!(sys,d,"Reg_Down")
+ end
+
+ for sys in [sys_UC, sys_ED]
+ d= PSY.get_component(PSY.VariableReserve,sys,"REG_UP")
+ PSY.set_name!(sys,d,"Reg_Up")
+ end
+
+ PSY.set_units_base_system!(sys_UC, PSY.IS.UnitSystem.DEVICE_BASE)
+ PSY.set_units_base_system!(sys_ED, PSY.IS.UnitSystem.DEVICE_BASE)
+
+ return sys_UC, sys_ED
+end
+
+function remove_vre_gens!(sys::PSY.System)
+ for gen in get_all_techs(sys)
+ if typeof(gen) == PSY.RenewableDispatch
+ println(PSY.get_name(gen))
+ println(PSY.get_ext(gen))
+ PSY.remove_component!(sys, gen)
+ end
+ end
+end
diff --git a/src/struct_creators/simulation_structs/siip_psy_device_creator.jl b/src/struct_creators/simulation_structs/siip_psy_device_creator.jl
index 997e37e..1a23e04 100644
--- a/src/struct_creators/simulation_structs/siip_psy_device_creator.jl
+++ b/src/struct_creators/simulation_structs/siip_psy_device_creator.jl
@@ -1,110 +1,143 @@
-
-"""
-This function creates a PowerSystems ThermalStandard unit.
-"""
-function create_PSY_generator(gen::ThermalGenEMIS{<: BuildPhase}, sys::PSY.System)
-
- tech = get_tech(gen)
- base_power = get_maxcap(gen)
-
- buses = PSY.get_components(PSY.Bus, sys)
- project_bus = PSY.Bus[]
-
- for b in buses
- if PSY.get_number(b) == get_bus(tech)
- push!(project_bus, b)
- end
- end
-
- PSY_gen = PSY.ThermalStandard(
- get_name(gen), # name
- true, # available
- true, # status
- project_bus[1], # bus
- get_maxcap(gen) / base_power, # active power
- 1.0, # reactive power
- get_maxcap(gen) / base_power, # rating
- (min = get_mincap(gen) / base_power, max = get_maxcap(gen) / base_power), # active power limits
- nothing, # reactive power limits
- (up = get_ramp_limits(tech)[:up] / (base_power * 60), down = get_ramp_limits(tech)[:down] / (base_power * 60)), # ramp limits
- get_operation_cost(tech), # operation cost
- base_power, # base power
- get_time_limits(tech), # up and down time limits
- PSY.PrimeMovers.Symbol(get_type(tech)), # primemover
- PSY.ThermalFuels.Symbol(get_fuel(tech)), # fuel type
- )
- return PSY_gen
-end
-
-"""
-This function creates a PowerSystems RenewableDispatch unit.
-"""
-function create_PSY_generator(gen::RenewableGenEMIS{<: BuildPhase}, sys::PSY.System)
- tech = get_tech(gen)
- base_power = get_maxcap(gen)
-
- if get_type(tech) == "WT"
- primemover = PSY.PrimeMovers.WT
- elseif get_type(tech) == "PVe"
- primemover = PSY.PrimeMovers.PVe
- end
-
- buses = PSY.get_components(PSY.Bus, sys)
- project_bus = PSY.Bus[]
-
- for b in buses
- if PSY.get_number(b) == get_bus(tech)
- push!(project_bus, b)
- end
- end
-
- PSY_gen = PSY.RenewableDispatch(
- get_name(gen), # name
- true, # available
- project_bus[1], # bus
- get_maxcap(gen) / base_power, # active power
- 1.0, # reactive power
- get_maxcap(gen) / base_power, # rating
- primemover, # primemover
- nothing, # reactivepower limits
- 1.0, # power factor
- get_operation_cost(tech),
- base_power, # base power
- )
- return PSY_gen
-end
-
-"""
-This function creates a PowerSystems GenericBattery unit.
-"""
-function create_PSY_generator(gen::BatteryEMIS{<: BuildPhase}, sys::PSY.System)
- tech = get_tech(gen)
- base_power = get_maxcap(gen)
-
- buses = PSY.get_components(PSY.Bus, sys)
- project_bus = PSY.Bus[]
-
- for b in buses
- if PSY.get_number(b) == get_bus(tech)
- push!(project_bus, b)
- end
- end
-
- PSY_gen = PSY.GenericBattery(
- get_name(gen), # name
- true, # available
- project_bus[1], # bus
- PSY.PrimeMovers.Symbol(get_type(tech)), # primemover
- get_soc(tech) / (get_maxcap(gen) * base_power), #initial state of charge
- (min = get_storage_capacity(tech)[:min] / (get_maxcap(gen) * base_power), max = get_storage_capacity(tech)[:max] / (get_maxcap(gen) * base_power)), # state of charge limits
- get_maxcap(gen) / base_power, # rating
- get_maxcap(gen) / base_power, # active power
- (min = get_input_active_power_limits(tech)[:min] / base_power, max = get_input_active_power_limits(tech)[:max] / base_power), # input active power limits
- (min = get_output_active_power_limits(tech)[:min] / base_power, max = get_output_active_power_limits(tech)[:max] / base_power), # output active power limits
- get_efficiency(tech), # efficiency
- 1.0, # reactive power
- nothing, # reactive power limits
- base_power, # base power
- )
- return PSY_gen
-end
+
+function add_inertia_constant!(device::PSY.Device, product::T) where T <: Product
+ return
+end
+
+function add_inertia_constant!(device::PSY.Device, product::Inertia)
+ device.ext["inertia"] = get_h_constant(product)
+ return
+end
+
+"""
+This function creates a PowerSystems ThermalStandard unit.
+"""
+function create_PSY_generator(gen::ThermalGenEMIS{<: BuildPhase}, sys::PSY.System)
+
+ tech = get_tech(gen)
+ base_power = get_maxcap(gen)
+
+ buses = PSY.get_components(PSY.Bus, sys)
+ project_bus = PSY.Bus[]
+
+ for b in buses
+ if PSY.get_number(b) == get_bus(tech)
+ push!(project_bus, b)
+ end
+ end
+
+ type = deepcopy(get_type(tech))
+ if type == "RE_CT"
+ type = "CT"
+ end
+
+ PSY_gen = PSY.ThermalStandard(
+ get_name(gen), # name
+ true, # available
+ true, # status
+ project_bus[1], # bus
+ get_maxcap(gen) / base_power, # active power
+ 1.0, # reactive power
+ get_maxcap(gen) / base_power, # rating
+ (min = get_mincap(gen) / base_power, max = get_maxcap(gen) / base_power), # active power limits
+ nothing, # reactive power limits
+ (up = get_ramp_limits(tech)[:up] / (base_power * 60), down = get_ramp_limits(tech)[:down] / (base_power * 60)), # ramp limits
+ get_operation_cost(tech), # operation cost
+ base_power, # base power
+ get_time_limits(tech), # up and down time limits
+ PSY.PrimeMovers(findfirst(x -> Symbol(x) == Symbol(type), collect(instances(PSY.PrimeMovers)))), # primemover
+ PSY.ThermalFuels(findfirst(x -> Symbol(x) == Symbol(get_fuel(tech)), collect(instances(PSY.ThermalFuels)))), # fuel type
+ )
+ for product in get_products(gen)
+ add_inertia_constant!(PSY_gen, product)
+ end
+
+ add_outage_info!(PSY_gen, tech)
+
+ return PSY_gen
+end
+
+"""
+This function creates a PowerSystems RenewableDispatch unit.
+"""
+function create_PSY_generator(gen::RenewableGenEMIS{<: BuildPhase}, sys::PSY.System)
+ tech = get_tech(gen)
+ base_power = get_maxcap(gen)
+
+ if get_type(tech) == "WT"
+ primemover = PSY.PrimeMovers.WT
+ elseif get_type(tech) == "PVe"
+ primemover = PSY.PrimeMovers.PVe
+ end
+
+ buses = PSY.get_components(PSY.Bus, sys)
+ project_bus = PSY.Bus[]
+
+ for b in buses
+ if PSY.get_number(b) == get_bus(tech)
+ push!(project_bus, b)
+ end
+ end
+
+ PSY_gen = PSY.RenewableDispatch(
+ get_name(gen), # name
+ true, # available
+ project_bus[1], # bus
+ get_maxcap(gen) / base_power, # active power
+ 1.0, # reactive power
+ get_maxcap(gen) / base_power, # rating
+ primemover, # primemover
+ nothing, # reactivepower limits
+ 1.0, # power factor
+ get_operation_cost(tech),
+ base_power, # base power
+ )
+ for product in get_products(gen)
+ add_inertia_constant!(PSY_gen, product)
+ end
+
+ add_outage_info!(PSY_gen, tech)
+
+ return PSY_gen
+end
+
+"""
+This function creates a PowerSystems GenericBattery unit.
+"""
+function create_PSY_generator(gen::BatteryEMIS{<: BuildPhase}, sys::PSY.System)
+ tech = get_tech(gen)
+ base_power = get_maxcap(gen)
+
+ buses = PSY.get_components(PSY.Bus, sys)
+ project_bus = PSY.Bus[]
+
+ for b in buses
+ if PSY.get_number(b) == get_bus(tech)
+ push!(project_bus, b)
+ end
+ end
+
+ PSY_gen = PSY.GenericBattery(
+ get_name(gen), # name
+ true, # available
+ project_bus[1], # bus
+ PSY.PrimeMovers.BA, # primemover
+ get_soc(tech) / (base_power), #initial state of charge
+ (min = get_storage_capacity(tech)[:min] / base_power, max = get_storage_capacity(tech)[:max] / base_power), # state of charge limits
+ get_maxcap(gen) / base_power, # rating
+ get_maxcap(gen) / base_power, # active power
+ (min = get_input_active_power_limits(tech)[:min] / base_power, max = get_input_active_power_limits(tech)[:max] / base_power), # input active power limits
+ (min = get_output_active_power_limits(tech)[:min] / base_power, max = get_output_active_power_limits(tech)[:max] / base_power), # output active power limits
+ get_efficiency(tech), # efficiency
+ 1.0, # reactive power
+ nothing, # reactive power limits
+ base_power, # base power
+ nothing, #operation_cost
+ )
+ for product in get_products(gen)
+ add_inertia_constant!(PSY_gen, product)
+ end
+
+ add_outage_info!(PSY_gen, tech)
+
+ return PSY_gen
+end
diff --git a/src/structs/AgentSimulation.jl b/src/structs/AgentSimulation.jl
index 33c16b9..c55b878 100644
--- a/src/structs/AgentSimulation.jl
+++ b/src/structs/AgentSimulation.jl
@@ -12,6 +12,7 @@ This struct contains all the data for the simulation to be run.
peak_load: Annual system-wide peak load for capacity market clearing.
markets: Boolean for selecting which markets are modeled.
carbon_tax: Vector of annual carbon taxes
+ rec_requirement: Vector of annual REC requirement
investors: Vectors of all investors created in the simulation.
derating_data: Derating data foe each technology type.
annual_growth: Annual growth rate data of different parameters.
@@ -30,9 +31,11 @@ mutable struct AgentSimulation
peak_load::Float64
markets::Dict{Symbol, Bool}
carbon_tax::Vector{Float64}
+ rec_requirement::Vector{Float64}
investors::Vector{Investor}
derating_data::DataFrames.DataFrame
annual_growth::AxisArrays.AxisArray{Float64, 2}
+ resource_adequacy::ResourceAdequacy
end
get_case(sim::AgentSimulation) = sim.case
@@ -47,9 +50,12 @@ get_hour_weight(sim::AgentSimulation) = sim.hour_weight
get_peak_load(sim::AgentSimulation) = sim.peak_load
get_markets(sim::AgentSimulation) = sim.markets
get_carbon_tax(sim::AgentSimulation) = sim.carbon_tax
+get_rec_requirement(sim::AgentSimulation) = sim.rec_requirement
get_investors(sim::AgentSimulation) = sim.investors
get_derating_data(sim::AgentSimulation) = sim.derating_data
get_annual_growth(sim::AgentSimulation) = sim.annual_growth
+get_resource_adequacy(sim::AgentSimulation) = sim.resource_adequacy
+
function set_iteration_year!(simulation::AgentSimulation, iteration_year::Int64)
simulation.iteration_year = iteration_year
@@ -76,10 +82,12 @@ mutable struct AgentSimulationData
peak_load::Float64
markets::Dict{Symbol, Bool}
carbon_tax::Vector{Float64}
+ rec_requirement::Vector{Float64}
investors::Union{Nothing, Vector{Investor}}
queue_cost_data::DataFrames.DataFrame
derating_data::DataFrames.DataFrame
annual_growth::AxisArrays.AxisArray{Float64, 2}
+ resource_adequacy::ResourceAdequacy
end
function AgentSimulationData(case::CaseDefinition,
@@ -93,9 +101,11 @@ function AgentSimulationData(case::CaseDefinition,
peak_load::Float64,
markets::Dict{Symbol, Bool},
carbon_tax::Vector{Float64},
+ rec_requirement::Vector{Float64},
queue_cost_data::DataFrames.DataFrame,
derating_data::DataFrames.DataFrame,
- annual_growth::AxisArrays.AxisArray{Float64, 2})
+ annual_growth::AxisArrays.AxisArray{Float64, 2},
+ resource_adequacy::ResourceAdequacy)
return AgentSimulationData(case,
1,
system_UC,
@@ -108,10 +118,12 @@ function AgentSimulationData(case::CaseDefinition,
peak_load,
markets,
carbon_tax,
+ rec_requirement,
nothing,
queue_cost_data,
derating_data,
- annual_growth)
+ annual_growth,
+ resource_adequacy)
end
get_case(sim::AgentSimulationData) = sim.case
@@ -126,10 +138,12 @@ get_rep_hour_weight(sim::AgentSimulationData) = sim.rep_hour_weight
get_peak_load(sim::AgentSimulationData) = sim.peak_load
get_markets(sim::AgentSimulationData) = sim.markets
get_carbon_tax(sim::AgentSimulationData) = sim.carbon_tax
+get_rec_requirement(sim::AgentSimulationData) = sim.rec_requirement
get_investors(sim::AgentSimulationData) = sim.investors
get_queue_cost_data(sim::AgentSimulationData) = sim.queue_cost_data
get_derating_data(sim::AgentSimulationData) = sim.derating_data
get_annual_growth(sim::AgentSimulationData) = sim.annual_growth
+get_resource_adequacy(sim::AgentSimulationData) = sim.resource_adequacy
function set_investors!(simulation_data::AgentSimulationData,
investors::Vector{Investor})
diff --git a/src/structs/CaseDefinition.jl b/src/structs/CaseDefinition.jl
index 0fbf007..7907d0f 100644
--- a/src/structs/CaseDefinition.jl
+++ b/src/structs/CaseDefinition.jl
@@ -1,9 +1,11 @@
"""
This struct contains all the required data, parameters and solvers
for creating and running AgentSimulation.
+ name: Case name
base_dir: Directory where Simulation input data for markets and investors are stored.
sys_dir: Test system directory.
- solver: Solvers used for optimization problems. (The solver should be able to solve QP for price prediction and MILP for SIIP production cost model)
+ cem_solver: Solvers used for optimization problems. (The solver should be able to solve QP for price prediction and MILP for SIIP production cost model)
+ siip_solver: Solvers used for optimization problems. (The solver should be able to solve QP for price prediction and MILP for SIIP production cost model)
siip_market_clearing: Whether SIIP production cost model is to be used for energy market clearing. If false, the endogenous Economic Dispatch model will be used for market clearing.
start_year: Start year for the simulation (default is set to 2020)
total_horizon: Number of years of data available for the simulation.
@@ -12,8 +14,20 @@
num_rep_days: Number of representative days used for price prediction.
da_resolution: Resolution of Day Ahead market clearing (minutes)
rt_resolution: Resolution of Real Time market clearing (minutes)
+ rps_target: High, Mid or Low RPS Target
+ markets: Dictionary of which markets are simulated
+ ordc_curved: Whether to include the curved part of the ORDC
+ ordc_unavailability_method: Which method (Sequential Monte Carlo or Convolution) to use for generating unavailability distribution for ORDCs
+ derating_scale: Factor for scaling derating factors
+ mopr: Whether Minimum Offer Price Rule is applied
+ battery_cap_mkt: Whether Batteries can paritcipate in capacity markets
+ vre_reserves: Whether VRE can provide reserves
heterogeneity: Whether investors' heterogeneous financial characteristics and technology preferences are modeled.
+ reserve_penalty: High, Mid or Low penalty prices for reserves
+ static_capacity_market: Whether the capacity market demand curve is static or RA-informed
+ irm_scalar: Scalar for installed reserve margin to be used for creating the capacity market demand curve.
forecast_type: "Perfect" or "imperfect" forecasts used for price prediction.
+ max_carbon_tax_increase: Maximum annual increase in carbon prices due to under-achievement of Clean Energy Targets.
info_symmetry: Whether investors have symmetric information about forecast parameters.
belief_update: Whether investors' beliefs are updated each year after actual market clearing.
uncertainty: Whether multiple probability weighted scenarios are used instead of a deterministic forecast.
@@ -23,6 +37,7 @@
"""
struct CaseDefinition
+ name::String
base_dir::String
sys_dir::String
solver::JuMP.MOI.OptimizerWithAttributes
@@ -34,8 +49,20 @@ struct CaseDefinition
num_rep_days::Int64
da_resolution::Int64
rt_resolution::Int64
+ rps_target::String
+ markets::Dict{Symbol, Bool}
+ ordc_curved::Bool
+ ordc_unavailability_method::String
+ reserve_penalty::String
+ static_capacity_market::Bool
+ irm_scalar::Float64
+ derating_scale::Float64
+ mopr::Bool
+ battery_cap_mkt::Bool
+ vre_reserves::Bool
heterogeneity::Bool
forecast_type::String
+ max_carbon_tax_increase::Float64
info_symmetry::Bool
belief_update::Bool
uncertainty::Bool
@@ -43,7 +70,8 @@ struct CaseDefinition
parallel_investors::Bool
parallel_scenarios::Bool
- function CaseDefinition(base_dir,
+ function CaseDefinition(name,
+ base_dir,
sys_dir,
solver,
siip_market_clearing,
@@ -54,8 +82,20 @@ struct CaseDefinition
num_rep_days,
da_resolution,
rt_resolution,
+ rps_target,
+ markets,
+ ordc_curved,
+ ordc_unavailability_method,
+ reserve_penalty,
+ static_capacity_market,
+ irm_scalar,
+ derating_scale,
+ mopr,
+ battery_cap_mkt,
+ vre_reserves,
heterogeneity,
forecast_type,
+ max_carbon_tax_increase,
info_symmetry,
belief_update,
uncertainty,
@@ -67,6 +107,8 @@ struct CaseDefinition
forecast_type = lowercase(forecast_type)
@assert forecast_type == "perfect" || lowercase(forecast_type) == "imperfect"
+ @assert lowercase(rps_target) == "high" || lowercase(rps_target) == "mid" || lowercase(rps_target) == "low"
+ @assert lowercase(reserve_penalty) == "high" || lowercase(reserve_penalty) == "mid" || lowercase(reserve_penalty) == "low"
if forecast_type == "perfect"
@assert info_symmetry == true
@@ -76,11 +118,14 @@ struct CaseDefinition
end
@assert da_resolution >= rt_resolution
+ @assert irm_scalar >= 0.0
+ #=
if !(siip_market_clearing)
@assert da_resolution == rt_resolution
end
-
- return new(base_dir,
+ =#
+ return new(name,
+ base_dir,
sys_dir,
solver,
siip_market_clearing,
@@ -91,8 +136,20 @@ struct CaseDefinition
num_rep_days,
da_resolution,
rt_resolution,
+ rps_target,
+ markets,
+ ordc_curved,
+ ordc_unavailability_method,
+ reserve_penalty,
+ static_capacity_market,
+ irm_scalar,
+ derating_scale,
+ mopr,
+ battery_cap_mkt,
+ vre_reserves,
heterogeneity,
forecast_type,
+ max_carbon_tax_increase,
info_symmetry,
belief_update,
uncertainty,
@@ -102,7 +159,8 @@ struct CaseDefinition
end
end
-function CaseDefinition(base_dir::String,
+function CaseDefinition(name::String,
+ base_dir::String,
sys_dir::String,
solver::JuMP.MOI.OptimizerWithAttributes;
siip_market_clearing::Bool = true,
@@ -113,8 +171,20 @@ function CaseDefinition(base_dir::String,
num_rep_days::Int64 = 12,
da_resolution::Int64 = 60,
rt_resolution::Int64 = 5,
+ rps_target::String = "Mid",
+ markets::Dict{Symbol, Bool} = Dict(:Energy => true, :Synchronous => true, :Primary => true, :Reg_Up => true, :Reg_Down => true, :Flex_Up => true, :Flex_Down => true, :Capacity => true, :REC => true, :CarbonTax => true),
+ ordc_curved::Bool = true,
+ ordc_unavailability_method::String = "Convolution",
+ reserve_penalty::String = "Mid",
+ static_capacity_market::Bool = true,
+ irm_scalar::Float64 = 1.0,
+ derating_scale::Float64 = 1.0,
+ mopr::Bool = false,
+ battery_cap_mkt::Bool = true,
+ vre_reserves::Bool = true,
heterogeneity::Bool = false,
forecast_type::String = "perfect",
+ max_carbon_tax_increase::Float64 = 0.0,
info_symmetry::Bool = true,
belief_update::Bool = false,
uncertainty::Bool = false,
@@ -122,7 +192,8 @@ function CaseDefinition(base_dir::String,
parallel_investors::Bool = false,
parallel_scenarios::Bool = false)
- CaseDefinition(base_dir,
+ CaseDefinition(name,
+ base_dir,
sys_dir,
solver,
siip_market_clearing,
@@ -133,8 +204,20 @@ function CaseDefinition(base_dir::String,
num_rep_days,
da_resolution,
rt_resolution,
+ rps_target,
+ markets,
+ ordc_curved,
+ ordc_unavailability_method,
+ reserve_penalty,
+ static_capacity_market,
+ irm_scalar,
+ derating_scale,
+ mopr,
+ battery_cap_mkt,
+ vre_reserves,
heterogeneity,
forecast_type,
+ max_carbon_tax_increase,
info_symmetry,
belief_update,
uncertainty,
@@ -154,21 +237,29 @@ get_simulation_years(case::CaseDefinition) = case.simulation_years
get_num_rep_days(case::CaseDefinition) = case.num_rep_days
get_da_resolution(case::CaseDefinition) = case.da_resolution
get_rt_resolution(case::CaseDefinition) = case.rt_resolution
+get_rps_target(case::CaseDefinition) = case.rps_target
+get_markets(case::CaseDefinition) = case.markets
+get_ordc_curved(case::CaseDefinition) = case.ordc_curved
+get_reserve_penalty(case::CaseDefinition) = case.reserve_penalty
+get_static_capacity_market(case::CaseDefinition) = case.static_capacity_market
+get_irm_scalar(case::CaseDefinition) = case.irm_scalar
+get_ordc_unavailability_method(case::CaseDefinition) = case.ordc_unavailability_method
+get_derating_scale(case::CaseDefinition) = case.derating_scale
+get_mopr(case::CaseDefinition) = case.mopr
+get_battery_cap_mkt(case::CaseDefinition) = case.battery_cap_mkt
+get_vre_reserves(case::CaseDefinition) = case.vre_reserves
get_heterogeneity(case::CaseDefinition) = case.heterogeneity
get_info_symmetry(case::CaseDefinition) = case.info_symmetry
get_belief_update(case::CaseDefinition) = case.belief_update
get_forecast_type(case::CaseDefinition) = case.forecast_type
+get_max_carbon_tax_increase(case::CaseDefinition) = case.max_carbon_tax_increase
get_uncertainty(case::CaseDefinition) = case.uncertainty
get_risk_aversion(case::CaseDefinition) = case.risk_aversion
get_parallel_investors(case::CaseDefinition) = case.parallel_investors
get_parallel_scenarios(case::CaseDefinition) = case.parallel_scenarios
function get_name(case::CaseDefinition)
- if get_heterogeneity(case)
- investors = "Heterogeneous"
- else
- investors = "Homogeneous"
- end
+ #=
if get_info_symmetry(case)
information = "InfoSym"
@@ -196,7 +287,72 @@ function get_name(case::CaseDefinition)
case_name = "$(investors)_$(information)_Forecast-$(get_forecast_type(case))_$(uncertainty)_$(update)_$(risk)_$(get_simulation_years(case))years"
+
+ if get_heterogeneity(case)
+ investors = "Het"
+ else
+ investors = "Hom"
+ end
+
+ #New case name
+
+ rps = "$(get_rps_target(case))_RPS"
+
+ if get_markets(case)[:Capacity]
+ capacity = "CapMkt"
+ else
+ capacity = "NoCapMkt"
+ end
+
+ if get_ordc_curved(case)
+ ordc = "with_ORDC"
+ else
+ ordc = "without_ORDC"
+ end
+
+ penalty = "$(get_reserve_penalty(case))_penalty"
+
+ if get_markets(case)[:CarbonTax]
+ carbon = "Carbon_Tax"
+ else
+ carbon = "No_Carbon_Tax"
+ end
+
+
+ if get_mopr(case)
+ mopr = "MOPR_ON"
+ else
+ mopr = "MOPR_OFF"
+ end
+
+ if get_battery_cap_mkt(case)
+ bat_cap = "BAT_Cap_ON"
+ else
+ bat_cap = "BAT_Cap_OFF"
+ end
+
+ derating_scale = replace("$(get_derating_scale(case))", "." => "_")
+
+ derating = "derating_scale_$(derating_scale)"
+
+ if get_vre_reserves(case)
+ vre_reserves = "VRE_reserves"
+ else
+ vre_reserves = "No_VRE_and Bat_reserves"
+ end
+
+ if get_markets(case)[:Inertia]
+ inertia = "Inertia"
+ else
+ inertia = "No_Inertia"
+ end
+
+ case_name = "$(investors)_$(rps)_$(capacity)_$(ordc)_$(penalty)_$(carbon)_$(derating)_$(mopr)_$(bat_cap)_$(vre_reserves)_$(inertia)"
+
return case_name
+
+ =#
+ return "$(case.name)_$(get_rps_target(case))_RPS"
end
function get_data_dir(case::CaseDefinition)
@@ -205,3 +361,6 @@ function get_data_dir(case::CaseDefinition)
return case_dir
end
+
+
+
diff --git a/src/structs/Case_Def_new.jl b/src/structs/Case_Def_new.jl
new file mode 100644
index 0000000..22fd4b5
--- /dev/null
+++ b/src/structs/Case_Def_new.jl
@@ -0,0 +1,322 @@
+"""
+ This struct contains all the required data, parameters and solvers
+ for creating and running AgentSimulation.
+ base_dir: Directory where Simulation input data for markets and investors are stored.
+ sys_dir: Test system directory.
+ cem_solver: Solvers used for optimization problems. (The solver should be able to solve QP for price prediction and MILP for SIIP production cost model)
+ siip_solver: Solvers used for optimization problems. (The solver should be able to solve QP for price prediction and MILP for SIIP production cost model)
+ siip_market_clearing: Whether SIIP production cost model is to be used for energy market clearing. If false, the endogenous Economic Dispatch model will be used for market clearing.
+ start_year: Start year for the simulation (default is set to 2020)
+ total_horizon: Number of years of data available for the simulation.
+ rolling_horizon: Number of years to be used for price prediction. If end of rolling horizon exceeds the years of available data, a receding horizon approach is used.
+ simulation_years: Number of years to be simulated.
+ num_rep_days: Number of representative days used for price prediction.
+ da_resolution: Resolution of Day Ahead market clearing (minutes)
+ rt_resolution: Resolution of Real Time market clearing (minutes)
+ rps_target: High, Mid or Low RPS Target
+ markets: Dictionary of which markets are simulated
+ ordc_curved: Whether to include the curved part of the ORDC
+ derating_scale: Factor for scaling derating factors
+ mopr: Whether Minimum Offer Price Rule is applied
+ vre_reserves: Whether VRE can provide reserves
+ heterogeneity: Whether investors' heterogeneous financial characteristics and technology preferences are modeled.
+ reserve_penalty: High, Mid or Low penalty prices for reserves
+ forecast_type: "Perfect" or "imperfect" forecasts used for price prediction.
+ info_symmetry: Whether investors have symmetric information about forecast parameters.
+ belief_update: Whether investors' beliefs are updated each year after actual market clearing.
+ uncertainty: Whether multiple probability weighted scenarios are used instead of a deterministic forecast.
+ risk_aversion: Whether investors are risk averse.
+ parallel_investors: Whether investors' price prediction is to be parallelized.
+ parallel_scenarios: Whether each investor's price prediction scenarios are to be parallelized.
+"""
+
+struct CaseDefinition
+ base_dir::String
+ sys_dir::String
+ cem_solver::JuMP.MOI.OptimizerWithAttributes
+ siip_solver::JuMP.MOI.OptimizerWithAttributes
+ siip_market_clearing::Bool
+ start_year::Int64
+ total_horizon::Int64
+ rolling_horizon::Int64
+ simulation_years::Int64
+ num_rep_days::Int64
+ da_resolution::Int64
+ rt_resolution::Int64
+ rps_target::String
+ markets::Dict{Symbol, Bool}
+ ordc_curved::Bool
+ reserve_penalty::String
+ derating_scale::Float64
+ mopr::Bool
+ vre_reserves::Bool
+ heterogeneity::Bool
+ forecast_type::String
+ info_symmetry::Bool
+ belief_update::Bool
+ uncertainty::Bool
+ risk_aversion::Bool
+ parallel_investors::Bool
+ parallel_scenarios::Bool
+ solver_name::String
+
+ function CaseDefinition(base_dir,
+ sys_dir,
+ cem_solver,
+ siip_solver,
+ siip_market_clearing,
+ start_year,
+ total_horizon,
+ rolling_horizon,
+ simulation_years,
+ num_rep_days,
+ da_resolution,
+ rt_resolution,
+ rps_target,
+ markets,
+ ordc_curved,
+ reserve_penalty,
+ derating_scale,
+ mopr,
+ vre_reserves,
+ heterogeneity,
+ forecast_type,
+ info_symmetry,
+ belief_update,
+ uncertainty,
+ risk_aversion,
+ parallel_investors,
+ parallel_scenarios,
+ solver_name)
+
+ @assert total_horizon >= simulation_years
+
+ forecast_type = lowercase(forecast_type)
+ @assert forecast_type == "perfect" || lowercase(forecast_type) == "imperfect"
+ @assert lowercase(rps_target) == "high" || lowercase(rps_target) == "mid" || lowercase(rps_target) == "low"
+ @assert lowercase(reserve_penalty) == "high" || lowercase(reserve_penalty) == "mid" || lowercase(reserve_penalty) == "low"
+
+ if forecast_type == "perfect"
+ @assert info_symmetry == true
+ @assert belief_update == false
+ @assert uncertainty == false
+ @assert risk_aversion == false
+ end
+
+ @assert da_resolution >= rt_resolution
+ #=
+ if !(siip_market_clearing)
+ @assert da_resolution == rt_resolution
+ end
+ =#
+ return new(base_dir,
+ sys_dir,
+ cem_solver,
+ siip_solver,
+ siip_market_clearing,
+ start_year,
+ total_horizon,
+ rolling_horizon,
+ simulation_years,
+ num_rep_days,
+ da_resolution,
+ rt_resolution,
+ rps_target,
+ markets,
+ ordc_curved,
+ reserve_penalty,
+ derating_scale,
+ mopr,
+ vre_reserves,
+ heterogeneity,
+ forecast_type,
+ info_symmetry,
+ belief_update,
+ uncertainty,
+ risk_aversion,
+ parallel_investors,
+ parallel_scenarios,
+ solver_name)
+ end
+end
+
+function CaseDefinition(base_dir::String,
+ sys_dir::String,
+ cem_solver::JuMP.MOI.OptimizerWithAttributes,
+ siip_solver::JuMP.MOI.OptimizerWithAttributes;
+ siip_market_clearing::Bool = true,
+ start_year::Int64 = 2020,
+ total_horizon::Int64 = 20,
+ rolling_horizon::Int64 = 10,
+ simulation_years::Int64 = 10,
+ num_rep_days::Int64 = 12,
+ da_resolution::Int64 = 60,
+ rt_resolution::Int64 = 5,
+ rps_target::String = "Mid",
+ markets::Dict{Symbol, Bool} = Dict(:Energy => true, :Synchronous => true, :Primary => true, :Reg_Up => true, :Reg_Down => true, :Flex_Up => true, :Flex_Down => true, :Capacity => true, :REC => true, :CarbonTax => true),
+ ordc_curved::Bool = true,
+ reserve_penalty::String = "Mid",
+ derating_scale::Float64 = 1.0,
+ mopr::Bool = false,
+ vre_reserves::Bool = true,
+ heterogeneity::Bool = false,
+ forecast_type::String = "perfect",
+ info_symmetry::Bool = true,
+ belief_update::Bool = false,
+ uncertainty::Bool = false,
+ risk_aversion::Bool = false,
+ parallel_investors::Bool = false,
+ parallel_scenarios::Bool = false,
+ solver_name::String)
+
+ CaseDefinition(base_dir,
+ sys_dir,
+ cem_solver,
+ siip_solver,
+ siip_market_clearing,
+ start_year,
+ total_horizon,
+ rolling_horizon,
+ simulation_years,
+ num_rep_days,
+ da_resolution,
+ rt_resolution,
+ rps_target,
+ markets,
+ ordc_curved,
+ reserve_penalty,
+ derating_scale,
+ mopr,
+ vre_reserves,
+ heterogeneity,
+ forecast_type,
+ info_symmetry,
+ belief_update,
+ uncertainty,
+ risk_aversion,
+ parallel_investors,
+ parallel_scenarios,
+ solver_name)
+end
+
+get_base_dir(case::CaseDefinition) = case.base_dir
+get_sys_dir(case::CaseDefinition) = case.sys_dir
+get_cem_solver(case::CaseDefinition) = case.cem_solver
+get_siip_solver(case::CaseDefinition) = case.siip_solver
+get_siip_market_clearing(case::CaseDefinition) = case.siip_market_clearing
+get_start_year(case::CaseDefinition) = case.start_year
+get_total_horizon(case::CaseDefinition) = case.total_horizon
+get_rolling_horizon(case::CaseDefinition) = case.rolling_horizon
+get_simulation_years(case::CaseDefinition) = case.simulation_years
+get_num_rep_days(case::CaseDefinition) = case.num_rep_days
+get_da_resolution(case::CaseDefinition) = case.da_resolution
+get_rt_resolution(case::CaseDefinition) = case.rt_resolution
+get_rps_target(case::CaseDefinition) = case.rps_target
+get_markets(case::CaseDefinition) = case.markets
+get_ordc_curved(case::CaseDefinition) = case.ordc_curved
+get_reserve_penalty(case::CaseDefinition) = case.reserve_penalty
+get_derating_scale(case::CaseDefinition) = case.derating_scale
+get_mopr(case::CaseDefinition) = case.mopr
+get_vre_reserves(case::CaseDefinition) = case.vre_reserves
+get_heterogeneity(case::CaseDefinition) = case.heterogeneity
+get_info_symmetry(case::CaseDefinition) = case.info_symmetry
+get_belief_update(case::CaseDefinition) = case.belief_update
+get_forecast_type(case::CaseDefinition) = case.forecast_type
+get_uncertainty(case::CaseDefinition) = case.uncertainty
+get_risk_aversion(case::CaseDefinition) = case.risk_aversion
+get_parallel_investors(case::CaseDefinition) = case.parallel_investors
+get_parallel_scenarios(case::CaseDefinition) = case.parallel_scenarios
+get_solver_name(case::CaseDefinition) = case.solver_name
+
+function get_name(case::CaseDefinition)
+ #=
+ if get_heterogeneity(case)
+ investors = "Heterogeneous"
+ else
+ investors = "Homogeneous"
+ end
+
+ if get_info_symmetry(case)
+ information = "InfoSym"
+ else
+ information = "InfoASym"
+ end
+
+ if get_belief_update(case)
+ update = "UpdateBelief"
+ else
+ update = "NoUpdate"
+ end
+
+ if get_uncertainty(case)
+ uncertainty = "Uncertain"
+ else
+ uncertainty = "Deterministic"
+ end
+
+ if get_risk_aversion(case)
+ risk = "RiskAverse"
+ else
+ risk = "RiskNeutral"
+ end
+
+ case_name = "$(investors)_$(information)_Forecast-$(get_forecast_type(case))_$(uncertainty)_$(update)_$(risk)_$(get_simulation_years(case))years"
+ =#
+ #New case name
+ rps = "$(get_rps_target(case))_RPS"
+
+ if get_markets(case)[:Capacity]
+ capacity = "CapMkt"
+ else
+ capacity = "NoCapMkt"
+ end
+
+ if get_ordc_curved(case)
+ ordc = "with_ORDC"
+ else
+ ordc = "without_ORDC"
+ end
+
+ penalty = "$(get_reserve_penalty(case))_penalty"
+
+ if get_markets(case)[:CarbonTax]
+ carbon = "Carbon_Tax"
+ else
+ carbon = "No_Carbon_Tax"
+ end
+
+ if get_mopr(case)
+ mopr = "MOPR_ON"
+ else
+ mopr = "MOPR_OFF"
+ end
+
+ derating_scale = replace("$(get_derating_scale(case))", "." => "_")
+
+ derating = "derating_scale_$(derating_scale)"
+
+ if get_vre_reserves(case)
+ vre_reserves = "VRE_reserves"
+ else
+ vre_reserves = "No_VRE_reserves"
+ end
+
+ if get_markets(case)[:Inertia]
+ inertia = "Inertia"
+ else
+ inertia = "No_Inertia"
+ end
+
+ solver_name = get_solver_name(case)
+
+ case_name = "$(rps)_$(capacity)_$(ordc)_$(penalty)_$(carbon)_$(derating)_$(mopr)_$(vre_reserves)_$(inertia)_$(solver_name)"
+
+ return case_name
+end
+
+function get_data_dir(case::CaseDefinition)
+ base_dir = get_base_dir(case)
+ case_dir = joinpath(base_dir, get_name(case))
+
+ return case_dir
+end
diff --git a/src/structs/Finance.jl b/src/structs/Finance.jl
index a76cdda..2e1774d 100644
--- a/src/structs/Finance.jl
+++ b/src/structs/Finance.jl
@@ -21,6 +21,7 @@ This struct contains the financial data of projects.
mutable struct Finance
investment_cost::Vector{Float64}
effective_investment_cost::Float64
+ preference_multiplier::Vector{Float64}
lag_time::Int64
life_time::Int64
capex_years::Int64
@@ -40,6 +41,7 @@ end
get_investment_cost(finance_data::Finance) = finance_data.investment_cost
get_effective_investment_cost(finance_data::Finance) = finance_data.effective_investment_cost
+get_preference_multiplier(finance_data::Finance) = finance_data.preference_multiplier
get_lag_time(finance_data::Finance) = finance_data.lag_time
get_life_time(finance_data::Finance) = finance_data.life_time
get_capex_years(finance_data::Finance) = finance_data.capex_years
@@ -56,6 +58,7 @@ get_expected_utility(finance_data::Finance) = finance_data.expected_utility
get_annual_cashflow(finance_data::Finance) = finance_data.annual_cashflow
get_ownedby(finance_data::Finance) = finance_data.ownedby
+
function set_scenario_npv!(finance_data::Finance, scenario_name::String, iteration_year::Int64, npv::Float64)
finance_data.scenario_npv[scenario_name][iteration_year] = npv
return
@@ -137,6 +140,7 @@ end
function set_realized_profit!(finance_data::Finance, profit::AxisArrays.AxisArray{Float64, 2})
finance_data.realized_profit = profit
+
return
end
@@ -152,6 +156,7 @@ function set_realized_profit!(finance_data::Finance,
year_index::Int64,
profit::Float64)
finance_data.realized_profit[product_name, year_index] = profit
+
return
end
@@ -161,9 +166,12 @@ function set_realized_profit!(finance_data::Finance,
end_year::Int64,
profit::Vector{Float64})
finance_data.realized_profit[product_name, start_year:end_year] = profit
+
return
end
function set_annual_cashflow!(finance_data::Finance, year::Int64, cashflow::Float64)
finance_data.annual_cashflow[year] = cashflow
end
+
+
diff --git a/src/structs/Forecast.jl b/src/structs/Forecast.jl
index 910d368..6469995 100644
--- a/src/structs/Forecast.jl
+++ b/src/structs/Forecast.jl
@@ -1,22 +1,22 @@
-"""
-Forecast types include Perfect and Imperfect forecasts
-"""
-abstract type Forecast end
-
-"""
-Perfect forecasts.
-"""
-struct Perfect <: Forecast
- scenario_data::Vector{Scenario}
-end
-
-"""
-Imperfect forecasts with Kalman Filter based updates.
-"""
-struct Imperfect <: Forecast
- kalman_filter::Union{Nothing, KalmanFilter}
- scenario_data::Vector{Scenario}
-end
-
-get_scenario_data(forecast::F) where F<: Forecast = forecast.scenario_data
-get_kalman_filter(forecast::Imperfect) = forecast.kalman_filter
+"""
+Forecast types include Perfect and Imperfect forecasts
+"""
+abstract type Forecast end
+
+"""
+Perfect forecasts.
+"""
+struct Perfect <: Forecast
+ scenario_data::Vector{Scenario}
+end
+
+"""
+Imperfect forecasts with Kalman Filter based updates.
+"""
+struct Imperfect <: Forecast
+ kalman_filter::Union{Nothing, KalmanFilter}
+ scenario_data::Vector{Scenario}
+end
+
+get_scenario_data(forecast::F) where F<: Forecast = forecast.scenario_data
+get_kalman_filter(forecast::Imperfect) = forecast.kalman_filter
diff --git a/src/structs/Investor.jl b/src/structs/Investor.jl
index 8b13ed8..b675ef9 100644
--- a/src/structs/Investor.jl
+++ b/src/structs/Investor.jl
@@ -1,100 +1,116 @@
-"""
-This struct contains all the data for an investor.
- name: Investor's name.
- data_dir: Directoty where investors' data is stored.
- projects: Vector of all projects owned by the investor.
- markets: Vector of markets in which the investor is participating.
- carbon_tax: Vector of annual carbon tax.
- market_prices: Expected market prices.
- rep_hour_weight: Representative hour weight for operating markets.
- forecast: Forecast parameters.
- cap_cost_multiplier: Capital cost multiplier.
- max_annual_projects: Maximum number of projects.
- risk_preference: Whether investor is Risk Neutral or Risk Averse.
-"""
-mutable struct Investor
- name::String
- data_dir::String
- projects::Vector{<: Project{<: BuildPhase}}
- markets::Vector{Symbol}
- carbon_tax::Vector{Float64}
- market_prices::MarketPrices
- rep_hour_weight::Vector{Float64}
- forecast::F where F <: Forecast
- cap_cost_multiplier::Float64
- max_annual_projects::Int64
- risk_preference::T where T <: RiskPreference
-end
-
-get_name(investor::Investor) = investor.name
-get_data_dir(investor::Investor) = investor.data_dir
-get_projects(investor::Investor) = investor.projects
-get_markets(investor::Investor) = investor.markets
-get_carbon_tax(investor::Investor) = investor.carbon_tax
-get_market_prices(investor::Investor) = investor.market_prices
-get_rep_hour_weight(investor::Investor) = investor.rep_hour_weight
-get_forecast(investor::Investor) = investor.forecast
-get_cap_cost_multiplier(investor::Investor) = investor.cap_cost_multiplier
-get_max_annual_projects(investor::Investor) = investor.max_annual_projects
-get_risk_preference(investor::Investor) = investor.risk_preference
-
-# Get projects based on their buildphases
-function get_existing(investor::Investor)
- leaftypes_existing = leaftypes(Project{Existing})
- projects = get_projects(investor)
- existing_projects = filter(project -> in(typeof(project), leaftypes_existing), projects)
-
- return existing_projects
-end
-
-function get_planned(investor::Investor)
- leaftypes_planned = leaftypes(Project{Planned})
- projects = get_projects(investor)
- planned_projects = filter(project -> in(typeof(project), leaftypes_planned), projects)
-
- return planned_projects
-end
-
-function get_queue(investor::Investor)
- leaftypes_queue = leaftypes(Project{Queue})
- projects = get_projects(investor)
- queue_projects = filter(project -> in(typeof(project), leaftypes_queue), projects)
-
- return queue_projects
-end
-
-function get_options(investor::Investor)
- leaftypes_option = leaftypes(Project{Option})
- projects = get_projects(investor)
- option_projects = filter(project -> in(typeof(project), leaftypes_option), projects)
-
- return option_projects
-end
-
-function get_retired(investor::Investor)
- leaftypes_retired = leaftypes(Project{Retired})
- projects = get_projects(investor)
- retired_projects = filter(project -> in(typeof(project), leaftypes_retired), projects)
-
- return retired_projects
-end
-
-function find_active_invested_projects(projects::Vector{<: Project})
- leaftypes_option = leaftypes(Project{Option})
- leaftypes_retired= leaftypes(Project{Retired})
- active_invested_projects = filter(project -> (!in(typeof(project), leaftypes_option) && !in(typeof(project), leaftypes_retired) ), projects)
-
- return active_invested_projects
-end
-
-function find_option_projects(projects::Vector{<: Project})
- leaftypes_option = leaftypes(Project{Option})
- option_projects = filter(project -> in(typeof(project), leaftypes_option), projects)
-
- return option_projects
-end
-
-function set_market_prices!(investor::Investor,
- market_prices::MarketPrices)
- investor.market_prices = market_prices
-end
+"""
+This struct contains all the data for an investor.
+ name: Investor's name.
+ data_dir: Directoty where investors' data is stored.
+ projects: Vector of all projects owned by the investor.
+ markets: Vector of markets in which the investor is participating.
+ carbon_tax: Vector of annual carbon tax.
+ market_prices: Expected market prices.
+ rep_hour_weight: Representative hour weight for operating markets.
+ forecast: Forecast parameters.
+ cap_cost_multiplier: Capital cost multiplier.
+ max_annual_projects: Maximum number of projects.
+ risk_preference: Whether investor is Risk Neutral or Risk Averse.
+ retirement_lookback: Number of past years' data taken into account for making retirement decision
+"""
+mutable struct Investor
+ name::String
+ data_dir::String
+ projects::Vector{<: Project{<: BuildPhase}}
+ markets::Vector{Symbol}
+ carbon_tax::Vector{Float64}
+ market_prices::MarketPrices
+ rep_hour_weight::Vector{Float64}
+ forecast::F where F <: Forecast
+ cap_cost_multiplier::Float64
+ preference_multiplier_range::NamedTuple{(:min, :max),Tuple{Float64,Float64}}
+ portfolio_preference_multipliers::Dict{Tuple{String, String}, Vector{Float64}}
+ max_annual_projects::Int64
+ risk_preference::T where T <: RiskPreference
+ retirement_lookback::Int64
+end
+
+get_name(investor::Investor) = investor.name
+get_data_dir(investor::Investor) = investor.data_dir
+get_projects(investor::Investor) = investor.projects
+get_markets(investor::Investor) = investor.markets
+get_carbon_tax(investor::Investor) = investor.carbon_tax
+get_market_prices(investor::Investor) = investor.market_prices
+get_rep_hour_weight(investor::Investor) = investor.rep_hour_weight
+get_forecast(investor::Investor) = investor.forecast
+get_cap_cost_multiplier(investor::Investor) = investor.cap_cost_multiplier
+get_max_annual_projects(investor::Investor) = investor.max_annual_projects
+get_risk_preference(investor::Investor) = investor.risk_preference
+get_retirement_lookback(investor::Investor) = investor.retirement_lookback
+get_preference_multiplier_range(investor::Investor) = investor.preference_multiplier_range
+get_portfolio_preference_multipliers(investor::Investor) = investor.portfolio_preference_multipliers
+
+
+# Get projects based on their buildphases
+function get_existing(investor::Investor)
+ leaftypes_existing = leaftypes(Project{Existing})
+ projects = get_projects(investor)
+ existing_projects = filter(project -> in(typeof(project), leaftypes_existing), projects)
+
+ return existing_projects
+end
+
+function get_planned(investor::Investor)
+ leaftypes_planned = leaftypes(Project{Planned})
+ projects = get_projects(investor)
+ planned_projects = filter(project -> in(typeof(project), leaftypes_planned), projects)
+
+ return planned_projects
+end
+
+function get_queue(investor::Investor)
+ leaftypes_queue = leaftypes(Project{Queue})
+ projects = get_projects(investor)
+ queue_projects = filter(project -> in(typeof(project), leaftypes_queue), projects)
+
+ return queue_projects
+end
+
+function get_options(investor::Investor)
+ leaftypes_option = leaftypes(Project{Option})
+ projects = get_projects(investor)
+ option_projects = filter(project -> in(typeof(project), leaftypes_option), projects)
+
+ return option_projects
+end
+
+function get_retired(investor::Investor)
+ leaftypes_retired = leaftypes(Project{Retired})
+ projects = get_projects(investor)
+ retired_projects = filter(project -> in(typeof(project), leaftypes_retired), projects)
+
+ return retired_projects
+end
+
+function find_active_invested_projects(projects::Vector{<: Project})
+ leaftypes_option = leaftypes(Project{Option})
+ leaftypes_retired= leaftypes(Project{Retired})
+ active_invested_projects = filter(project -> (!in(typeof(project), leaftypes_option) && !in(typeof(project), leaftypes_retired) ), projects)
+
+ return active_invested_projects
+end
+
+function find_option_projects(projects::Vector{<: Project})
+ leaftypes_option = leaftypes(Project{Option})
+ option_projects = filter(project -> in(typeof(project), leaftypes_option), projects)
+
+ return option_projects
+end
+
+function set_market_prices!(investor::Investor,
+ market_prices::MarketPrices)
+ investor.market_prices = market_prices
+end
+
+function get_preference_multiplier(investor::Investor, tech::String, zone::String)
+ return investor.portfolio_preference_multipliers[(tech, zone)]
+end
+
+function set_preference_multiplier!(investor::Investor, tech::String, zone::String, iteration_year::Int64, value::Float64)
+ investor.portfolio_preference_multipliers[(tech, zone)][iteration_year] = value
+end
diff --git a/src/structs/InvestorBelief.jl b/src/structs/InvestorBelief.jl
index 3310c81..e88afc1 100644
--- a/src/structs/InvestorBelief.jl
+++ b/src/structs/InvestorBelief.jl
@@ -1,100 +1,100 @@
-""" This struct defines the necessary parameters for build a Kalman filter to predict
-and update investor expectations/beliefs about the future """
-
-mutable struct InvestorBelief
- process_covariance::AxisArrays.AxisArray{Float64, 1}
- measurement_covariance::AxisArrays.AxisArray{Float64, 1}
- state_transition_matrix::Matrix{Float64}
- state_measurement_matrix::Matrix{Float64}
-
- function InvestorBelief(process_covariance::AxisArrays.AxisArray{Float64, 1},
- measurement_covariance::AxisArrays.AxisArray{Float64, 1},
- state_transition_matrix::Matrix{Float64},
- state_measurement_matrix::Matrix{Float64}
- )
- n = size(process_covariance, 1)
- (m, ) = size(state_measurement_matrix)
- @assert size(state_transition_matrix) == (n, n)
- @assert size(state_measurement_matrix) == (m, n)
- @assert size(measurement_covariance) == (n,)
-
- new(process_covariance,
- measurement_covariance,
- state_transition_matrix,
- state_measurement_matrix,
- )
- end
-end
-
-function InvestorBelief(process_covariance::AxisArrays.AxisArray{Float64, 1},
- measurement_covariance::AxisArrays.AxisArray{Float64, 1}
-)
-
-
- n = size(process_covariance, 1)
- state_transition_matrix = Matrix(1.0*LinearAlgebra.I, n, n)
- state_measurement_matrix = Matrix(1.0*LinearAlgebra.I, n, n)
-
- return InvestorBelief(process_covariance,
- measurement_covariance,
- state_transition_matrix,
- state_measurement_matrix,
- )
-end
-
-
-get_process_covariance(v::InvestorBelief) = v.process_covariance
-get_measurement_covariance(v::InvestorBelief) = v.measurement_covariance
-get_state_transition_matrix(v::InvestorBelief) = v.state_transition_matrix
-get_state_measurement_matrix(v::InvestorBelief) = v.state_measurement_matrix
-get_n(v::InvestorBelief) = size(v.process_covariance, 1)
-get_m(v::InvestorBelief) = size(v.state_measurement_matrix, 1)
-
-
-mutable struct KalmanFilter
- investor_data::InvestorBelief
- state_estimate::AxisArrays.AxisArray{Float64, 1}
- error_covariance_estimate::AxisArrays.AxisArray{Float64, 2}
-end
-
-get_investor_data(v::KalmanFilter) = v.investor_data
-get_state_estimate(v::KalmanFilter) = v.state_estimate
-get_error_covariance_estimate(v::KalmanFilter) = v.error_covariance_estimate
-
-set_state_estimate(v::KalmanFilter, value) = v.state_estimate = value
-set_error_covariance_estimate(v::KalmanFilter, value) = v.error_covariance_estimate = value
-
-function predict(kf::KalmanFilter)
-
- investor_data = kf.investor_data
- predicted_state = investor_data.state_transition_matrix * kf.state_estimate
- predicted_error_covariance = ((investor_data.state_transition_matrix * kf.error_covariance_estimate
- * investor_data.state_transition_matrix' ) + LinearAlgebra.Diagonal(investor_data.process_covariance))
-
- return (predicted_state, predicted_error_covariance)
-end
-
-function update_belief!(kf::KalmanFilter, measurement::AxisArrays.AxisArray{Float64, 1})
- investor_data = kf.investor_data
- n = get_n(investor_data)
- state_estimate_priori, error_covariance_priori = predict(kf)
- kalman_gain = ( (error_covariance_priori * investor_data.state_measurement_matrix) /
- (investor_data.state_measurement_matrix * error_covariance_priori * (investor_data.state_measurement_matrix)' + LinearAlgebra.Diagonal(investor_data.measurement_covariance)))
-
- state_estimate_posteriori = state_estimate_priori + kalman_gain * (measurement - investor_data.state_measurement_matrix * state_estimate_priori)
-
- error_covariance_posteriori = (LinearAlgebra.I(n) - kalman_gain * investor_data.state_measurement_matrix) * error_covariance_priori
-
- param_names = AxisArrays.axisvalues(investor_data.measurement_covariance)[1]
-
- state_estimate_posteriori = AxisArrays.AxisArray(state_estimate_posteriori, param_names)
- error_covariance_posteriori = AxisArrays.AxisArray(error_covariance_posteriori, param_names, param_names)
-
- set_state_estimate(kf, state_estimate_posteriori)
- set_error_covariance_estimate(kf, error_covariance_posteriori)
- return
-end
-
-function update_belief!(kf::Nothing, measurement::AxisArrays.AxisArray{Float64, 1})
- return
-end
+""" This struct defines the necessary parameters for build a Kalman filter to predict
+and update investor expectations/beliefs about the future """
+
+mutable struct InvestorBelief
+ process_covariance::AxisArrays.AxisArray{Float64, 1}
+ measurement_covariance::AxisArrays.AxisArray{Float64, 1}
+ state_transition_matrix::Matrix{Float64}
+ state_measurement_matrix::Matrix{Float64}
+
+ function InvestorBelief(process_covariance::AxisArrays.AxisArray{Float64, 1},
+ measurement_covariance::AxisArrays.AxisArray{Float64, 1},
+ state_transition_matrix::Matrix{Float64},
+ state_measurement_matrix::Matrix{Float64}
+ )
+ n = size(process_covariance, 1)
+ (m, ) = size(state_measurement_matrix)
+ @assert size(state_transition_matrix) == (n, n)
+ @assert size(state_measurement_matrix) == (m, n)
+ @assert size(measurement_covariance) == (n,)
+
+ new(process_covariance,
+ measurement_covariance,
+ state_transition_matrix,
+ state_measurement_matrix,
+ )
+ end
+end
+
+function InvestorBelief(process_covariance::AxisArrays.AxisArray{Float64, 1},
+ measurement_covariance::AxisArrays.AxisArray{Float64, 1}
+)
+
+
+ n = size(process_covariance, 1)
+ state_transition_matrix = Matrix(1.0*LinearAlgebra.I, n, n)
+ state_measurement_matrix = Matrix(1.0*LinearAlgebra.I, n, n)
+
+ return InvestorBelief(process_covariance,
+ measurement_covariance,
+ state_transition_matrix,
+ state_measurement_matrix,
+ )
+end
+
+
+get_process_covariance(v::InvestorBelief) = v.process_covariance
+get_measurement_covariance(v::InvestorBelief) = v.measurement_covariance
+get_state_transition_matrix(v::InvestorBelief) = v.state_transition_matrix
+get_state_measurement_matrix(v::InvestorBelief) = v.state_measurement_matrix
+get_n(v::InvestorBelief) = size(v.process_covariance, 1)
+get_m(v::InvestorBelief) = size(v.state_measurement_matrix, 1)
+
+
+mutable struct KalmanFilter
+ investor_data::InvestorBelief
+ state_estimate::AxisArrays.AxisArray{Float64, 1}
+ error_covariance_estimate::AxisArrays.AxisArray{Float64, 2}
+end
+
+get_investor_data(v::KalmanFilter) = v.investor_data
+get_state_estimate(v::KalmanFilter) = v.state_estimate
+get_error_covariance_estimate(v::KalmanFilter) = v.error_covariance_estimate
+
+set_state_estimate(v::KalmanFilter, value) = v.state_estimate = value
+set_error_covariance_estimate(v::KalmanFilter, value) = v.error_covariance_estimate = value
+
+function predict(kf::KalmanFilter)
+
+ investor_data = kf.investor_data
+ predicted_state = investor_data.state_transition_matrix * kf.state_estimate
+ predicted_error_covariance = ((investor_data.state_transition_matrix * kf.error_covariance_estimate
+ * investor_data.state_transition_matrix' ) + LinearAlgebra.Diagonal(investor_data.process_covariance))
+
+ return (predicted_state, predicted_error_covariance)
+end
+
+function update_belief!(kf::KalmanFilter, measurement::AxisArrays.AxisArray{Float64, 1})
+ investor_data = kf.investor_data
+ n = get_n(investor_data)
+ state_estimate_priori, error_covariance_priori = predict(kf)
+ kalman_gain = ( (error_covariance_priori * investor_data.state_measurement_matrix) /
+ (investor_data.state_measurement_matrix * error_covariance_priori * (investor_data.state_measurement_matrix)' + LinearAlgebra.Diagonal(investor_data.measurement_covariance)))
+
+ state_estimate_posteriori = state_estimate_priori + kalman_gain * (measurement - investor_data.state_measurement_matrix * state_estimate_priori)
+
+ error_covariance_posteriori = (LinearAlgebra.I(n) - kalman_gain * investor_data.state_measurement_matrix) * error_covariance_priori
+
+ param_names = AxisArrays.axisvalues(investor_data.measurement_covariance)[1]
+
+ state_estimate_posteriori = AxisArrays.AxisArray(state_estimate_posteriori, param_names)
+ error_covariance_posteriori = AxisArrays.AxisArray(error_covariance_posteriori, param_names, param_names)
+
+ set_state_estimate(kf, state_estimate_posteriori)
+ set_error_covariance_estimate(kf, error_covariance_posteriori)
+ return
+end
+
+function update_belief!(kf::Nothing, measurement::AxisArrays.AxisArray{Float64, 1})
+ return
+end
diff --git a/src/structs/MarketPrices.jl b/src/structs/MarketPrices.jl
index 312a767..499f7f4 100644
--- a/src/structs/MarketPrices.jl
+++ b/src/structs/MarketPrices.jl
@@ -1,70 +1,89 @@
-"""
-This struct holds the market price data for each product.
- energy_price: Dictionary of zonal energy prices with hourly resolution for each scenario.
- reserve_price: Dictionary of reserve prices with hourly resolution for each scenario.
- capacity_price: Dictionary of annual capacity prices for each scenario.
- rec_price: Dictionary of annual REC prices for each scenario.
-"""
-
-mutable struct MarketPrices
- energy_price::Union{Nothing, Dict{String, AxisArrays.AxisArray{Float64, 3}}}
- reserve_price::Union{Nothing, Dict{String, Dict{String, Array{Float64, 2}}}}
- capacity_price::Union{Nothing, Dict{String, AxisArrays.AxisArray{Float64, 1}}}
- rec_price::Union{Nothing, Dict{String, AxisArrays.AxisArray{Float64, 1}}}
-end
-
-function MarketPrices()
- return MarketPrices(nothing, nothing, nothing, nothing)
-end
-
-get_energy_price(prices::MarketPrices) = prices.energy_price
-get_reserve_price(prices::MarketPrices) = prices.reserve_price
-get_capacity_price(prices::MarketPrices) = prices.capacity_price
-get_rec_price(prices::MarketPrices) = prices.rec_price
-
-get_prices(prices::MarketPrices, prod::T) where T<: Product = nothing
-get_prices(prices::MarketPrices, prod::Energy) = get_energy_price(prices)
-
-function get_prices(prices::MarketPrices, prod::Union{OperatingReserve{ReserveUpEMIS}, OperatingReserve{ReserveDownEMIS}})
- product_name = get_name(prod)
- price_data = get_reserve_price(prices)[String(product_name)]
- return price_data
-end
-
-get_prices(prices::MarketPrices, prod::Capacity) = get_capacity_price(prices)
-get_prices(prices::MarketPrices, prod::REC) = get_rec_price(prices)
-
-function set_energy_price!(prices::MarketPrices, scenario_name::String, energy_price::AxisArrays.AxisArray{Float64, 3})
- if !isnothing(prices.energy_price)
- prices.energy_price[scenario_name] = energy_price
- else
- prices.energy_price = Dict(scenario_name => energy_price)
- end
- return
-end
-
-function set_reserve_price!(prices::MarketPrices, scenario_name::String, reserve_price::Dict{String, Array{Float64, 2}})
-
- reserve_price_dict = Dict(product => Dict(scenario_name => reserve_price[product]) for product in keys(reserve_price))
- prices.reserve_price = reserve_price_dict
-
- return
-end
-
-function set_capacity_price!(prices::MarketPrices, scenario_name::String, capacity_price::AxisArrays.AxisArray{Float64, 1})
- if !isnothing(prices.capacity_price)
- prices.capacity_price[scenario_name] = capacity_price
- else
- prices.capacity_price = Dict(scenario_name => capacity_price)
- end
- return
-end
-
-function set_rec_price!(prices::MarketPrices, scenario_name::String, rec_price::AxisArrays.AxisArray{Float64, 1})
- if !isnothing(prices.rec_price)
- prices.rec_price[scenario_name] = rec_price
- else
- prices.rec_price = Dict(scenario_name => rec_price)
- end
- return
-end
+"""
+This struct holds the market price data for each product.
+ energy_price: Dictionary of zonal energy prices with hourly resolution for each scenario.
+ reserve_price: Dictionary of reserve prices with hourly resolution for each scenario.
+ capacity_price: Dictionary of annual capacity prices for each scenario.
+ rec_price: Dictionary of annual REC prices for each scenario.
+"""
+
+mutable struct MarketPrices
+ energy_price::Union{Nothing, Dict{String, AxisArrays.AxisArray{Float64, 3}}}
+ reserve_price::Union{Nothing, Dict{String, Dict{String, Array{Float64, 2}}}}
+ capacity_price::Union{Nothing, Dict{String, AxisArrays.AxisArray{Float64, 1}}}
+ rec_price::Union{Nothing, Dict{String, AxisArrays.AxisArray{Float64, 1}}}
+ inertia_price::Union{Nothing, Dict{String, AxisArrays.AxisArray{Float64, 2}}}
+end
+
+function MarketPrices()
+ return MarketPrices(nothing, nothing, nothing, nothing, nothing)
+end
+
+get_energy_price(prices::MarketPrices) = prices.energy_price
+get_reserve_price(prices::MarketPrices) = prices.reserve_price
+get_capacity_price(prices::MarketPrices) = prices.capacity_price
+get_rec_price(prices::MarketPrices) = prices.rec_price
+get_inertia_price(prices::MarketPrices) = prices.inertia_price
+
+get_prices(prices::MarketPrices, prod::T) where T<: Product = nothing
+get_prices(prices::MarketPrices, prod::Energy) = get_energy_price(prices)
+
+function get_prices(prices::MarketPrices, prod::Union{OperatingReserve{ReserveUpEMIS}, OperatingReserve{ReserveDownEMIS}})
+ product_name = get_name(prod)
+ price_data = get_reserve_price(prices)[String(product_name)]
+ return price_data
+end
+
+get_prices(prices::MarketPrices, prod::Capacity) = get_capacity_price(prices)
+get_prices(prices::MarketPrices, prod::REC) = get_rec_price(prices)
+get_prices(prices::MarketPrices, prod::Inertia) = get_inertia_price(prices)
+
+
+function set_energy_price!(prices::MarketPrices, scenario_name::String, energy_price::AxisArrays.AxisArray{Float64, 3})
+ if !isnothing(prices.energy_price)
+ prices.energy_price[scenario_name] = energy_price
+ else
+ prices.energy_price = Dict(scenario_name => energy_price)
+ end
+ return
+end
+
+function set_reserve_price!(prices::MarketPrices, scenario_name::String, reserve_price::Dict{String, Array{Float64, 2}})
+
+ if isnothing(prices.reserve_price)
+ reserve_price_dict = Dict(product => Dict(scenario_name => reserve_price[product]) for product in keys(reserve_price))
+ prices.reserve_price = reserve_price_dict
+ else
+ for product in keys(reserve_price)
+ prices.reserve_price[product][scenario_name] = reserve_price[product]
+ end
+ end
+
+ return
+end
+
+function set_capacity_price!(prices::MarketPrices, scenario_name::String, capacity_price::AxisArrays.AxisArray{Float64, 1})
+ if !isnothing(prices.capacity_price)
+ prices.capacity_price[scenario_name] = capacity_price
+ else
+ prices.capacity_price = Dict(scenario_name => capacity_price)
+ end
+ return
+end
+
+function set_rec_price!(prices::MarketPrices, scenario_name::String, rec_price::AxisArrays.AxisArray{Float64, 1})
+ if !isnothing(prices.rec_price)
+ prices.rec_price[scenario_name] = rec_price
+ else
+ prices.rec_price = Dict(scenario_name => rec_price)
+ end
+ return
+end
+
+function set_inertia_price!(prices::MarketPrices, scenario_name::String, inertia_price::AxisArrays.AxisArray{Float64, 2})
+ if !isnothing(prices.inertia_price)
+ prices.inertia_price[scenario_name] = inertia_price
+ else
+ prices.inertia_price = Dict(scenario_name => inertia_price)
+ end
+ return
+end
diff --git a/src/structs/ResourceAdequacy.jl b/src/structs/ResourceAdequacy.jl
new file mode 100644
index 0000000..2358857
--- /dev/null
+++ b/src/structs/ResourceAdequacy.jl
@@ -0,0 +1,20 @@
+mutable struct ResourceAdequacy
+ targets :: Dict{String, Float64}
+ delta_irm :: Vector{Float64}
+ metrics :: Vector{Dict{String, Float64}}
+end
+
+get_targets(ra::ResourceAdequacy) = ra.targets
+get_delta_irm(ra::ResourceAdequacy) = ra.delta_irm
+get_delta_irm(ra::ResourceAdequacy, iteration_year::Int64) = ra.delta_irm[iteration_year]
+get_metrics(ra::ResourceAdequacy) = ra.metrics
+get_metrics(ra::ResourceAdequacy, iteration_year::Int64) = ra.metrics[iteration_year]
+
+
+function set_delta_irm!(ra::ResourceAdequacy, iteration_year::Int64, value::Float64)
+ ra.delta_irm[iteration_year] = value
+end
+
+function set_metrics!(ra::ResourceAdequacy, iteration_year::Int64, value::Dict{String, Float64})
+ ra.metrics[iteration_year] = value
+end
diff --git a/src/structs/RiskPreference.jl b/src/structs/RiskPreference.jl
index 71b73a6..b6f5691 100644
--- a/src/structs/RiskPreference.jl
+++ b/src/structs/RiskPreference.jl
@@ -1,22 +1,22 @@
-# Investor's Risk Preference
-abstract type RiskPreference end
-
-"""
-Risk Neutrality
-"""
-struct RiskNeutral <: RiskPreference end
-
-"""
-This struct contains the parameters for modeling Risk Aversion,
- where a project utility is calculated as follows:
- U = constant - multiplier * e ^ (- risk_coefficient ^ NPV)
-"""
-struct RiskAverse <: RiskPreference
- constant::Float64
- multiplier::Float64
- risk_coefficient::Float64
-end
-
-get_risk_coefficient(preference::RiskAverse) = preference.risk_coefficient
-get_constant(preference::RiskAverse) = preference.constant
-get_multiplier(preference::RiskAverse) = preference.multiplier
+# Investor's Risk Preference
+abstract type RiskPreference end
+
+"""
+Risk Neutrality
+"""
+struct RiskNeutral <: RiskPreference end
+
+"""
+This struct contains the parameters for modeling Risk Aversion,
+ where a project utility is calculated as follows:
+ U = constant - multiplier * e ^ (- risk_coefficient ^ NPV)
+"""
+struct RiskAverse <: RiskPreference
+ constant::Float64
+ multiplier::Float64
+ risk_coefficient::Float64
+end
+
+get_risk_coefficient(preference::RiskAverse) = preference.risk_coefficient
+get_constant(preference::RiskAverse) = preference.constant
+get_multiplier(preference::RiskAverse) = preference.multiplier
diff --git a/src/structs/Scenario.jl b/src/structs/Scenario.jl
index 1560740..88981a0 100644
--- a/src/structs/Scenario.jl
+++ b/src/structs/Scenario.jl
@@ -1,22 +1,22 @@
-"""
-This struct contains the data for each scenario.
- name: Scenario name
- probability: Probability of the scenario
- parameter_multipliers: Parameter multipliers for each scenario - to be multiplied to updated parameters from kalman filters
- parameter_values: Dictionary containing parameter values for the scenario
-"""
-mutable struct Scenario
- name::String
- probability::Float64
- parameter_multipliers::Union{Nothing, Dict{String, Float64}}
- parameter_values::Vector{AxisArrays.AxisArray{Float64, 2}}
-end
-
-get_name(scenario::Scenario) = scenario.name
-get_probability(scenario::Scenario) = scenario.probability
-get_parameter_multipliers(scenario::Scenario) = scenario.parameter_multipliers
-get_parameter_values(scenario::Scenario) = scenario.parameter_values
-
-function set_parameter_values!(scenario::Scenario, iteration_year::Int64, parameter_values::AxisArrays.AxisArray{Float64, 2})
- scenario.parameter_values[iteration_year] = parameter_values
-end
+"""
+This struct contains the data for each scenario.
+ name: Scenario name
+ probability: Probability of the scenario
+ parameter_multipliers: Parameter multipliers for each scenario - to be multiplied to updated parameters from kalman filters
+ parameter_values: Dictionary containing parameter values for the scenario
+"""
+mutable struct Scenario
+ name::String
+ probability::Float64
+ parameter_multipliers::Union{Nothing, Dict{String, Float64}}
+ parameter_values::Vector{AxisArrays.AxisArray{Float64, 2}}
+end
+
+get_name(scenario::Scenario) = scenario.name
+get_probability(scenario::Scenario) = scenario.probability
+get_parameter_multipliers(scenario::Scenario) = scenario.parameter_multipliers
+get_parameter_values(scenario::Scenario) = scenario.parameter_values
+
+function set_parameter_values!(scenario::Scenario, iteration_year::Int64, parameter_values::AxisArrays.AxisArray{Float64, 2})
+ scenario.parameter_values[iteration_year] = parameter_values
+end
diff --git a/src/structs/ZonalLine.jl b/src/structs/ZonalLine.jl
index ac2a623..d1d7056 100644
--- a/src/structs/ZonalLine.jl
+++ b/src/structs/ZonalLine.jl
@@ -1,23 +1,23 @@
-"""
-This struct contains all the data for the transmission lines between zones.
- name: Path to the directory where input data is stored.
- from_zone: Name of originating zone.
- to_zone: Name of destination zone.
- active_power_limit: Maximum flow capacity (MW).
-"""
-mutable struct ZonalLine
- name::String
- from_zone::String
- to_zone::String
- active_power_limit::Float64
-end
-
-get_name(line::ZonalLine) = line.name
-get_from_zone(line::ZonalLine) = line.from_zone
-get_to_zone(line::ZonalLine) = line.to_zone
-get_active_power_limit(line::ZonalLine) = line.active_power_limit
-
-function set_active_power_limit!(line::ZonalLine, active_power_limit::Float64)
- line.active_power_limit = active_power_limit
- return
-end
+"""
+This struct contains all the data for the transmission lines between zones.
+ name: Path to the directory where input data is stored.
+ from_zone: Name of originating zone.
+ to_zone: Name of destination zone.
+ active_power_limit: Maximum flow capacity (MW).
+"""
+mutable struct ZonalLine
+ name::String
+ from_zone::String
+ to_zone::String
+ active_power_limit::Float64
+end
+
+get_name(line::ZonalLine) = line.name
+get_from_zone(line::ZonalLine) = line.from_zone
+get_to_zone(line::ZonalLine) = line.to_zone
+get_active_power_limit(line::ZonalLine) = line.active_power_limit
+
+function set_active_power_limit!(line::ZonalLine, active_power_limit::Float64)
+ line.active_power_limit = active_power_limit
+ return
+end
diff --git a/src/structs/devices/BatteryEMIS.jl b/src/structs/devices/BatteryEMIS.jl
index 0f26633..1b84cce 100644
--- a/src/structs/devices/BatteryEMIS.jl
+++ b/src/structs/devices/BatteryEMIS.jl
@@ -1,58 +1,60 @@
-"""
-This struct contains the technical data of a battery.
- type: Battery technology type.
- input_active_power_limits: Named tuple of minimum and maximum input power limits.
- output_active_power_limits: Named tuple of minimum and maximum output power limits.
- ramp_limits: Ramp limits (MW/hr).
- storage_capacity: Named tuple of minimum and maximum storage capacity.
- soc: State of charge - Important for first time-step of the iteration.
- efficiency: Charging and discharging efficiencies.
- bus: Name of bus at which the battery is located.
- zone: Name of zone in which the battery is located.
-"""
-
-struct BatteryTech <: Tech
- type::String
- input_active_power_limits::NamedTuple{(:min, :max),Tuple{Float64,Float64}}
- output_active_power_limits::NamedTuple{(:min, :max),Tuple{Float64,Float64}}
- ramp_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}
- storage_capacity::NamedTuple{(:min, :max),Tuple{Float64,Float64}}
- soc::Float64
- efficiency::NamedTuple{(:in, :out), Tuple{Float64, Float64}}
- bus::Int64
- zone::String
- FOR::Float64
-end
-
-get_type(tech::BatteryTech) = tech.type
-get_input_active_power_limits(tech::BatteryTech) = tech.input_active_power_limits
-get_output_active_power_limits(tech::BatteryTech) = tech.output_active_power_limits
-get_ramp_limits(tech::BatteryTech) = tech.ramp_limits
-get_storage_capacity(tech::BatteryTech) = tech.storage_capacity
-get_soc(tech::BatteryTech) = tech.soc
-get_efficiency(tech::BatteryTech) = tech.efficiency
-get_bus(tech::BatteryTech) = tech.bus
-get_zone(tech::BatteryTech) = tech.zone
-get_FOR(tech::BatteryTech) = tech.FOR
-
-"""
-This struct contains all the data for battery storage.
- name: Battery's name.
- tech: Struct containing technical data.
- decision_year: The year in which the investment decision is made.
- construction_year: The year in which construction is completed.
- retirement_year: The year in which the generator is retired.
- end_life_year: Age-based retirement year.
- products: Vector containing the data of all products provided by the generator.
- finance_data: Struct containing financial data.
-"""
-mutable struct BatteryEMIS{T <: BuildPhase} <: StorageEMIS{T}
- name::String
- tech::BatteryTech
- decision_year::Int64
- construction_year::Int64
- retirement_year::Int64
- end_life_year::Int64
- products::Vector{Product}
- finance_data::Finance
-end
+"""
+This struct contains the technical data of a battery.
+ type: Battery technology type.
+ input_active_power_limits: Named tuple of minimum and maximum input power limits.
+ output_active_power_limits: Named tuple of minimum and maximum output power limits.
+ ramp_limits: Ramp limits (MW/hr).
+ storage_capacity: Named tuple of minimum and maximum storage capacity.
+ soc: State of charge - Important for first time-step of the iteration.
+ efficiency: Charging and discharging efficiencies.
+ bus: Name of bus at which the battery is located.
+ zone: Name of zone in which the battery is located.
+"""
+
+struct BatteryTech <: Tech
+ type::String
+ input_active_power_limits::NamedTuple{(:min, :max),Tuple{Float64,Float64}}
+ output_active_power_limits::NamedTuple{(:min, :max),Tuple{Float64,Float64}}
+ ramp_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}
+ storage_capacity::NamedTuple{(:min, :max),Tuple{Float64,Float64}}
+ soc::Float64
+ efficiency::NamedTuple{(:in, :out), Tuple{Float64, Float64}}
+ bus::Int64
+ zone::String
+ FOR::Float64
+ MTTR::Int64
+end
+
+get_type(tech::BatteryTech) = tech.type
+get_input_active_power_limits(tech::BatteryTech) = tech.input_active_power_limits
+get_output_active_power_limits(tech::BatteryTech) = tech.output_active_power_limits
+get_ramp_limits(tech::BatteryTech) = tech.ramp_limits
+get_storage_capacity(tech::BatteryTech) = tech.storage_capacity
+get_soc(tech::BatteryTech) = tech.soc
+get_efficiency(tech::BatteryTech) = tech.efficiency
+get_bus(tech::BatteryTech) = tech.bus
+get_zone(tech::BatteryTech) = tech.zone
+get_FOR(tech::BatteryTech) = tech.FOR
+get_MTTR(tech::BatteryTech) = tech.MTTR
+
+"""
+This struct contains all the data for battery storage.
+ name: Battery's name.
+ tech: Struct containing technical data.
+ decision_year: The year in which the investment decision is made.
+ construction_year: The year in which construction is completed.
+ retirement_year: The year in which the generator is retired.
+ end_life_year: Age-based retirement year.
+ products: Vector containing the data of all products provided by the generator.
+ finance_data: Struct containing financial data.
+"""
+mutable struct BatteryEMIS{T <: BuildPhase} <: StorageEMIS{T}
+ name::String
+ tech::BatteryTech
+ decision_year::Int64
+ construction_year::Int64
+ retirement_year::Int64
+ end_life_year::Int64
+ products::Vector{Product}
+ finance_data::Finance
+end
diff --git a/src/structs/devices/BuildPhase.jl b/src/structs/devices/BuildPhase.jl
index 6dd0a3c..9fd0993 100644
--- a/src/structs/devices/BuildPhase.jl
+++ b/src/structs/devices/BuildPhase.jl
@@ -1,7 +1,7 @@
-abstract type BuildPhase end
-
-abstract type Existing <: BuildPhase end #Existing projects.
-abstract type Planned <: BuildPhase end #Projects under construction.
-abstract type Queue <: BuildPhase end #Projects in queue.
-abstract type Option <: BuildPhase end #Projects which can be built.
-abstract type Retired <: BuildPhase end #Retired projects.
+abstract type BuildPhase end
+
+abstract type Existing <: BuildPhase end #Existing projects.
+abstract type Planned <: BuildPhase end #Projects under construction.
+abstract type Queue <: BuildPhase end #Projects in queue.
+abstract type Option <: BuildPhase end #Projects which can be built.
+abstract type Retired <: BuildPhase end #Retired projects.
diff --git a/src/structs/devices/HydroGenEMIS.jl b/src/structs/devices/HydroGenEMIS.jl
index bd784ba..f3d2f87 100644
--- a/src/structs/devices/HydroGenEMIS.jl
+++ b/src/structs/devices/HydroGenEMIS.jl
@@ -1,51 +1,53 @@
-"""
-This struct contains the technical data of a hydropower generator.
- type: Generator technology type.
- active_power_limits: Named tuple of minimum and maximum generation limits.
- ramp_limits: Ramp limits (MW/hr).
- time_limits: Minimum up and Minimum down time limits (hr).
- operation_cost: Operation cost based on PowerSystems.jl definitions.
- bus: Name of the bus on which the generator is located.
- zone: Name of zone in which the generator is located.
-"""
-struct HydroTech <: Tech
- type::String
- active_power_limits::NamedTuple{(:min, :max),Tuple{Float64,Float64}}
- ramp_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}
- time_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}
- operation_cost::Union{Nothing,PSY.TwoPartCost}
- bus::Int64
- zone::String
- FOR::Float64
-end
-
-get_type(tech::HydroTech) = tech.type
-get_active_power_limits(tech::HydroTech) = tech.active_power_limits
-get_time_limits(tech::HydroTech) = tech.time_limits
-get_ramp_limits(tech::HydroTech) = tech.ramp_limits
-get_operation_cost(tech::HydroTech) = tech.operation_cost
-get_bus(tech::HydroTech) = tech.bus
-get_zone(tech::HydroTech) = tech.zone
-get_FOR(tech::HydroTech) = tech.FOR
-
-"""
-This struct contains all the data for a hydropower generator.
- name: Generator's name.
- tech: Struct containing technical data.
- decision_year: The year in which the investment decision is made.
- construction_year: The year in which construction is completed.
- retirement_year: The year in which the generator is retired.
- end_life_year: Age-based retirement year.
- products: Vector containing the data of all products provided by the generator.
- finance_data: Struct containing financial data.
-"""
-mutable struct HydroGenEMIS{T <: BuildPhase} <: GeneratorEMIS{T}
- name::String
- tech::HydroTech
- decision_year::Int64
- construction_year::Int64
- retirement_year::Int64
- end_life_year::Int64
- products::Vector{Product}
- finance_data::Finance
-end
+"""
+This struct contains the technical data of a hydropower generator.
+ type: Generator technology type.
+ active_power_limits: Named tuple of minimum and maximum generation limits.
+ ramp_limits: Ramp limits (MW/hr).
+ time_limits: Minimum up and Minimum down time limits (hr).
+ operation_cost: Operation cost based on PowerSystems.jl definitions.
+ bus: Name of the bus on which the generator is located.
+ zone: Name of zone in which the generator is located.
+"""
+struct HydroTech <: Tech
+ type::String
+ active_power_limits::NamedTuple{(:min, :max),Tuple{Float64,Float64}}
+ ramp_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}
+ time_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}
+ operation_cost::Union{Nothing,PSY.TwoPartCost}
+ bus::Int64
+ zone::String
+ FOR::Float64
+ MTTR::Int64
+end
+
+get_type(tech::HydroTech) = tech.type
+get_active_power_limits(tech::HydroTech) = tech.active_power_limits
+get_time_limits(tech::HydroTech) = tech.time_limits
+get_ramp_limits(tech::HydroTech) = tech.ramp_limits
+get_operation_cost(tech::HydroTech) = tech.operation_cost
+get_bus(tech::HydroTech) = tech.bus
+get_zone(tech::HydroTech) = tech.zone
+get_FOR(tech::HydroTech) = tech.FOR
+get_MTTR(tech::HydroTech) = tech.MTTR
+
+"""
+This struct contains all the data for a hydropower generator.
+ name: Generator's name.
+ tech: Struct containing technical data.
+ decision_year: The year in which the investment decision is made.
+ construction_year: The year in which construction is completed.
+ retirement_year: The year in which the generator is retired.
+ end_life_year: Age-based retirement year.
+ products: Vector containing the data of all products provided by the generator.
+ finance_data: Struct containing financial data.
+"""
+mutable struct HydroGenEMIS{T <: BuildPhase} <: GeneratorEMIS{T}
+ name::String
+ tech::HydroTech
+ decision_year::Int64
+ construction_year::Int64
+ retirement_year::Int64
+ end_life_year::Int64
+ products::Vector{Product}
+ finance_data::Finance
+end
diff --git a/src/structs/devices/Project.jl b/src/structs/devices/Project.jl
index c0a8d8c..cdacad9 100644
--- a/src/structs/devices/Project.jl
+++ b/src/structs/devices/Project.jl
@@ -1,64 +1,72 @@
-abstract type DeviceEMIS end
-
-abstract type Project{T <: BuildPhase} <: DeviceEMIS end #Projects, parameterized by buildphase
-abstract type GeneratorEMIS{T <: BuildPhase} <: Project{T} end #Generators - subtype of Projects
-abstract type StorageEMIS{T <: BuildPhase} <: Project{T} end #Storage - subtype of Projects
-
-abstract type Tech end
-
-get_name(project::P) where P <: Project{<: BuildPhase} = project.name
-get_tech(project::P) where P <: Project{<: BuildPhase} = project.tech
-get_decision_year(project::P) where P <: Project{<: BuildPhase} = project.decision_year
-get_construction_year(project::P) where P <: Project{<: BuildPhase} = project.construction_year
-get_retirement_year(project::P) where P <: Project{<: BuildPhase} = project.retirement_year
-get_end_life_year(project::P) where P <: Project{<: BuildPhase} = project.end_life_year
-get_finance_data(project::P) where P <: Project{<: BuildPhase} = project.finance_data
-get_products(project::P) where P <: Project{<: BuildPhase} = project.products
-get_mincap(project::P) where P <: GeneratorEMIS{<: BuildPhase} = get_active_power_limits(get_tech(project))[:min]
-get_maxcap(project::P) where P <: GeneratorEMIS{<: BuildPhase} = get_active_power_limits(get_tech(project))[:max]
-get_mincap(project::P) where P <: StorageEMIS{<: BuildPhase} = get_output_active_power_limits(get_tech(project))[:min]
-get_maxcap(project::P) where P <: StorageEMIS{<: BuildPhase} = get_output_active_power_limits(get_tech(project))[:max]
-
-get_operation_cost(tech::Tech) = nothing
-get_heat_rate_curve(tech::Tech) = nothing
-get_fuel_cost(tech::Tech) = 0.0
-
-function set_name!(project::P,
- name::String) where P <: Project{<: BuildPhase}
- project.name = name
- return
-end
-
-function set_decision_year!(project::P, year::Int64) where P <: Project{<: BuildPhase}
- project.decision_year = year
- return
-end
-
-function set_construction_year!(project::P, year::Int64) where P <: Project{<: BuildPhase}
- project.construction_year = year
- return
-end
-
-function set_retirement_year!(project::P, year) where P <: Project{<: BuildPhase}
- project.retirement_year = year
-end
-
-function set_end_life_year!(project::P, year::Int64) where P <: Project{<: BuildPhase}
- project.end_life_year = year
- return
-end
-
-function set_investment_cost!(project::P,
- investment_cost::Vector{Float64}) where P <: Project{<: BuildPhase}
- return
-end
-
-function set_investment_cost!(project::P,
- investment_cost::Vector{Float64}) where P <: Project{Option}
- project.finance_data.investment_cost = investment_cost
-end
-
-function set_effective_investment_cost!(project::P,
- investment_cost::Float64) where P <: Project{<: BuildPhase}
- project.finance_data.effective_investment_cost = investment_cost
-end
+abstract type DeviceEMIS end
+
+abstract type Project{T <: BuildPhase} <: DeviceEMIS end #Projects, parameterized by buildphase
+abstract type GeneratorEMIS{T <: BuildPhase} <: Project{T} end #Generators - subtype of Projects
+abstract type StorageEMIS{T <: BuildPhase} <: Project{T} end #Storage - subtype of Projects
+
+abstract type Tech end
+
+get_name(project::P) where P <: Project{<: BuildPhase} = project.name
+get_tech(project::P) where P <: Project{<: BuildPhase} = project.tech
+get_decision_year(project::P) where P <: Project{<: BuildPhase} = project.decision_year
+get_construction_year(project::P) where P <: Project{<: BuildPhase} = project.construction_year
+get_retirement_year(project::P) where P <: Project{<: BuildPhase} = project.retirement_year
+get_end_life_year(project::P) where P <: Project{<: BuildPhase} = project.end_life_year
+get_finance_data(project::P) where P <: Project{<: BuildPhase} = project.finance_data
+get_products(project::P) where P <: Project{<: BuildPhase} = project.products
+get_mincap(project::P) where P <: GeneratorEMIS{<: BuildPhase} = get_active_power_limits(get_tech(project))[:min]
+get_maxcap(project::P) where P <: GeneratorEMIS{<: BuildPhase} = get_active_power_limits(get_tech(project))[:max]
+get_mincap(project::P) where P <: StorageEMIS{<: BuildPhase} = get_output_active_power_limits(get_tech(project))[:min]
+get_maxcap(project::P) where P <: StorageEMIS{<: BuildPhase} = get_output_active_power_limits(get_tech(project))[:max]
+
+get_operation_cost(tech::Tech) = nothing
+get_heat_rate_curve(tech::Tech) = nothing
+get_fuel_cost(tech::Tech) = 0.0
+
+function set_name!(project::P,
+ name::String) where P <: Project{<: BuildPhase}
+ project.name = name
+ return
+end
+
+function set_decision_year!(project::P, year::Int64) where P <: Project{<: BuildPhase}
+ project.decision_year = year
+ return
+end
+
+function set_construction_year!(project::P, year::Int64) where P <: Project{<: BuildPhase}
+ project.construction_year = year
+ return
+end
+
+function set_retirement_year!(project::P, year) where P <: Project{<: BuildPhase}
+ project.retirement_year = year
+end
+
+function set_end_life_year!(project::P, year::Int64) where P <: Project{<: BuildPhase}
+ project.end_life_year = year
+ return
+end
+
+function set_investment_cost!(project::P,
+ investment_cost::Vector{Float64}) where P <: Project{<: BuildPhase}
+ return
+end
+
+function set_investment_cost!(project::P,
+ investment_cost::Vector{Float64}) where P <: Project{Option}
+ project.finance_data.investment_cost = investment_cost
+end
+
+function set_effective_investment_cost!(project::P,
+ investment_cost::Float64) where P <: Project{<: BuildPhase}
+ project.finance_data.effective_investment_cost = investment_cost
+end
+
+function get_project_preference_multiplier(project::Project)
+ return get_preference_multiplier(get_finance_data(project))
+end
+
+function set_preference_multiplier!(project::Project, iteration_year::Int64, value:: Float64)
+ project.finance_data.preference_multiplier[iteration_year] = value
+end
diff --git a/src/structs/devices/RenewableGenEMIS.jl b/src/structs/devices/RenewableGenEMIS.jl
index cb34e27..b75eb1f 100644
--- a/src/structs/devices/RenewableGenEMIS.jl
+++ b/src/structs/devices/RenewableGenEMIS.jl
@@ -1,48 +1,50 @@
-"""
-This struct contains the technical data of a renewable generator.
- type: Generator technology type.
- active_power_limits: Named tuple of minimum and maximum generation limits.
- ramp_limits: Ramp limits (MW/hr).
- operation_cost: Operation cost based on PowerSystems.jl definitions.
- bus: Name of the bus on which the generator is located.
- zone: Name of zone in which the generator is located.
-"""
-struct RenewableTech <: Tech
- type::String
- active_power_limits::NamedTuple{(:min, :max),Tuple{Float64,Float64}}
- ramp_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}
- operation_cost::Union{Nothing, PSY.TwoPartCost}
- bus::Int64
- zone::String
- FOR::Float64
-end
-
-get_type(tech::RenewableTech) = tech.type
-get_active_power_limits(tech::RenewableTech) = tech.active_power_limits
-get_ramp_limits(tech::RenewableTech) = tech.ramp_limits
-get_operation_cost(tech::RenewableTech) = tech.operation_cost
-get_bus(tech::RenewableTech) = tech.bus
-get_zone(tech::RenewableTech) = tech.zone
-get_FOR(tech::RenewableTech) = tech.FOR
-
-"""
-This struct contains all the data for a renewable generator.
- name: Generator's name.
- tech: Struct containing technical data.
- decision_year: The year in which the investment decision is made.
- construction_year: The year in which construction is completed.
- retirement_year: The year in which the generator is retired.
- end_life_year: Age-based retirement year.
- products: Vector containing the data of all products provided by the generator.
- finance_data: Struct containing financial data.
-"""
-mutable struct RenewableGenEMIS{T <: BuildPhase} <: GeneratorEMIS{T}
- name::String
- tech::RenewableTech
- decision_year::Int64
- construction_year::Int64
- retirement_year::Int64
- end_life_year::Int64
- products::Vector{Product}
- finance_data::Finance
-end
+"""
+This struct contains the technical data of a renewable generator.
+ type: Generator technology type.
+ active_power_limits: Named tuple of minimum and maximum generation limits.
+ ramp_limits: Ramp limits (MW/hr).
+ operation_cost: Operation cost based on PowerSystems.jl definitions.
+ bus: Name of the bus on which the generator is located.
+ zone: Name of zone in which the generator is located.
+"""
+struct RenewableTech <: Tech
+ type::String
+ active_power_limits::NamedTuple{(:min, :max),Tuple{Float64,Float64}}
+ ramp_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}
+ operation_cost::Union{Nothing, PSY.TwoPartCost}
+ bus::Int64
+ zone::String
+ FOR::Float64
+ MTTR::Int64
+end
+
+get_type(tech::RenewableTech) = tech.type
+get_active_power_limits(tech::RenewableTech) = tech.active_power_limits
+get_ramp_limits(tech::RenewableTech) = tech.ramp_limits
+get_operation_cost(tech::RenewableTech) = tech.operation_cost
+get_bus(tech::RenewableTech) = tech.bus
+get_zone(tech::RenewableTech) = tech.zone
+get_FOR(tech::RenewableTech) = tech.FOR
+get_MTTR(tech::RenewableTech) = tech.MTTR
+
+"""
+This struct contains all the data for a renewable generator.
+ name: Generator's name.
+ tech: Struct containing technical data.
+ decision_year: The year in which the investment decision is made.
+ construction_year: The year in which construction is completed.
+ retirement_year: The year in which the generator is retired.
+ end_life_year: Age-based retirement year.
+ products: Vector containing the data of all products provided by the generator.
+ finance_data: Struct containing financial data.
+"""
+mutable struct RenewableGenEMIS{T <: BuildPhase} <: GeneratorEMIS{T}
+ name::String
+ tech::RenewableTech
+ decision_year::Int64
+ construction_year::Int64
+ retirement_year::Int64
+ end_life_year::Int64
+ products::Vector{Product}
+ finance_data::Finance
+end
diff --git a/src/structs/devices/ThermalFastStartSIIP.jl b/src/structs/devices/ThermalFastStartSIIP.jl
new file mode 100644
index 0000000..19e935e
--- /dev/null
+++ b/src/structs/devices/ThermalFastStartSIIP.jl
@@ -0,0 +1,197 @@
+"""
+ mutable struct ThermalFastStartSIIP <: ThermalGen
+ name::String
+ available::Bool
+ status::Bool
+ bus::Bus
+ active_power::Float64
+ reactive_power::Float64
+ rating::Float64
+ active_power_limits::NamedTuple{(:min, :max), Tuple{Float64, Float64}}
+ reactive_power_limits::Union{Nothing, Min_Max}
+ ramp_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}
+ operation_cost::OperationalCost
+ base_power::Float64
+ time_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}
+ prime_mover::PrimeMovers
+ fuel::ThermalFuels
+ services::Vector{Service}
+ time_at_status::Float64
+ dynamic_injector::Union{Nothing, DynamicInjection}
+ ext::Dict{String, Any}
+ time_series_container::InfrastructureSystems.TimeSeriesContainer
+ internal::InfrastructureSystemsInternal
+ end
+Data Structure for thermal generation technologies.
+# Arguments
+- `name::String`
+- `available::Bool`
+- `status::Bool`
+- `bus::Bus`
+- `active_power::Float64`, validation range: `active_power_limits`, action if invalid: `warn`
+- `reactive_power::Float64`, validation range: `reactive_power_limits`, action if invalid: `warn`
+- `rating::Float64`: Thermal limited MVA Power Output of the unit. <= Capacity, validation range: `(0, nothing)`, action if invalid: `error`
+- `active_power_limits::NamedTuple{(:min, :max), Tuple{Float64, Float64}}`, validation range: `(0, nothing)`, action if invalid: `warn`
+- `reactive_power_limits::Union{Nothing, Min_Max}`
+- `ramp_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}`: ramp up and ramp down limits in MW (in component base per unit) per minute, validation range: `(0, nothing)`, action if invalid: `error`
+- `operation_cost::OperationalCost`
+- `base_power::Float64`: Base power of the unit in MVA, validation range: `(0, nothing)`, action if invalid: `warn`
+- `time_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}`: Minimum up and Minimum down time limits in hours, validation range: `(0, nothing)`, action if invalid: `error`
+- `prime_mover::PrimeMovers`: Prime mover technology according to EIA 923
+- `fuel::ThermalFuels`: Prime mover fuel according to EIA 923
+- `services::Vector{Service}`: Services that this device contributes to
+- `time_at_status::Float64`
+- `dynamic_injector::Union{Nothing, DynamicInjection}`: corresponding dynamic injection device
+- `ext::Dict{String, Any}`
+- `time_series_container::InfrastructureSystems.TimeSeriesContainer`: internal time_series storage
+- `internal::InfrastructureSystemsInternal`: power system internal reference, do not modify
+"""
+mutable struct ThermalFastStartSIIP <: PSY.ThermalGen
+ name::String
+ available::Bool
+ status::Bool
+ bus::PSY.Bus
+ active_power::Float64
+ reactive_power::Float64
+ "Thermal limited MVA Power Output of the unit. <= Capacity"
+ rating::Float64
+ active_power_limits::NamedTuple{(:min, :max), Tuple{Float64, Float64}}
+ reactive_power_limits::Union{Nothing, PSY.Min_Max}
+ "ramp up and ramp down limits in MW (in component base per unit) per minute"
+ ramp_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}
+ operation_cost::PSY.OperationalCost
+ "Base power of the unit in MVA"
+ base_power::Float64
+ "Minimum up and Minimum down time limits in hours"
+ time_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}
+ "Prime mover technology according to EIA 923"
+ prime_mover::PSY.PrimeMovers
+ "Prime mover fuel according to EIA 923"
+ fuel::PSY.ThermalFuels
+ "Services that this device contributes to"
+ services::Vector{PSY.Service}
+ time_at_status::Float64
+ "corresponding dynamic injection device"
+ dynamic_injector::Union{Nothing, PSY.DynamicInjection}
+ ext::Dict{String, Any}
+ "internal time_series storage"
+ time_series_container::InfrastructureSystems.TimeSeriesContainer
+ "power system internal reference, do not modify"
+ internal::InfrastructureSystems.InfrastructureSystemsInternal
+end
+
+function ThermalFastStartSIIP(name, available, status, bus, active_power, reactive_power, rating, active_power_limits, reactive_power_limits, ramp_limits, operation_cost, base_power, time_limits=nothing, prime_mover=PSY.PrimeMovers.OT, fuel=PSY.ThermalFuels.OTHER, services=PSY.Device[], time_at_status=PSY.INFINITE_TIME, dynamic_injector=nothing, ext=Dict{String, Any}(), time_series_container=InfrastructureSystems.TimeSeriesContainer(), )
+ ThermalFastStartSIIP(name, available, status, bus, active_power, reactive_power, rating, active_power_limits, reactive_power_limits, ramp_limits, operation_cost, base_power, time_limits, prime_mover, fuel, services, time_at_status, dynamic_injector, ext, time_series_container, InfrastructureSystemsInternal(), )
+end
+
+function ThermalFastStartSIIP(; name, available, status, bus, active_power, reactive_power, rating, active_power_limits, reactive_power_limits, ramp_limits, operation_cost, base_power, time_limits=nothing, prime_mover=PSY.PrimeMovers.OT, fuel=PSY.ThermalFuels.OTHER, services=PSY.Device[], time_at_status=PSY.INFINITE_TIME, dynamic_injector=nothing, ext=Dict{String, Any}(), time_series_container=InfrastructureSystems.TimeSeriesContainer(), internal=InfrastructureSystems.InfrastructureSystemsInternal(), )
+ ThermalFastStartSIIP(name, available, status, bus, active_power, reactive_power, rating, active_power_limits, reactive_power_limits, ramp_limits, operation_cost, base_power, time_limits, prime_mover, fuel, services, time_at_status, dynamic_injector, ext, time_series_container, internal, )
+end
+
+# Constructor for demo purposes; non-functional.
+function ThermalFastStartSIIP(::Nothing)
+ ThermalFastStartSIIP(;
+ name="init",
+ available=false,
+ status=false,
+ bus=Bus(nothing),
+ active_power=0.0,
+ reactive_power=0.0,
+ rating=0.0,
+ active_power_limits=(min=0.0, max=0.0),
+ reactive_power_limits=nothing,
+ ramp_limits=nothing,
+ operation_cost=ThreePartCost(nothing),
+ base_power=0.0,
+ time_limits=nothing,
+ prime_mover=PrimeMovers.OT,
+ fuel=ThermalFuels.OTHER,
+ services=Device[],
+ time_at_status=INFINITE_TIME,
+ dynamic_injector=nothing,
+ ext=Dict{String, Any}(),
+ time_series_container=InfrastructureSystems.TimeSeriesContainer(),
+ )
+end
+
+"""Get [`ThermalFastStartSIIP`](@ref) `name`."""
+PSY.get_name(value::ThermalFastStartSIIP) = value.name
+"""Get [`ThermalFastStartSIIP`](@ref) `available`."""
+PSY.get_available(value::ThermalFastStartSIIP) = value.available
+"""Get [`ThermalFastStartSIIP`](@ref) `status`."""
+PSY.get_status(value::ThermalFastStartSIIP) = value.status
+"""Get [`ThermalFastStartSIIP`](@ref) `bus`."""
+PSY.get_bus(value::ThermalFastStartSIIP) = value.bus
+"""Get [`ThermalFastStartSIIP`](@ref) `active_power`."""
+PSY.get_active_power(value::ThermalFastStartSIIP) = PSY.get_value(value, value.active_power)
+"""Get [`ThermalFastStartSIIP`](@ref) `reactive_power`."""
+PSY.get_reactive_power(value::ThermalFastStartSIIP) = PSY.get_value(value, value.reactive_power)
+"""Get [`ThermalFastStartSIIP`](@ref) `rating`."""
+PSY.get_rating(value::ThermalFastStartSIIP) = PSY.get_value(value, value.rating)
+"""Get [`ThermalFastStartSIIP`](@ref) `active_power_limits`."""
+PSY.get_active_power_limits(value::ThermalFastStartSIIP) = PSY.get_value(value, value.active_power_limits)
+"""Get [`ThermalFastStartSIIP`](@ref) `reactive_power_limits`."""
+PSY.get_reactive_power_limits(value::ThermalFastStartSIIP) = PSY.get_value(value, value.reactive_power_limits)
+"""Get [`ThermalFastStartSIIP`](@ref) `ramp_limits`."""
+PSY.get_ramp_limits(value::ThermalFastStartSIIP) = PSY.get_value(value, value.ramp_limits)
+"""Get [`ThermalFastStartSIIP`](@ref) `operation_cost`."""
+PSY.get_operation_cost(value::ThermalFastStartSIIP) = value.operation_cost
+"""Get [`ThermalFastStartSIIP`](@ref) `base_power`."""
+PSY.get_base_power(value::ThermalFastStartSIIP) = value.base_power
+"""Get [`ThermalFastStartSIIP`](@ref) `time_limits`."""
+PSY.get_time_limits(value::ThermalFastStartSIIP) = value.time_limits
+"""Get [`ThermalFastStartSIIP`](@ref) `prime_mover`."""
+PSY.get_prime_mover(value::ThermalFastStartSIIP) = value.prime_mover
+"""Get [`ThermalFastStartSIIP`](@ref) `fuel`."""
+PSY.get_fuel(value::ThermalFastStartSIIP) = value.fuel
+"""Get [`ThermalFastStartSIIP`](@ref) `services`."""
+PSY.get_services(value::ThermalFastStartSIIP) = value.services
+"""Get [`ThermalFastStartSIIP`](@ref) `time_at_status`."""
+PSY.get_time_at_status(value::ThermalFastStartSIIP) = value.time_at_status
+"""Get [`ThermalFastStartSIIP`](@ref) `dynamic_injector`."""
+PSY.get_dynamic_injector(value::ThermalFastStartSIIP) = value.dynamic_injector
+"""Get [`ThermalFastStartSIIP`](@ref) `ext`."""
+PSY.get_ext(value::ThermalFastStartSIIP) = value.ext
+"""Get [`ThermalFastStartSIIP`](@ref) `time_series_container`."""
+PSY.get_time_series_container(value::ThermalFastStartSIIP) = value.time_series_container
+"""Get [`ThermalFastStartSIIP`](@ref) `internal`."""
+PSY.get_internal(value::ThermalFastStartSIIP) = value.internal
+
+"""Set [`ThermalFastStartSIIP`](@ref) `name`."""
+PSY.set_name!(value::ThermalFastStartSIIP, val) = value.name = val
+"""Set [`ThermalFastStartSIIP`](@ref) `available`."""
+PSY.set_available!(value::ThermalFastStartSIIP, val) = value.available = val
+"""Set [`ThermalFastStartSIIP`](@ref) `status`."""
+PSY.set_status!(value::ThermalFastStartSIIP, val) = value.status = val
+"""Set [`ThermalFastStartSIIP`](@ref) `bus`."""
+PSY.set_bus!(value::ThermalFastStartSIIP, val) = value.bus = val
+"""Set [`ThermalFastStartSIIP`](@ref) `active_power`."""
+PSY.set_active_power!(value::ThermalFastStartSIIP, val) = value.active_power = set_value(value, val)
+"""Set [`ThermalFastStartSIIP`](@ref) `reactive_power`."""
+PSY.set_reactive_power!(value::ThermalFastStartSIIP, val) = value.reactive_power = set_value(value, val)
+"""Set [`ThermalFastStartSIIP`](@ref) `rating`."""
+PSY.set_rating!(value::ThermalFastStartSIIP, val) = value.rating = set_value(value, val)
+"""Set [`ThermalFastStartSIIP`](@ref) `active_power_limits`."""
+PSY.set_active_power_limits!(value::ThermalFastStartSIIP, val) = value.active_power_limits = set_value(value, val)
+"""Set [`ThermalFastStartSIIP`](@ref) `reactive_power_limits`."""
+PSY.set_reactive_power_limits!(value::ThermalFastStartSIIP, val) = value.reactive_power_limits = set_value(value, val)
+"""Set [`ThermalFastStartSIIP`](@ref) `ramp_limits`."""
+PSY.set_ramp_limits!(value::ThermalFastStartSIIP, val) = value.ramp_limits = set_value(value, val)
+"""Set [`ThermalFastStartSIIP`](@ref) `operation_cost`."""
+PSY.set_operation_cost!(value::ThermalFastStartSIIP, val) = value.operation_cost = val
+"""Set [`ThermalFastStartSIIP`](@ref) `base_power`."""
+PSY.set_base_power!(value::ThermalFastStartSIIP, val) = value.base_power = val
+"""Set [`ThermalFastStartSIIP`](@ref) `time_limits`."""
+PSY.set_time_limits!(value::ThermalFastStartSIIP, val) = value.time_limits = val
+"""Set [`ThermalFastStartSIIP`](@ref) `prime_mover`."""
+PSY.set_prime_mover!(value::ThermalFastStartSIIP, val) = value.prime_mover = val
+"""Set [`ThermalFastStartSIIP`](@ref) `fuel`."""
+PSY.set_fuel!(value::ThermalFastStartSIIP, val) = value.fuel = val
+"""Set [`ThermalFastStartSIIP`](@ref) `services`."""
+PSY.set_services!(value::ThermalFastStartSIIP, val) = value.services = val
+"""Set [`ThermalFastStartSIIP`](@ref) `time_at_status`."""
+PSY.set_time_at_status!(value::ThermalFastStartSIIP, val) = value.time_at_status = val
+"""Set [`ThermalFastStartSIIP`](@ref) `ext`."""
+PSY.set_ext!(value::ThermalFastStartSIIP, val) = value.ext = val
+"""Set [`ThermalFastStartSIIP`](@ref) `time_series_container`."""
+PSY.set_time_series_container!(value::ThermalFastStartSIIP, val) = value.time_series_container = val
diff --git a/src/structs/devices/ThermalGenEMIS.jl b/src/structs/devices/ThermalGenEMIS.jl
index 3f3a1c8..2191669 100644
--- a/src/structs/devices/ThermalGenEMIS.jl
+++ b/src/structs/devices/ThermalGenEMIS.jl
@@ -1,60 +1,62 @@
-"""
-This struct contains the technical data of a thermal generator.
- type: Generator technology type.
- fuel: Fuel type.
- active_power_limits: Named tuple of minimum and maximum generation limits.
- ramp_limits: Ramp limits (MW/hr).
- time_limits: Minimum up and Minimum down time limits (hr).
- operation_cost: Operation cost based on PowerSystems.jl definitions.
- fuel_cost: Fuel cost
- heat_rate_curve: Heat Rate curve
- bus: Name of the bus on which the generator is located.
- zone: Name of zone in which the generator is located.
-"""
-struct ThermalTech <: Tech
- type::String
- fuel::String
- active_power_limits::NamedTuple{(:min, :max),Tuple{Float64,Float64}}
- ramp_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}
- time_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}
- operation_cost::Union{Nothing, PSY.ThreePartCost}
- fuel_cost::Float64
- heat_rate_curve::Vector{Tuple{Float64, Float64}}
- bus::Int64
- zone::String
- FOR::Float64
-end
-
-get_type(tech::ThermalTech) = tech.type
-get_fuel(tech::ThermalTech) = tech.fuel
-get_active_power_limits(tech::ThermalTech) = tech.active_power_limits
-get_ramp_limits(tech::ThermalTech) = tech.ramp_limits
-get_time_limits(tech::ThermalTech) = tech.time_limits
-get_operation_cost(tech::ThermalTech) = tech.operation_cost
-get_fuel_cost(tech::ThermalTech) = tech.fuel_cost
-get_bus(tech::ThermalTech) = tech.bus
-get_zone(tech::ThermalTech) = tech.zone
-get_FOR(tech::ThermalTech) = tech.FOR
-get_heat_rate_curve(tech::ThermalTech) = tech.heat_rate_curve
-
-"""
-This struct contains all the data for a thermal generator.
- name: Generator's name.
- tech: Struct containing technical data.
- decision_year: The year in which the investment decision is made.
- construction_year: The year in which construction is completed.
- retirement_year: The year in which the generator is retired.
- end_life_year: Age-based retirement year.
- products: Vector containing the data of all products provided by the generator.
- finance_data: Struct containing financial data.
-"""
-mutable struct ThermalGenEMIS{T <: BuildPhase} <: GeneratorEMIS{T}
- name::String
- tech::ThermalTech
- decision_year::Int64
- construction_year::Int64
- retirement_year::Int64
- end_life_year::Int64
- products::Vector{Product}
- finance_data::Finance
-end
+"""
+This struct contains the technical data of a thermal generator.
+ type: Generator technology type.
+ fuel: Fuel type.
+ active_power_limits: Named tuple of minimum and maximum generation limits.
+ ramp_limits: Ramp limits (MW/hr).
+ time_limits: Minimum up and Minimum down time limits (hr).
+ operation_cost: Operation cost based on PowerSystems.jl definitions.
+ fuel_cost: Fuel cost
+ heat_rate_curve: Heat Rate curve
+ bus: Name of the bus on which the generator is located.
+ zone: Name of zone in which the generator is located.
+"""
+struct ThermalTech <: Tech
+ type::String
+ fuel::String
+ active_power_limits::NamedTuple{(:min, :max),Tuple{Float64,Float64}}
+ ramp_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}
+ time_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}
+ operation_cost::Union{Nothing, PSY.ThreePartCost}
+ fuel_cost::Float64
+ heat_rate_curve::Vector{Tuple{Float64, Float64}}
+ bus::Int64
+ zone::String
+ FOR::Float64
+ MTTR::Int64
+end
+
+get_type(tech::ThermalTech) = tech.type
+get_fuel(tech::ThermalTech) = tech.fuel
+get_active_power_limits(tech::ThermalTech) = tech.active_power_limits
+get_ramp_limits(tech::ThermalTech) = tech.ramp_limits
+get_time_limits(tech::ThermalTech) = tech.time_limits
+get_operation_cost(tech::ThermalTech) = tech.operation_cost
+get_fuel_cost(tech::ThermalTech) = tech.fuel_cost
+get_bus(tech::ThermalTech) = tech.bus
+get_zone(tech::ThermalTech) = tech.zone
+get_FOR(tech::ThermalTech) = tech.FOR
+get_MTTR(tech::ThermalTech) = tech.MTTR
+get_heat_rate_curve(tech::ThermalTech) = tech.heat_rate_curve
+
+"""
+This struct contains all the data for a thermal generator.
+ name: Generator's name.
+ tech: Struct containing technical data.
+ decision_year: The year in which the investment decision is made.
+ construction_year: The year in which construction is completed.
+ retirement_year: The year in which the generator is retired.
+ end_life_year: Age-based retirement year.
+ products: Vector containing the data of all products provided by the generator.
+ finance_data: Struct containing financial data.
+"""
+mutable struct ThermalGenEMIS{T <: BuildPhase} <: GeneratorEMIS{T}
+ name::String
+ tech::ThermalTech
+ decision_year::Int64
+ construction_year::Int64
+ retirement_year::Int64
+ end_life_year::Int64
+ products::Vector{Product}
+ finance_data::Finance
+end
diff --git a/src/structs/market_structs/CapacityMarket.jl b/src/structs/market_structs/CapacityMarket.jl
index 5dbf7fe..3b4a213 100644
--- a/src/structs/market_structs/CapacityMarket.jl
+++ b/src/structs/market_structs/CapacityMarket.jl
@@ -1,20 +1,20 @@
-"""
-This struct contains the capacity market parameters
-for price forcasts using CEM and endogeneous Economic Dispatch.
-The Capacity market is implemented using a piece-wise Capacity Demand Curve,
-with break-points and price-points parameterized within this struct.
-"""
-struct CapacityMarket # Assumes a piece-wise linear demand curve
-
- break_points::Vector{Float64} # demand curve break points
- price_points::Vector{Float64} # $/MW-year
-
- function CapacityMarket(b, p)
- @assert all(b .>= 0)
- @assert all(p .>= 0)
- @assert length(b) > 1
- @assert length(p) == length(b)
- new(b, p)
- end
-
-end
+"""
+This struct contains the capacity market parameters
+for price forcasts using CEM and endogeneous Economic Dispatch.
+The Capacity market is implemented using a piece-wise Capacity Demand Curve,
+with break-points and price-points parameterized within this struct.
+"""
+struct CapacityMarket # Assumes a piece-wise linear demand curve
+
+ break_points::Vector{Float64} # demand curve break points
+ price_points::Vector{Float64} # $/MW-year
+
+ function CapacityMarket(b, p)
+ @assert all(b .>= 0)
+ @assert all(p .>= 0)
+ @assert length(b) > 1
+ @assert length(p) == length(b)
+ new(b, p)
+ end
+
+end
diff --git a/src/structs/market_structs/EnergyMarket.jl b/src/structs/market_structs/EnergyMarket.jl
index 71b3e28..73f4787 100644
--- a/src/structs/market_structs/EnergyMarket.jl
+++ b/src/structs/market_structs/EnergyMarket.jl
@@ -1,20 +1,20 @@
-"""
-This struct contains the energy market parameters
-for price forcasts using CEM and endogeneous Economic Dispatch.
-The energy market assumes inelastic demand.
-"""
-
-struct EnergyMarket{Z, T}
-
- demand::AxisArrays.AxisArray{Float64, 2} # MW
- price_cap::AxisArrays.AxisArray{Float64, 1} # $/MWh
-
- function EnergyMarket{}(demand::AxisArrays.AxisArray{Float64, 2}, price_cap::AxisArrays.AxisArray{Float64, 1})
- @assert all(demand .>= 0)
- @assert all(price_cap .>= 0)
- Z = size(demand, 1)
- T = size(demand, 2)
- new{Z, T}(demand, price_cap)
- end
-
-end
+"""
+This struct contains the energy market parameters
+for price forcasts using CEM and endogeneous Economic Dispatch.
+The energy market assumes inelastic demand.
+"""
+
+struct EnergyMarket{Z, T}
+
+ demand::AxisArrays.AxisArray{Float64, 2} # MW
+ price_cap::AxisArrays.AxisArray{Float64, 1} # $/MWh
+
+ function EnergyMarket{}(demand::AxisArrays.AxisArray{Float64, 2}, price_cap::AxisArrays.AxisArray{Float64, 1})
+ @assert all(demand .>= 0)
+ @assert all(price_cap .>= 0)
+ Z = size(demand, 1)
+ T = size(demand, 2)
+ new{Z, T}(demand, price_cap)
+ end
+
+end
diff --git a/src/structs/market_structs/InertiaMarket.jl b/src/structs/market_structs/InertiaMarket.jl
new file mode 100644
index 0000000..2b3ee4b
--- /dev/null
+++ b/src/structs/market_structs/InertiaMarket.jl
@@ -0,0 +1,18 @@
+"""
+This struct contains the Inertia market parameters
+ for price forcasts using CEM and endogeneous Economic Dispatch.
+The Inertia market involves a minimum inertia requirment included in this struct.
+"""
+
+struct InertiaMarket
+
+ inertia_req::Float64 # MW-s
+ price_cap::Float64 # MW
+
+ function InertiaMarket(req::Float64, price_cap::Float64)
+ @assert req >= 0
+ @assert price_cap >= 0
+ new(req, price_cap)
+ end
+
+end
diff --git a/src/structs/market_structs/MarketClearingProblem.jl b/src/structs/market_structs/MarketClearingProblem.jl
index bc1467b..c27b0c5 100644
--- a/src/structs/market_structs/MarketClearingProblem.jl
+++ b/src/structs/market_structs/MarketClearingProblem.jl
@@ -1,38 +1,38 @@
-"""
-This struct contains all data for running a CEM for price forecasts or for endoogeneous Economic Dispatch.
- zones: Name of zones included in the market clearing problem.
- lines: Vector of inter-zonal transmission lines included in the market clearing problem.
- capital_cost_multiplier: Average capital cost multiplier for new builds.
- inv_periods: Vector of all markets modeled for the entire CEM planning horizon.
- carbon_tax: Vector of carbon tax for the planning horizon
- projects: Vector of all projects included in the market clearing problem.
- rep_hour_weight: Weight of representative hours in the market clearing problem.
-"""
-
-struct MarketClearingProblem{Z, T}
-
- zones::Vector{String}
- lines::Vector{ZonalLine}
- capital_cost_multiplier::Float64
- inv_periods::Vector{MarketCollection{Z, T}}
- carbon_tax::Vector{Float64}
- projects::Vector{MarketProject}
- rep_hour_weight::Vector{Float64}
-
- function MarketClearingProblem(
- zones::Vector{String},
- lines::Vector{ZonalLine},
- capital_cost_multiplier::Float64,
- inv_periods::Vector{MarketCollection{Z, T}},
- carbon_tax::Vector{Float64},
- projects::Vector{MarketProject},
- rep_hour_weight::Vector{Float64}
- ) where {Z, T}
- @assert T == length(rep_hour_weight)
- @assert length(zones) == Z
- @assert length(inv_periods) == length(carbon_tax)
- @assert allunique(getproperty.(projects, :name))
- new{Z, T}(zones, lines, capital_cost_multiplier, inv_periods, carbon_tax, projects, rep_hour_weight)
- end
-
-end
+"""
+This struct contains all data for running a CEM for price forecasts or for endoogeneous Economic Dispatch.
+ zones: Name of zones included in the market clearing problem.
+ lines: Vector of inter-zonal transmission lines included in the market clearing problem.
+ capital_cost_multiplier: Average capital cost multiplier for new builds.
+ inv_periods: Vector of all markets modeled for the entire CEM planning horizon.
+ carbon_tax: Vector of carbon tax for the planning horizon
+ projects: Vector of all projects included in the market clearing problem.
+ rep_hour_weight: Weight of representative hours in the market clearing problem.
+"""
+
+struct MarketClearingProblem{Z, T}
+
+ zones::Vector{String}
+ lines::Vector{ZonalLine}
+ capital_cost_multiplier::Float64
+ inv_periods::Vector{MarketCollection{Z, T}}
+ carbon_tax::Vector{Float64}
+ projects::Vector{MarketProject}
+ rep_hour_weight::Vector{Float64}
+
+ function MarketClearingProblem(
+ zones::Vector{String},
+ lines::Vector{ZonalLine},
+ capital_cost_multiplier::Float64,
+ inv_periods::Vector{MarketCollection{Z, T}},
+ carbon_tax::Vector{Float64},
+ projects::Vector{MarketProject},
+ rep_hour_weight::Vector{Float64}
+ ) where {Z, T}
+ @assert T == length(rep_hour_weight)
+ @assert length(zones) == Z
+ @assert length(inv_periods) == length(carbon_tax)
+ @assert allunique(getproperty.(projects, :name))
+ new{Z, T}(zones, lines, capital_cost_multiplier, inv_periods, carbon_tax, projects, rep_hour_weight)
+ end
+
+end
diff --git a/src/structs/market_structs/MarketCollection.jl b/src/structs/market_structs/MarketCollection.jl
index 3e757ec..6b7e108 100644
--- a/src/structs/market_structs/MarketCollection.jl
+++ b/src/structs/market_structs/MarketCollection.jl
@@ -1,25 +1,28 @@
-"""
-This struct contains the collection of all markets modeled
-for price forcasts using CEM and endogeneous Economic Dispatch.
-"""
-
-struct MarketCollection{Z, T}
-
- capacity::CapacityMarket
- energy::EnergyMarket
- reserveup::Union{Nothing, Dict{String, ReserveUpMarket{T}}}
- reservedown::Union{Nothing, Dict{String, ReserveDownMarket{T}}}
- ordc::Union{Nothing, Dict{String, ReserveORDCMarket{T}}}
- rec::RECMarket
-
- function MarketCollection(
- c::CapacityMarket,
- e::EnergyMarket{Z, T},
- ru::Union{Nothing, Dict{String, ReserveUpMarket{T}}},
- rd::Union{Nothing, Dict{String, ReserveDownMarket{T}}},
- ordc::Union{Nothing, Dict{String, ReserveORDCMarket{T}}},
- rec::RECMarket
- ) where {Z, T}
- new{Z, T}(c, e, ru, rd, ordc, rec)
- end
-end
+"""
+This struct contains the collection of all markets modeled
+for price forcasts using CEM and endogeneous Economic Dispatch.
+"""
+
+struct MarketCollection{Z, T}
+
+ capacity::CapacityMarket
+ energy::EnergyMarket
+ reserveup::Union{Nothing, Dict{String, ReserveUpMarket{T}}}
+ reservedown::Union{Nothing, Dict{String, ReserveDownMarket{T}}}
+ ordc::Union{Nothing, Dict{String, ReserveORDCMarket{T}}}
+ rec::RECMarket
+ inertia::InertiaMarket
+
+ function MarketCollection(
+ c::CapacityMarket,
+ e::EnergyMarket{Z, T},
+ ru::Union{Nothing, Dict{String, ReserveUpMarket{T}}},
+ rd::Union{Nothing, Dict{String, ReserveDownMarket{T}}},
+ ordc::Union{Nothing, Dict{String, ReserveORDCMarket{T}}},
+ rec::RECMarket,
+ inertia::InertiaMarket
+
+ ) where {Z, T}
+ new{Z, T}(c, e, ru, rd, ordc, rec, inertia)
+ end
+end
diff --git a/src/structs/market_structs/MarketProject.jl b/src/structs/market_structs/MarketProject.jl
index 7b63c86..f35939b 100644
--- a/src/structs/market_structs/MarketProject.jl
+++ b/src/structs/market_structs/MarketProject.jl
@@ -1,152 +1,162 @@
-"""
-This struct contains all the data for creating a market project
- for price forcasts using CEM and endogeneous Economic Dispatch.
-"""
-
-mutable struct MarketProject
-
- name::String # name of project
- project_type::String # is the project of storage type
- tech_type::String # technology type
- fixed_cost::Float64 # $/unit/investment period
- queue_cost::Vector{Float64} # queue cost
- marginal_energy_cost::Float64 # $/MW/hour
- marginal_reserve_cost::Dict{String, Float64}
- emission_intensity::Float64
- expansion_cost::Vector{Float64} # $/unit
- discount_rate::Float64 # discount rate
- min_gen::Float64 # MW/unit
- max_gen::Float64 # MW/unit
- min_input::Float64 # MW/unit
- max_input::Float64 # MW/unit
- efficiency_in::Float64 # input efficiency
- efficiency_out::Float64 # output efficiency
- min_storage::Float64 # minimum storage capacity
- max_storage::Float64 # maximum storage capacity
- init_storage::Float64 # initial storage
- availability::Array{Float64, 2} # Hourly availability factor
- derating_factor::Float64 # Derating factor
- ramp_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}} # MW/unit/hour
- max_reserve_limits::Dict{String, Float64}
- existing_units::Float64 # existing units
- units_in_queue::Vector{Float64} # units in queue
- build_lead_time::Int64 # build lead time
- remaining_build_time::Int64 # remaining build time
- max_new_options::Int # maximum units
- base_cost_units::Int64 # number of units that can be built without capital cost multiplier
- capex_years::Int64 # capital cost recovery years
- life_time::Int64 # life_time
- remaining_life::Int64 # Remaining life_time
- capacity_eligible::Bool # eligible for capacity market participation
- rec_eligible::Bool # eligible for rps compliance
- zone::String # project zone
- ownedby::Vector{String} # onned by which investors
-
- function MarketProject(
- name::String,
- project_type::String,
- tech_type::String,
- fixed_cost::Number,
- queue_cost::Vector{<: Number},
- marginal_energy_cost::Number,
- marginal_reserve_cost::Dict{String, Float64},
- emission_intensity::Float64,
- expansion_cost::Vector{Float64},
- discount_rate::Number,
- min_gen::Number,
- max_gen::Number,
- min_input::Number,
- max_input::Number,
- efficiency_in::Number,
- efficiency_out::Number,
- min_storage::Number,
- max_storage::Number,
- init_storage::Number,
- availability::Array{<: Number, 2},
- derating_factor::Number,
- ramp_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}},
- max_reserve_limits::Dict{String, Float64},
- existing_units::Number,
- units_in_queue::Vector{<: Number},
- build_lead_time::Number,
- remaining_build_time::Number,
- max_new_options::Number,
- base_cost_units::Number,
- capex_years::Number,
- life_time::Number,
- remaining_life::Int64,
- capacity_eligible::Bool,
- rec_eligible::Bool,
- zone::String,
- ownedby::Vector{String})
-
- @assert fixed_cost >= 0
- @assert marginal_energy_cost >= 0
- @assert all(values(marginal_reserve_cost) .>= 0)
- @assert emission_intensity >= 0
- @assert all(expansion_cost .>= 0)
- @assert all(queue_cost .>= 0)
- @assert discount_rate >= 0
- @assert min_gen >= 0
- @assert max_gen >= 0
- @assert min_input >= 0
- @assert max_input >= 0
- @assert all(values(max_reserve_limits) .>= 0)
- @assert efficiency_in >= 0
- @assert efficiency_out >= 0
- @assert min_storage >= 0
- @assert max_storage >= 0
- @assert init_storage >= 0
- @assert all(availability .>= 0)
- @assert derating_factor >= 0
- @assert existing_units >= 0
- @assert all(units_in_queue .>= 0)
- @assert build_lead_time >= 0
- @assert remaining_build_time >= 0
- @assert base_cost_units >= 0
- @assert max_new_options >= base_cost_units
- @assert capex_years >- 0
- @assert life_time >= 0
- @assert remaining_life >= 0
-
- new(name,
- project_type,
- tech_type,
- fixed_cost,
- queue_cost,
- marginal_energy_cost,
- marginal_reserve_cost,
- emission_intensity,
- expansion_cost,
- discount_rate,
- min_gen,
- max_gen,
- min_input,
- max_input,
- efficiency_in,
- efficiency_out,
- min_storage,
- max_storage,
- init_storage,
- availability,
- derating_factor,
- ramp_limits,
- max_reserve_limits,
- existing_units,
- units_in_queue,
- build_lead_time,
- remaining_build_time,
- max_new_options,
- base_cost_units,
- capex_years,
- life_time,
- remaining_life,
- rec_eligible,
- capacity_eligible,
- zone,
- ownedby
- )
-
- end
-
-end
+"""
+This struct contains all the data for creating a market project
+ for price forcasts using CEM and endogeneous Economic Dispatch.
+"""
+
+mutable struct MarketProject
+
+ name::String # name of project
+ project_type::String # is the project of storage type
+ tech_type::String # technology type
+ fixed_cost::Float64 # $/unit/investment period
+ queue_cost::Vector{Float64} # queue cost
+ marginal_energy_cost::Float64 # $/MW/hour
+ marginal_reserve_cost::Dict{String, Float64}
+ emission_intensity::Float64
+ expansion_cost::Vector{Float64} # $/unit
+ discount_rate::Float64 # discount rate
+ min_gen::Float64 # MW/unit
+ max_gen::Float64 # MW/unit
+ min_input::Float64 # MW/unit
+ max_input::Float64 # MW/unit
+ efficiency_in::Float64 # input efficiency
+ efficiency_out::Float64 # output efficiency
+ min_storage::Float64 # minimum storage capacity
+ max_storage::Float64 # maximum storage capacity
+ init_storage::Float64 # initial storage
+ availability::Array{Float64, 2} # Hourly availability factor
+ derating_factor::Float64 # Derating factor
+ ramp_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}} # MW/unit/hour
+ max_reserve_limits::Dict{String, Float64}
+ existing_units::Float64 # existing units
+ units_in_queue::Vector{Float64} # units in queue
+ build_lead_time::Int64 # build lead time
+ remaining_build_time::Int64 # remaining build time
+ max_new_options::Int # maximum units
+ base_cost_units::Int64 # number of units that can be built without capital cost multiplier
+ capex_years::Int64 # capital cost recovery years
+ life_time::Int64 # life_time
+ remaining_life::Int64 # Remaining life_time
+ capacity_eligible::Bool # eligible for capacity market participation
+ rec_eligible::Bool # eligible for rps compliance
+ rec_correction::Float64 # rec correction factor based on actual production
+ inertia_constant::Float64 # inertia H constant
+ synchronous_inertia::Bool # whether inertia is synchronous
+ zone::String # project zone
+ ownedby::Vector{String} # onned by which investors
+
+ function MarketProject(
+ name::String,
+ project_type::String,
+ tech_type::String,
+ fixed_cost::Number,
+ queue_cost::Vector{<: Number},
+ marginal_energy_cost::Number,
+ marginal_reserve_cost::Dict{String, Float64},
+ emission_intensity::Float64,
+ expansion_cost::Vector{Float64},
+ discount_rate::Number,
+ min_gen::Number,
+ max_gen::Number,
+ min_input::Number,
+ max_input::Number,
+ efficiency_in::Number,
+ efficiency_out::Number,
+ min_storage::Number,
+ max_storage::Number,
+ init_storage::Number,
+ availability::Array{<: Number, 2},
+ derating_factor::Number,
+ ramp_limits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}},
+ max_reserve_limits::Dict{String, Float64},
+ existing_units::Number,
+ units_in_queue::Vector{<: Number},
+ build_lead_time::Number,
+ remaining_build_time::Number,
+ max_new_options::Number,
+ base_cost_units::Number,
+ capex_years::Number,
+ life_time::Number,
+ remaining_life::Int64,
+ capacity_eligible::Bool,
+ rec_eligible::Bool,
+ rec_correction::Float64,
+ inertia_constant::Float64,
+ synchronous_inertia::Bool,
+ zone::String,
+ ownedby::Vector{String})
+
+ @assert fixed_cost >= 0
+ @assert marginal_energy_cost >= 0
+ @assert all(values(marginal_reserve_cost) .>= 0)
+ @assert emission_intensity >= 0
+ @assert all(expansion_cost .>= 0)
+ @assert all(queue_cost .>= 0)
+ @assert discount_rate >= 0
+ @assert min_gen >= 0
+ @assert max_gen >= 0
+ @assert min_input >= 0
+ @assert max_input >= 0
+ @assert all(values(max_reserve_limits) .>= 0)
+ @assert efficiency_in >= 0
+ @assert efficiency_out >= 0
+ @assert min_storage >= 0
+ @assert max_storage >= 0
+ @assert init_storage >= 0
+ @assert all(availability .>= 0)
+ @assert derating_factor >= 0
+ @assert existing_units >= 0
+ @assert all(units_in_queue .>= 0)
+ @assert build_lead_time >= 0
+ @assert remaining_build_time >= 0
+ @assert base_cost_units >= 0
+ @assert max_new_options >= base_cost_units
+ @assert capex_years >- 0
+ @assert life_time >= 0
+ @assert remaining_life >= 0
+ @assert rec_correction >= 0
+
+ new(name,
+ project_type,
+ tech_type,
+ fixed_cost,
+ queue_cost,
+ marginal_energy_cost,
+ marginal_reserve_cost,
+ emission_intensity,
+ expansion_cost,
+ discount_rate,
+ min_gen,
+ max_gen,
+ min_input,
+ max_input,
+ efficiency_in,
+ efficiency_out,
+ min_storage,
+ max_storage,
+ init_storage,
+ availability,
+ derating_factor,
+ ramp_limits,
+ max_reserve_limits,
+ existing_units,
+ units_in_queue,
+ build_lead_time,
+ remaining_build_time,
+ max_new_options,
+ base_cost_units,
+ capex_years,
+ life_time,
+ remaining_life,
+ capacity_eligible,
+ rec_eligible,
+ rec_correction,
+ inertia_constant,
+ synchronous_inertia,
+ zone,
+ ownedby
+ )
+
+ end
+
+end
diff --git a/src/structs/market_structs/RECMarket.jl b/src/structs/market_structs/RECMarket.jl
index 2d1864e..34264dd 100644
--- a/src/structs/market_structs/RECMarket.jl
+++ b/src/structs/market_structs/RECMarket.jl
@@ -1,19 +1,20 @@
-"""
-This struct contains the REC market parameters
- for price forcasts using CEM and endogeneous Economic Dispatch.
-The REC market assumes a percentage RPS requirment of annual energy consumption
-which is parameterized within this struct.
-"""
-
-struct RECMarket
-
- rec_req::Float64 # $/MW/investment period
- price_cap::Float64 # MW
-
- function RECMarket(req::Number, price_cap::Number)
- @assert 0 <= req <= 1
- @assert price_cap >= 0
- new(req, price_cap)
- end
-
-end
+"""
+This struct contains the REC market parameters
+ for price forcasts using CEM and endogeneous Economic Dispatch.
+The REC market assumes a percentage RPS requirment of annual energy consumption
+which is parameterized within this struct.
+"""
+
+struct RECMarket
+
+ rec_req::Float64 # $/MW/investment period
+ price_cap::Float64 # MW
+ binding::Bool
+
+ function RECMarket(req::Number, price_cap::Number, binding::Bool)
+ @assert 0 <= req <= 1
+ @assert price_cap >= 0
+ new(req, price_cap, binding)
+ end
+
+end
diff --git a/src/structs/market_structs/ReserveDownMarket.jl b/src/structs/market_structs/ReserveDownMarket.jl
index fa9a985..fdbec7a 100644
--- a/src/structs/market_structs/ReserveDownMarket.jl
+++ b/src/structs/market_structs/ReserveDownMarket.jl
@@ -1,21 +1,21 @@
-"""
-This struct contains the reserve down market parameters
-for price forcasts using CEM and endogeneous Economic Dispatch.
-The reserve down market assumes inelastic demand.
-"""
-
-struct ReserveDownMarket{T} #
-
- demand::Vector{Float64} # MW
- price_cap::Float64 # $/MWh
- zones::Vector{String}
- eligible_projects::Vector{String}
-
- function ReserveDownMarket{}(demand::Vector{Float64}, price_cap::Float64, zones::Vector{String}, eligible_projects::Vector{String})
- @assert all(demand .>= 0)
- @assert price_cap >= 0
- T = length(demand)
- new{T}(demand, price_cap, zones, eligible_projects)
- end
-
-end
+"""
+This struct contains the reserve down market parameters
+for price forcasts using CEM and endogeneous Economic Dispatch.
+The reserve down market assumes inelastic demand.
+"""
+
+struct ReserveDownMarket{T} #
+
+ demand::Vector{Float64} # MW
+ price_cap::Float64 # $/MWh
+ zones::Vector{String}
+ eligible_projects::Vector{String}
+
+ function ReserveDownMarket{}(demand::Vector{Float64}, price_cap::Float64, zones::Vector{String}, eligible_projects::Vector{String})
+ @assert all(demand .>= 0)
+ @assert price_cap >= 0
+ T = length(demand)
+ new{T}(demand, price_cap, zones, eligible_projects)
+ end
+
+end
diff --git a/src/structs/market_structs/ReserveORDCMarket.jl b/src/structs/market_structs/ReserveORDCMarket.jl
index 389ef48..dcf7b01 100644
--- a/src/structs/market_structs/ReserveORDCMarket.jl
+++ b/src/structs/market_structs/ReserveORDCMarket.jl
@@ -1,29 +1,29 @@
-"""
-This struct contains the reserve up market parameters
-for price forcasts using CEM and endogeneous Economic Dispatch.
-The Reserve ORDC Market is implemented using a piece-wise linear Operating Reserve Demand Curve (ORDC),
-with break-points and price-points parameterized within this struct.
-"""
-
-struct ReserveORDCMarket{T}
-
- break_points::AxisArrays.AxisArray{Vector{Float64}} # ORDC break points
- price_points::AxisArrays.AxisArray{Vector{Float64}} # $/MWh
- zones::Vector{String}
- eligible_projects::Vector{String}
- stepped::Bool
-
- function ReserveORDCMarket(b, p, z, e, s)
- T = length(b)
- @assert length(p) == length(b)
- for i = 1:length(b)
- @assert all(b[i] .>= 0)
- @assert all(p[i] .>= 0)
-
- @assert length(b[i]) > 1
- @assert length(b[i]) == length(p[i])
- end
- new{T}(b, p, z, e, s)
- end
-
-end
+"""
+This struct contains the reserve up market parameters
+for price forcasts using CEM and endogeneous Economic Dispatch.
+The Reserve ORDC Market is implemented using a piece-wise linear Operating Reserve Demand Curve (ORDC),
+with break-points and price-points parameterized within this struct.
+"""
+
+struct ReserveORDCMarket{T}
+
+ break_points::AxisArrays.AxisArray{Vector{Float64}} # ORDC break points
+ price_points::AxisArrays.AxisArray{Vector{Float64}} # $/MWh
+ zones::Vector{String}
+ eligible_projects::Vector{String}
+ stepped::Bool
+
+ function ReserveORDCMarket(b, p, z, e, s)
+ T = length(b)
+ @assert length(p) == length(b)
+ for i = 1:length(b)
+ @assert all(b[i] .>= 0)
+ @assert all(p[i] .>= 0)
+
+ @assert length(b[i]) > 1
+ @assert length(b[i]) == length(p[i])
+ end
+ new{T}(b, p, z, e, s)
+ end
+
+end
diff --git a/src/structs/market_structs/ReserveUpMarket.jl b/src/structs/market_structs/ReserveUpMarket.jl
index f9f678c..b8dbdb3 100644
--- a/src/structs/market_structs/ReserveUpMarket.jl
+++ b/src/structs/market_structs/ReserveUpMarket.jl
@@ -1,22 +1,22 @@
-"""
-This struct contains the reserve up market parameters
-for price forcasts using CEM and endogeneous Economic Dispatch.
-The Reserve Up Market is implemented using a piece-wise linear Operating Reserve Demand Curve (ORDC),
-with break-points and price-points parameterized within this struct.
-"""
-
-struct ReserveUpMarket{T} #
-
- demand::Vector{Float64} # MW
- price_cap::Float64 # $/MWh
- zones::Vector{String}
- eligible_projects::Vector{String}
-
- function ReserveUpMarket{}(demand::Vector{Float64}, price_cap::Float64, zones::Vector{String}, eligible_projects::Vector{String})
- @assert all(demand .>= 0)
- @assert price_cap >= 0
- T = length(demand)
- new{T}(demand, price_cap, zones, eligible_projects)
- end
-
-end
+"""
+This struct contains the reserve up market parameters
+for price forcasts using CEM and endogeneous Economic Dispatch.
+The Reserve Up Market is implemented using a piece-wise linear Operating Reserve Demand Curve (ORDC),
+with break-points and price-points parameterized within this struct.
+"""
+
+struct ReserveUpMarket{T} #
+
+ demand::Vector{Float64} # MW
+ price_cap::Float64 # $/MWh
+ zones::Vector{String}
+ eligible_projects::Vector{String}
+
+ function ReserveUpMarket{}(demand::Vector{Float64}, price_cap::Float64, zones::Vector{String}, eligible_projects::Vector{String})
+ @assert all(demand .>= 0)
+ @assert price_cap >= 0
+ T = length(demand)
+ new{T}(demand, price_cap, zones, eligible_projects)
+ end
+
+end
diff --git a/src/structs/products/Capacity.jl b/src/structs/products/Capacity.jl
index 9993bf1..ea595b5 100644
--- a/src/structs/products/Capacity.jl
+++ b/src/structs/products/Capacity.jl
@@ -1,55 +1,55 @@
-"""
-This struct contains data for the capacity market product.
- name: Capacity.
- derating: The derating factor for capacity market participation based on technology type.
- accepted_perc: The percentage of capacity cleared in capacity markets for each scenario.
- capacity_bid: Capacity market bid placed by the project.
-"""
-mutable struct Capacity <: Product
- name::Symbol
- derating::Float64
- accepted_perc::Dict{String, Vector{Float64}}
- capacity_bid::Float64
-end
-
-# Derating factors, accepted percentage and capacity bids only returned when product is of type Capacity
-get_derating(prod::Product) = nothing
-get_derating(prod::Capacity) = prod.derating
-get_accepted_perc(prod::Product) = nothing
-get_accepted_perc(prod::Capacity) = prod.accepted_perc
-get_capacity_bid(prod::Product) = nothing
-get_capacity_bid(prod::Capacity) = prod.capacity_bid
-
-# Derating factors only set when product is of type Capacity
-function set_derating!(prod::T, derating_factor) where T <: Product
- return
-end
-
-function set_derating!(prod::Capacity, derating_factor)
- prod.derating = derating_factor
- return
-end
-
-# Capacity bids only set when product is of type Capacity
-function set_capacity_bid!(prod::T, capacity_bid) where T <: Product
- return
-end
-
-function set_capacity_bid!(prod::Capacity, capacity_bid)
- prod.capacity_bid = capacity_bid
- return
-end
-
-# Capacity accepted percentage only set when product is of type Capacity
-function set_accepted_perc!(product::T,
- scenario_name::String,
- capacity_accepted_perc::Array{Float64, 1}) where T <: Product
- return
-end
-
-function set_accepted_perc!(product::Capacity,
- scenario_name::String,
- capacity_accepted_perc::Array{Float64, 1})
- product.accepted_perc[scenario_name] = capacity_accepted_perc
- return
-end
+"""
+This struct contains data for the capacity market product.
+ name: Capacity.
+ derating: The derating factor for capacity market participation based on technology type.
+ accepted_perc: The percentage of capacity cleared in capacity markets for each scenario.
+ capacity_bid: Capacity market bid placed by the project.
+"""
+mutable struct Capacity <: Product
+ name::Symbol
+ derating::Float64
+ accepted_perc::Dict{String, Vector{Float64}}
+ capacity_bid::Float64
+end
+
+# Derating factors, accepted percentage and capacity bids only returned when product is of type Capacity
+get_derating(prod::Product) = nothing
+get_derating(prod::Capacity) = prod.derating
+get_accepted_perc(prod::Product) = nothing
+get_accepted_perc(prod::Capacity) = prod.accepted_perc
+get_capacity_bid(prod::Product) = nothing
+get_capacity_bid(prod::Capacity) = prod.capacity_bid
+
+# Derating factors only set when product is of type Capacity
+function set_derating!(prod::T, derating_factor) where T <: Product
+ return
+end
+
+function set_derating!(prod::Capacity, derating_factor)
+ prod.derating = derating_factor
+ return
+end
+
+# Capacity bids only set when product is of type Capacity
+function set_capacity_bid!(prod::T, capacity_bid) where T <: Product
+ return
+end
+
+function set_capacity_bid!(prod::Capacity, capacity_bid)
+ prod.capacity_bid = capacity_bid
+ return
+end
+
+# Capacity accepted percentage only set when product is of type Capacity
+function set_accepted_perc!(product::T,
+ scenario_name::String,
+ capacity_accepted_perc::Array{Float64, 1}) where T <: Product
+ return
+end
+
+function set_accepted_perc!(product::Capacity,
+ scenario_name::String,
+ capacity_accepted_perc::Array{Float64, 1})
+ product.accepted_perc[scenario_name] = capacity_accepted_perc
+ return
+end
diff --git a/src/structs/products/CarbonTax.jl b/src/structs/products/CarbonTax.jl
index 824dc99..7907cee 100644
--- a/src/structs/products/CarbonTax.jl
+++ b/src/structs/products/CarbonTax.jl
@@ -1,44 +1,50 @@
-"""
-This struct contains data for the energy market product.
- name: CarbonTax.
- emission: Emission Intensity (ton/MMBTU).
- avg_heat_rate: Heat Rate (MMBTU/MWh)
- fuel_cost: Fuel Cost (\$/MMBTU)
-"""
-mutable struct CarbonTax <: Product
- name::Symbol
- emission_intensity::Float64
- avg_heat_rate::Float64
- fuel_cost::Float64
- total_emission::Vector{Float64}
-end
-
-# Emission only returned when product is of type CarbonTax
-function get_emission_intensity(prod::T) where T<:Product
- return
-end
-
-get_emission_intensity(prod::CarbonTax) = prod.emission_intensity
-
-# Heat Rate only returned when product is of type CarbonTax
-function get_avg_heat_rate(prod::T) where T<:Product
- return
-end
-
-get_avg_heat_rate(prod::CarbonTax) = prod.avg_heat_rate
-
-# Fuel Cost only returned when product is of type CarbonTax
-function get_fuel_cost(prod::T) where T<:Product
- return
-end
-
-get_fuel_cost(prod::CarbonTax) = prod.fuel_cost
-
-# Total emissionsy only returned when product is of type CarbonTax
-function get_total_emission(prod::T) where T<:Product
- return
-end
-
-get_total_emission(prod::CarbonTax) = prod.total_emission
-
-
+"""
+This struct contains data for the energy market product.
+ name: CarbonTax.
+ emission: Emission Intensity (ton/MMBTU).
+ avg_heat_rate: Heat Rate (MMBTU/MWh)
+ fuel_cost: Fuel Cost (\$/MMBTU)
+"""
+mutable struct CarbonTax <: Product
+ name::Symbol
+ emission_intensity::Float64
+ avg_heat_rate::Float64
+ fuel_cost::Float64
+ total_emission::Vector{Float64}
+end
+
+# Emission only returned when product is of type CarbonTax
+function get_emission_intensity(prod::T) where T<:Product
+ return
+end
+
+get_emission_intensity(prod::CarbonTax) = prod.emission_intensity
+
+# Heat Rate only returned when product is of type CarbonTax
+function get_avg_heat_rate(prod::T) where T<:Product
+ return
+end
+
+get_avg_heat_rate(prod::CarbonTax) = prod.avg_heat_rate
+
+# Fuel Cost only returned when product is of type CarbonTax
+function get_fuel_cost(prod::T) where T<:Product
+ return
+end
+
+get_fuel_cost(prod::CarbonTax) = prod.fuel_cost
+
+# Total emissionsy only returned when product is of type CarbonTax
+function get_total_emission(prod::T) where T<:Product
+ return
+end
+
+get_total_emission(prod::CarbonTax) = prod.total_emission
+
+function set_total_emission!(prod::T, emission::Vector{Float64}) where T<:Product
+ return
+end
+
+function set_total_emission!(prod::CarbonTax, emission::Vector{Float64})
+ prod.total_emission = emission
+end
diff --git a/src/structs/products/Energy.jl b/src/structs/products/Energy.jl
index e9ff6bd..d19cf14 100644
--- a/src/structs/products/Energy.jl
+++ b/src/structs/products/Energy.jl
@@ -1,34 +1,54 @@
-"""
-This struct contains data for the energy market product.
- name: Energy.
- capacity_factors: Dictionary of expected capacity factors for each scenario.
- marginal_cost: Marginal cost per unit of energy provided.
-"""
-mutable struct Energy <: OperatingProduct
- name::Symbol
- capacity_factors::Dict{String, Array{Float64, 2}}
- marginal_cost::Float64
-
-end
-
-# Capacity factors only returned when product is of type Energy
-function get_capacity_factors(prod::T) where T<:Product
- return
-end
-
-get_capacity_factors(prod::Energy) = prod.capacity_factors
-
-
-# Capacity factors only set when product is of type Energy
-function set_capacity_factors!(product::T,
- scenario_name::String,
- capacity_factors::Array{Float64, 2}) where T <: Product
- return
-end
-
-function set_capacity_factors!(product::Energy,
- scenario_name::String,
- capacity_factors::Array{Float64, 2})
- product.capacity_factors[scenario_name] = capacity_factors
- return
-end
+"""
+This struct contains data for the energy market product.
+ name: Energy.
+ capacity_factors: Dictionary of expected capacity factors for each scenario.
+ marginal_cost: Marginal cost per unit of energy provided.
+"""
+mutable struct Energy <: OperatingProduct
+ name::Symbol
+ capacity_factors::Dict{String, Array{Float64, 2}}
+ marginal_cost::Float64
+ expected_production::Float64
+end
+
+# Capacity factors only returned when product is of type Energy
+function get_capacity_factors(prod::T) where T<:Product
+ return
+end
+
+get_capacity_factors(prod::Energy) = prod.capacity_factors
+
+
+# Capacity factors only set when product is of type Energy
+function set_capacity_factors!(product::T,
+ scenario_name::String,
+ capacity_factors::Array{Float64, 2}) where T <: Product
+ return
+end
+
+function set_capacity_factors!(product::Energy,
+ scenario_name::String,
+ capacity_factors::Array{Float64, 2})
+ product.capacity_factors[scenario_name] = capacity_factors
+ return
+end
+
+
+# Expected energy production only returned when product is of type Energy
+function get_expected_production(prod::T) where T<:Product
+ return
+end
+
+get_expected_production(prod::Energy) = prod.expected_production
+
+# Expected energy production only set when product is of type Energy
+function set_expected_production!(product::T,
+ expected_production::Float64) where T <: Product
+ return
+end
+
+function set_expected_production!(product::Energy,
+ expected_production::Float64)
+ product.expected_production = expected_production
+ return
+end
diff --git a/src/structs/products/Inertia.jl b/src/structs/products/Inertia.jl
new file mode 100644
index 0000000..0f18483
--- /dev/null
+++ b/src/structs/products/Inertia.jl
@@ -0,0 +1,19 @@
+"""
+This struct contains data for the capacity market product.
+ name: Inertia.
+ synchronous: Whether unit provides synchronous inertia.
+ h_constant: H-constant of the unit.
+"""
+mutable struct Inertia <: OperatingProduct
+ name::Symbol
+ synchronous::Bool
+ h_constant::Float64
+ marginal_cost::Float64
+end
+
+# Derating factors, accepted percentage and capacity bids only returned when product is of type Capacity
+get_synchronous(prod::Product) = nothing
+get_synchronous(prod::Inertia) = prod.synchronous
+get_h_constant(prod::Product) = nothing
+get_h_constant(prod::Inertia) = prod.h_constant
+
diff --git a/src/structs/products/OperatingReserve.jl b/src/structs/products/OperatingReserve.jl
index 9edca95..59bb435 100644
--- a/src/structs/products/OperatingReserve.jl
+++ b/src/structs/products/OperatingReserve.jl
@@ -1,14 +1,14 @@
-"""
-This struct contains data for the operating reserve product
-parameterized by the reserve direction (up or down).
- name: ReserveUp or ReserveDown.
- max_limit: Maximum percentage of reserve that can be provided by the project.
- marginal_cost: Marginal cost per unit of reserve provided.
-"""
-struct OperatingReserve{T <: ReserveDirection} <: ReserveEMIS{T}
- name::Symbol
- max_limit::Float64
- marginal_cost::Float64
-end
-
-get_max_limit(prod::OperatingReserve) = prod.max_limit
+"""
+This struct contains data for the operating reserve product
+parameterized by the reserve direction (up or down).
+ name: ReserveUp or ReserveDown.
+ max_limit: Maximum percentage of reserve that can be provided by the project.
+ marginal_cost: Marginal cost per unit of reserve provided.
+"""
+struct OperatingReserve{T <: ReserveDirection} <: ReserveEMIS{T}
+ name::Symbol
+ max_limit::Float64
+ marginal_cost::Float64
+end
+
+get_max_limit(prod::OperatingReserve) = prod.max_limit
diff --git a/src/structs/products/Product.jl b/src/structs/products/Product.jl
index 9b8d393..581693b 100644
--- a/src/structs/products/Product.jl
+++ b/src/structs/products/Product.jl
@@ -1,9 +1,9 @@
-abstract type Product end
-abstract type OperatingProduct <: Product end
-abstract type ReserveDirection end
-abstract type ReserveUpEMIS <: ReserveDirection end
-abstract type ReserveDownEMIS <: ReserveDirection end
-abstract type ReserveEMIS{T<:ReserveDirection} <: OperatingProduct end
-
-get_name(prod::Product) = prod.name
-get_marginal_cost(prod::OperatingProduct) = prod.marginal_cost
+abstract type Product end
+abstract type OperatingProduct <: Product end
+abstract type ReserveDirection end
+abstract type ReserveUpEMIS <: ReserveDirection end
+abstract type ReserveDownEMIS <: ReserveDirection end
+abstract type ReserveEMIS{T<:ReserveDirection} <: OperatingProduct end
+
+get_name(prod::Product) = prod.name
+get_marginal_cost(prod::OperatingProduct) = prod.marginal_cost
diff --git a/src/structs/products/REC.jl b/src/structs/products/REC.jl
index 38fadec..12f48a2 100644
--- a/src/structs/products/REC.jl
+++ b/src/structs/products/REC.jl
@@ -1,36 +1,52 @@
-"""
-This struct contains data for the REC market product.
- name: REC.
- certificates: Expected output (number of certificates) for REC market bids
- rec_bid: REC market bidding price
-"""
-mutable struct REC <: Product
- name::Symbol
- certificates::Float64
- rec_bid::Float64
-end
-
-# Certificates and REC bids only returned when product is of type REC
-get_rec_certificates(prod::Product) = nothing
-get_rec_certificates(prod::REC) = prod.certificates
-get_rec_bid(prod::Product) = nothing
-get_rec_bid(prod::REC) = prod.rec_bid
-
-# REC bids and certificates only set when product is of type REC
-function set_rec_certificates!(prod::T, certificates) where T <: Product
- return
-end
-
-function set_rec_certificates!(prod::REC, certificates)
- prod.certificates = certificates
- return
-end
-
-function set_rec_bid!(prod::T, rec_bid) where T <: Product
- return
-end
-
-function set_rec_bid!(prod::REC, rec_bid)
- prod.rec_bid = rec_bid
- return
-end
+"""
+This struct contains data for the REC market product.
+ name: REC.
+ certificates: Expected output (number of certificates) for REC market bids
+ rec_bid: REC market bidding price
+"""
+mutable struct REC <: Product
+ name::Symbol
+ expected_certificates::Float64
+ correction_factor::Vector{Float64}
+ rec_bid::Float64
+end
+
+# Certificates and REC bids only returned when product is of type REC
+get_expected_rec_certificates(prod::Product) = nothing
+get_expected_rec_certificates(prod::REC) = prod.expected_certificates
+get_rec_correction_factor(prod::Product) = nothing
+get_rec_correction_factor(prod::REC) = prod.correction_factor
+get_rec_correction_factor(prod::Product, year::Int64) = nothing
+get_rec_correction_factor(prod::REC, year::Int64) = prod.correction_factor[year]
+get_rec_bid(prod::Product) = nothing
+get_rec_bid(prod::REC) = prod.rec_bid
+
+# REC bids and certificates only set when product is of type REC
+function set_expected_rec_certificates!(prod::T, certificates) where T <: Product
+ return
+end
+
+function set_expected_rec_certificates!(prod::REC, certificates)
+ prod.expected_certificates = certificates
+ return
+end
+
+function set_rec_bid!(prod::T, rec_bid) where T <: Product
+ return
+end
+
+function set_rec_bid!(prod::REC, rec_bid)
+ prod.rec_bid = rec_bid
+ return
+end
+
+# REC Production Correction Factor only set when product is of type REC
+function set_rec_correction_factor!(prod::T, year::Int64, value::Float64) where T <: Product
+ return
+end
+
+function set_rec_correction_factor!(prod::REC, year::Int64, value::Float64)
+ prod.correction_factor[year] = value
+ return
+end
+
diff --git a/src/test_system_parsers/rts_reader.jl b/src/test_system_parsers/rts_reader.jl
index 618785a..5521162 100644
--- a/src/test_system_parsers/rts_reader.jl
+++ b/src/test_system_parsers/rts_reader.jl
@@ -1,221 +1,221 @@
-"""
-This function reads the RTS-GMLC timeseries data and parses it into the format
-compatible with AgentSimulation
-"""
-
-function read_rts(data_dir::String,
- test_system_dir::String,
- base_dir::String,
- annual_growth::AxisArrays.AxisArray{Float64, 2},
- start_year::Int64,
- n_rep_days::Int64)
-
- test_system_load = DataFrames.DataFrame(CSV.File(joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "Load", "DAY_AHEAD_regional_Load.csv")))
- test_system_load_rt = DataFrames.DataFrame(CSV.File(joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "Load", "REAL_TIME_regional_Load.csv")))
-
- base_year = test_system_load[1, "Year"]
-
- average_annual_growth = [Statistics.mean(annual_growth[y, :] for y in 1:size(annual_growth)[1])]
-
- test_sys_num_hours = DataFrames.nrow(test_system_load)
- if test_sys_num_hours >= 8760
- test_sys_hour_weight = ones(test_sys_num_hours)
- else
- test_sys_hour_weight = ones(test_sys_num_hours) * 8760 / test_sys_num_hours
- end
-
- zone_numbers = names(test_system_load)[5:end]
- zones = ["zone_$(i)" for i in zone_numbers]
-
- reserve_params_df = read_data(joinpath(test_system_dir, "RTS_Data", "SourceData", "reserves.csv"))
-
- reserve_products = reserve_params_df[:, "Reserve Product"]
-
- test_system_reserves_data = Dict{String, DataFrames.DataFrame}()
-
- data_rows = DataFrames.nrow(test_system_load)
-
- for product in reserve_products
- test_system_reserves_data[product] = test_system_load[:, 1:4]
- test_system_reserves_data[product][:, product] = zeros(data_rows)
- data = DataFrames.DataFrame(CSV.File(joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "Reserves", "DAY_AHEAD_regional_$(product).csv")))
- for d in 1:Int(data_rows/24)
- for h in 1:24
- test_system_reserves_data[product][(d - 1) * 24 + h, product] = data[d, Symbol(h)]
- end
- end
- end
-
- scaled_test_sys_load = deepcopy(test_system_load)
- scaled_test_sys_load_rt = deepcopy(test_system_load_rt)
- scaled_test_system_reserves_data = deepcopy(test_system_reserves_data)
-
- scaled_test_sys_load[:, "Year"] = fill(start_year, data_rows)
- remove_leap_day!(scaled_test_sys_load, start_year)
-
- scaled_test_sys_load_rt[:, "Year"] = fill(start_year, DataFrames.nrow(test_system_load_rt))
- remove_leap_day!(scaled_test_sys_load_rt, start_year)
-
- for product in reserve_products
- scaled_test_system_reserves_data[product][:, "Year"] = fill(start_year, data_rows)
- remove_leap_day!(scaled_test_system_reserves_data[product], start_year)
- end
-
- for y in 1:(start_year - base_year)
- for zone in zone_numbers
- scaled_test_sys_load[:, "$(zone)"] = scaled_test_sys_load[:, "$(zone)"] * (1 + annual_growth[y, Symbol("load_zone_$(zone)")])
- scaled_test_sys_load_rt[:, "$(zone)"] = scaled_test_sys_load_rt[:, "$(zone)"] * (1 + annual_growth[y, Symbol("load_zone_$(zone)")])
- end
- for product in reserve_products
- scaled_test_system_reserves_data[product][:, product] = scaled_test_system_reserves_data[product][:, product] * (1 + average_annual_growth[y])
-
- end
- end
-
- net_load_df = scaled_test_sys_load[:, 1:4]
- net_load_df_rt = scaled_test_sys_load_rt[:, 1:4]
-
- for zone in zone_numbers
- net_load_df[:, "load_zone_$(zone)"] = scaled_test_sys_load[:, zone]
- net_load_df_rt[:, "load_zone_$(zone)"] = scaled_test_sys_load_rt[:, zone]
- end
-
- existing_generator_data = DataFrames.DataFrame(CSV.File(joinpath(test_system_dir, "RTS_Data", "SourceData", "gen.csv")))
-
- wind_timeseries_file = joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "WIND", "DAY_AHEAD_wind.csv")
- wind_timeseries_file_rt = joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "WIND", "REAL_TIME_wind.csv")
- if isfile(wind_timeseries_file)
- wind_timeseries_data = DataFrames.DataFrame(CSV.File(wind_timeseries_file))
- remove_leap_day!(wind_timeseries_data, start_year)
-
- wind_timeseries_data_rt = DataFrames.DataFrame(CSV.File(wind_timeseries_file_rt))
- remove_leap_day!(wind_timeseries_data_rt, start_year)
- end
-
- pv_timeseries_file = joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "PV", "DAY_AHEAD_pv.csv")
- pv_timeseries_file_rt = joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "PV", "REAL_TIME_pv.csv")
- if isfile(pv_timeseries_file)
- pv_timeseries_data = DataFrames.DataFrame(CSV.File(pv_timeseries_file))
- remove_leap_day!(pv_timeseries_data, start_year)
-
- pv_timeseries_data_rt = DataFrames.DataFrame(CSV.File(pv_timeseries_file_rt))
- remove_leap_day!(pv_timeseries_data_rt, start_year)
- end
-
- rtpv_timeseries_file = joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "RTPV", "DAY_AHEAD_rtpv.csv")
- rtpv_timeseries_file_rt = joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "RTPV", "REAL_TIME_rtpv.csv")
- if isfile(rtpv_timeseries_file)
- rtpv_timeseries_data = DataFrames.DataFrame(CSV.File(rtpv_timeseries_file))
- remove_leap_day!(rtpv_timeseries_data, start_year)
-
- rtpv_timeseries_data_rt = DataFrames.DataFrame(CSV.File(rtpv_timeseries_file_rt))
- remove_leap_day!(rtpv_timeseries_data_rt, start_year)
- end
-
- hydro_timeseries_file = joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "Hydro", "DAY_AHEAD_hydro.csv")
- hydro_timeseries_file_rt = joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "Hydro", "REAL_TIME_hydro.csv")
- if isfile(hydro_timeseries_file)
- hydro_timeseries_data = DataFrames.DataFrame(CSV.File(hydro_timeseries_file))
- remove_leap_day!(hydro_timeseries_data, start_year)
-
- hydro_timeseries_data_rt = DataFrames.DataFrame(CSV.File(hydro_timeseries_file_rt))
- remove_leap_day!(hydro_timeseries_data_rt, start_year)
- end
-
- gen_availability_df = scaled_test_sys_load[:, 1:4]
- gen_availability_df_rt = scaled_test_sys_load_rt[:, 1:4]
-
- for i in 1:DataFrames.nrow(existing_generator_data)
- if existing_generator_data[i, "Unit Type"] == "WIND"
- gen_availability_df[:, existing_generator_data[i, "GEN UID"]] = wind_timeseries_data[:, existing_generator_data[i, "GEN UID"]] / existing_generator_data[i, "PMax MW"]
- gen_availability_df_rt[:, existing_generator_data[i, "GEN UID"]] = wind_timeseries_data_rt[:, existing_generator_data[i, "GEN UID"]] / existing_generator_data[i, "PMax MW"]
-
- net_load_df[:, existing_generator_data[i, "GEN UID"]] = wind_timeseries_data[:, existing_generator_data[i, "GEN UID"]]
- net_load_df_rt[:, existing_generator_data[i, "GEN UID"]] = wind_timeseries_data_rt[:, existing_generator_data[i, "GEN UID"]]
- elseif existing_generator_data[i, "Unit Type"] == "PV"
- gen_availability_df[:, existing_generator_data[i, "GEN UID"]] = pv_timeseries_data[:, existing_generator_data[i, "GEN UID"]] / existing_generator_data[i, "PMax MW"]
- gen_availability_df_rt[:, existing_generator_data[i, "GEN UID"]] = pv_timeseries_data_rt[:, existing_generator_data[i, "GEN UID"]] / existing_generator_data[i, "PMax MW"]
-
- net_load_df[:, existing_generator_data[i, "GEN UID"]] = pv_timeseries_data[:, existing_generator_data[i, "GEN UID"]]
- net_load_df_rt[:, existing_generator_data[i, "GEN UID"]] = pv_timeseries_data_rt[:, existing_generator_data[i, "GEN UID"]]
- elseif existing_generator_data[i, "Unit Type"] == "RTPV"
- gen_availability_df[:, existing_generator_data[i, "GEN UID"]] = rtpv_timeseries_data[:, existing_generator_data[i, "GEN UID"]] / existing_generator_data[i, "PMax MW"]
- gen_availability_df_rt[:, existing_generator_data[i, "GEN UID"]] = rtpv_timeseries_data_rt[:, existing_generator_data[i, "GEN UID"]] / existing_generator_data[i, "PMax MW"]
- elseif existing_generator_data[i, "Unit Type"] == "HYDRO"
- gen_availability_df[:, existing_generator_data[i, "GEN UID"]] = hydro_timeseries_data[:, existing_generator_data[i, "GEN UID"]] / existing_generator_data[i, "PMax MW"]
- gen_availability_df_rt[:, existing_generator_data[i, "GEN UID"]] = hydro_timeseries_data_rt[:, existing_generator_data[i, "GEN UID"]] / existing_generator_data[i, "PMax MW"]
- else
- gen_availability_df[:, existing_generator_data[i, "GEN UID"]] = ones(DataFrames.nrow(gen_availability_df))
- gen_availability_df_rt[:, existing_generator_data[i, "GEN UID"]] = ones(DataFrames.nrow(gen_availability_df_rt))
- end
- end
-
- write_data(joinpath(data_dir, "timeseries_data_files", "Availability"), "DAY_AHEAD_availability.csv", gen_availability_df)
- write_data(joinpath(data_dir, "timeseries_data_files", "Availability"), "REAL_TIME_availability.csv", gen_availability_df_rt)
-
- write_data(joinpath(data_dir, "timeseries_data_files", "Net Load Data"), "load_n_vg_data.csv", net_load_df)
- write_data(joinpath(data_dir, "timeseries_data_files", "Net Load Data"), "load_n_vg_data_rt.csv", net_load_df_rt)
-
- representative_days = find_representative_days(data_dir, base_dir, n_rep_days)
- rep_load_data = filter(row -> in(Dates.Date(row[:Year], row[:Month], row[:Day]), keys(representative_days)), scaled_test_sys_load)
-
- rep_hour_weight = zeros(DataFrames.nrow(rep_load_data))
-
- for i in 1:length(rep_hour_weight)
- rep_hour_weight[i] = representative_days[Dates.Date(rep_load_data[i, :Year], rep_load_data[i, :Month], rep_load_data[i, :Day])]
- end
-
- write_data(joinpath(data_dir, "timeseries_data_files", "Load"), "load_0.csv", scaled_test_sys_load)
- write_data(joinpath(data_dir, "timeseries_data_files", "Load"), "rep_load_0.csv", rep_load_data)
-
- system_peak_load = maximum(sum(scaled_test_sys_load[:, zone] for zone in zone_numbers))
-
- rep_system_reserves_data = Dict{String, DataFrames.DataFrame}()
-
- for product in reserve_products
- rep_system_reserves_data[product] = filter(row -> in(Dates.Date(row[:Year], row[:Month], row[:Day]), keys(representative_days)), scaled_test_system_reserves_data[product])
- write_data(joinpath(data_dir, "timeseries_data_files", "Reserves"), "$(product)_0.csv", scaled_test_system_reserves_data[product])
- write_data(joinpath(data_dir, "timeseries_data_files", "Reserves"), "rep_$(product)_0.csv", rep_system_reserves_data[product])
- end
-
- # Create zonal lines
-
- zonal_lines = ZonalLine[]
-
- branches = read_data(joinpath(test_system_dir, "RTS_Data", "SourceData", "branch.csv"))
- dc_branches = read_data(joinpath(test_system_dir, "RTS_Data", "SourceData", "dc_branch.csv"))
-
- for b in 1:DataFrames.nrow(branches)
- from_bus = "$(branches[b, "From Bus"])"
- from_zone = "zone_$(first(from_bus, 1))"
-
- to_bus = "$(branches[b, "To Bus"])"
- to_zone = "zone_$(first(to_bus, 1))"
-
- similar_line = filter(l -> (in(from_zone, [get_from_zone(l), get_to_zone(l)]) && in(to_zone, [get_from_zone(l), get_to_zone(l)])), zonal_lines)
-
- if length(similar_line) < 1
- push!(zonal_lines, ZonalLine(branches[b, "UID"], from_zone, to_zone, branches[b, "Cont Rating"]))
- else
- set_active_power_limit!(similar_line[1], get_active_power_limit(similar_line[1]) + branches[b, "Cont Rating"])
- end
- end
-
- for b in 1:DataFrames.nrow(dc_branches)
- from_bus = "$(dc_branches[b, "From Bus"])"
- from_zone = "zone_$(first(from_bus, 1))"
-
- to_bus = "$(dc_branches[b, "To Bus"])"
- to_zone = "zone_$(first(to_bus, 1))"
-
- similar_line = filter(l -> (in(from_zone, [get_from_zone(l), get_to_zone(l)]) && in(to_zone, [get_from_zone(l), get_to_zone(l)])), zonal_lines)
-
- if length(similar_line) < 1
- push!(zonal_lines, ZonalLine(dc_branches[b, "UID"], from_zone, to_zone, dc_branches[b, "MW Load"]))
- else
- set_active_power_limit!(similar_line[1], get_active_power_limit(similar_line[1]) + dc_branches[b, "MW Load"])
- end
- end
-
- return zones, representative_days, rep_hour_weight, system_peak_load, test_sys_hour_weight, zonal_lines
-end
+"""
+This function reads the RTS-GMLC timeseries data and parses it into the format
+compatible with AgentSimulation
+"""
+
+function read_rts(data_dir::String,
+ test_system_dir::String,
+ base_dir::String,
+ annual_growth::AxisArrays.AxisArray{Float64, 2},
+ start_year::Int64,
+ n_rep_days::Int64)
+
+ test_system_load = DataFrames.DataFrame(CSV.File(joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "Load", "DAY_AHEAD_regional_Load.csv")))
+ test_system_load_rt = DataFrames.DataFrame(CSV.File(joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "Load", "REAL_TIME_regional_Load.csv")))
+
+ base_year = test_system_load[1, "Year"]
+
+ average_annual_growth = [Statistics.mean(annual_growth[y, :] for y in 1:size(annual_growth)[1])]
+
+ test_sys_num_hours = DataFrames.nrow(test_system_load)
+ if test_sys_num_hours >= 8760
+ test_sys_hour_weight = ones(test_sys_num_hours)
+ else
+ test_sys_hour_weight = ones(test_sys_num_hours) * 8760 / test_sys_num_hours
+ end
+
+ zone_numbers = names(test_system_load)[5:end]
+ zones = ["zone_$(i)" for i in zone_numbers]
+
+ reserve_params_df = read_data(joinpath(test_system_dir, "RTS_Data", "SourceData", "reserves.csv"))
+
+ reserve_products = reserve_params_df[:, "Reserve Product"]
+
+ test_system_reserves_data = Dict{String, DataFrames.DataFrame}()
+
+ data_rows = DataFrames.nrow(test_system_load)
+
+ for product in reserve_products
+ test_system_reserves_data[product] = test_system_load[:, 1:4]
+ test_system_reserves_data[product][:, product] = zeros(data_rows)
+ data = DataFrames.DataFrame(CSV.File(joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "Reserves", "DAY_AHEAD_regional_$(product).csv")))
+ for d in 1:Int(data_rows/24)
+ for h in 1:24
+ test_system_reserves_data[product][(d - 1) * 24 + h, product] = data[d, Symbol(h)]
+ end
+ end
+ end
+
+ scaled_test_sys_load = deepcopy(test_system_load)
+ scaled_test_sys_load_rt = deepcopy(test_system_load_rt)
+ scaled_test_system_reserves_data = deepcopy(test_system_reserves_data)
+
+ scaled_test_sys_load[:, "Year"] = fill(start_year, data_rows)
+ remove_leap_day!(scaled_test_sys_load, start_year)
+
+ scaled_test_sys_load_rt[:, "Year"] = fill(start_year, DataFrames.nrow(test_system_load_rt))
+ remove_leap_day!(scaled_test_sys_load_rt, start_year)
+
+ for product in reserve_products
+ scaled_test_system_reserves_data[product][:, "Year"] = fill(start_year, data_rows)
+ remove_leap_day!(scaled_test_system_reserves_data[product], start_year)
+ end
+
+ for y in 1:(start_year - base_year)
+ for zone in zone_numbers
+ scaled_test_sys_load[:, "$(zone)"] = scaled_test_sys_load[:, "$(zone)"] .* (1 + annual_growth["load_zone_$(zone)",y])
+ scaled_test_sys_load_rt[:, "$(zone)"] = scaled_test_sys_load_rt[:, "$(zone)"] .* (1 + annual_growth["load_zone_$(zone)",y])
+ end
+ for product in reserve_products
+ scaled_test_system_reserves_data[product][:, product] = scaled_test_system_reserves_data[product][:, product] * (1 + average_annual_growth[1][y])
+
+ end
+ end
+
+ net_load_df = scaled_test_sys_load[:, 1:4]
+ net_load_df_rt = scaled_test_sys_load_rt[:, 1:4]
+
+ for zone in zone_numbers
+ net_load_df[:, "load_zone_$(zone)"] = scaled_test_sys_load[:, zone]
+ net_load_df_rt[:, "load_zone_$(zone)"] = scaled_test_sys_load_rt[:, zone]
+ end
+
+ existing_generator_data = DataFrames.DataFrame(CSV.File(joinpath(test_system_dir, "RTS_Data", "SourceData", "gen.csv")))
+
+ wind_timeseries_file = joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "WIND", "DAY_AHEAD_wind.csv")
+ wind_timeseries_file_rt = joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "WIND", "REAL_TIME_wind.csv")
+ if isfile(wind_timeseries_file)
+ wind_timeseries_data = DataFrames.DataFrame(CSV.File(wind_timeseries_file))
+ remove_leap_day!(wind_timeseries_data, start_year)
+
+ wind_timeseries_data_rt = DataFrames.DataFrame(CSV.File(wind_timeseries_file_rt))
+ remove_leap_day!(wind_timeseries_data_rt, start_year)
+ end
+
+ pv_timeseries_file = joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "PV", "DAY_AHEAD_pv.csv")
+ pv_timeseries_file_rt = joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "PV", "REAL_TIME_pv.csv")
+ if isfile(pv_timeseries_file)
+ pv_timeseries_data = DataFrames.DataFrame(CSV.File(pv_timeseries_file))
+ remove_leap_day!(pv_timeseries_data, start_year)
+
+ pv_timeseries_data_rt = DataFrames.DataFrame(CSV.File(pv_timeseries_file_rt))
+ remove_leap_day!(pv_timeseries_data_rt, start_year)
+ end
+
+ rtpv_timeseries_file = joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "RTPV", "DAY_AHEAD_rtpv.csv")
+ rtpv_timeseries_file_rt = joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "RTPV", "REAL_TIME_rtpv.csv")
+ if isfile(rtpv_timeseries_file)
+ rtpv_timeseries_data = DataFrames.DataFrame(CSV.File(rtpv_timeseries_file))
+ remove_leap_day!(rtpv_timeseries_data, start_year)
+
+ rtpv_timeseries_data_rt = DataFrames.DataFrame(CSV.File(rtpv_timeseries_file_rt))
+ remove_leap_day!(rtpv_timeseries_data_rt, start_year)
+ end
+
+ hydro_timeseries_file = joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "Hydro", "DAY_AHEAD_hydro.csv")
+ hydro_timeseries_file_rt = joinpath(test_system_dir, "RTS_Data", "timeseries_data_files", "Hydro", "REAL_TIME_hydro.csv")
+ if isfile(hydro_timeseries_file)
+ hydro_timeseries_data = DataFrames.DataFrame(CSV.File(hydro_timeseries_file))
+ remove_leap_day!(hydro_timeseries_data, start_year)
+
+ hydro_timeseries_data_rt = DataFrames.DataFrame(CSV.File(hydro_timeseries_file_rt))
+ remove_leap_day!(hydro_timeseries_data_rt, start_year)
+ end
+
+ gen_availability_df = scaled_test_sys_load[:, 1:4]
+ gen_availability_df_rt = scaled_test_sys_load_rt[:, 1:4]
+
+ for i in 1:DataFrames.nrow(existing_generator_data)
+ if existing_generator_data[i, "Unit Type"] == "WIND"
+ gen_availability_df[:, existing_generator_data[i, "GEN UID"]] = wind_timeseries_data[:, existing_generator_data[i, "GEN UID"]] / existing_generator_data[i, "PMax MW"]
+ gen_availability_df_rt[:, existing_generator_data[i, "GEN UID"]] = wind_timeseries_data_rt[:, existing_generator_data[i, "GEN UID"]] / existing_generator_data[i, "PMax MW"]
+
+ net_load_df[:, existing_generator_data[i, "GEN UID"]] = wind_timeseries_data[:, existing_generator_data[i, "GEN UID"]]
+ net_load_df_rt[:, existing_generator_data[i, "GEN UID"]] = wind_timeseries_data_rt[:, existing_generator_data[i, "GEN UID"]]
+ elseif existing_generator_data[i, "Unit Type"] == "PV"
+ gen_availability_df[:, existing_generator_data[i, "GEN UID"]] = pv_timeseries_data[:, existing_generator_data[i, "GEN UID"]] / existing_generator_data[i, "PMax MW"]
+ gen_availability_df_rt[:, existing_generator_data[i, "GEN UID"]] = pv_timeseries_data_rt[:, existing_generator_data[i, "GEN UID"]] / existing_generator_data[i, "PMax MW"]
+
+ net_load_df[:, existing_generator_data[i, "GEN UID"]] = pv_timeseries_data[:, existing_generator_data[i, "GEN UID"]]
+ net_load_df_rt[:, existing_generator_data[i, "GEN UID"]] = pv_timeseries_data_rt[:, existing_generator_data[i, "GEN UID"]]
+ elseif existing_generator_data[i, "Unit Type"] == "RTPV"
+ gen_availability_df[:, existing_generator_data[i, "GEN UID"]] = rtpv_timeseries_data[:, existing_generator_data[i, "GEN UID"]] / existing_generator_data[i, "PMax MW"]
+ gen_availability_df_rt[:, existing_generator_data[i, "GEN UID"]] = rtpv_timeseries_data_rt[:, existing_generator_data[i, "GEN UID"]] / existing_generator_data[i, "PMax MW"]
+ elseif existing_generator_data[i, "Unit Type"] == "HYDRO" || existing_generator_data[i, "Unit Type"] == "ROR"
+ gen_availability_df[:, existing_generator_data[i, "GEN UID"]] = hydro_timeseries_data[:, existing_generator_data[i, "GEN UID"]] / existing_generator_data[i, "PMax MW"]
+ gen_availability_df_rt[:, existing_generator_data[i, "GEN UID"]] = hydro_timeseries_data_rt[:, existing_generator_data[i, "GEN UID"]] / existing_generator_data[i, "PMax MW"]
+ else
+ gen_availability_df[:, existing_generator_data[i, "GEN UID"]] = ones(DataFrames.nrow(gen_availability_df))
+ gen_availability_df_rt[:, existing_generator_data[i, "GEN UID"]] = ones(DataFrames.nrow(gen_availability_df_rt))
+ end
+ end
+
+ write_data(joinpath(data_dir, "timeseries_data_files", "Availability"), "DAY_AHEAD_availability.csv", gen_availability_df)
+ write_data(joinpath(data_dir, "timeseries_data_files", "Availability"), "REAL_TIME_availability.csv", gen_availability_df_rt)
+
+ write_data(joinpath(data_dir, "timeseries_data_files", "Net Load Data"), "load_n_vg_data.csv", net_load_df)
+ write_data(joinpath(data_dir, "timeseries_data_files", "Net Load Data"), "load_n_vg_data_rt.csv", net_load_df_rt)
+
+ representative_days = find_representative_days(data_dir, test_system_dir, base_dir, n_rep_days)
+ rep_load_data = filter(row -> in(Dates.Date(row[:Year], row[:Month], row[:Day]), keys(representative_days)), scaled_test_sys_load)
+
+ rep_hour_weight = zeros(DataFrames.nrow(rep_load_data))
+
+ for i in 1:length(rep_hour_weight)
+ rep_hour_weight[i] = representative_days[Dates.Date(rep_load_data[i, :Year], rep_load_data[i, :Month], rep_load_data[i, :Day])]
+ end
+
+ write_data(joinpath(data_dir, "timeseries_data_files", "Load"), "load_0.csv", scaled_test_sys_load)
+ write_data(joinpath(data_dir, "timeseries_data_files", "Load"), "rep_load_0.csv", rep_load_data)
+
+ system_peak_load = maximum(sum(scaled_test_sys_load[:, zone] for zone in zone_numbers))
+
+ rep_system_reserves_data = Dict{String, DataFrames.DataFrame}()
+
+ for product in reserve_products
+ rep_system_reserves_data[product] = filter(row -> in(Dates.Date(row[:Year], row[:Month], row[:Day]), keys(representative_days)), scaled_test_system_reserves_data[product])
+ write_data(joinpath(data_dir, "timeseries_data_files", "Reserves"), "$(product)_0.csv", scaled_test_system_reserves_data[product])
+ write_data(joinpath(data_dir, "timeseries_data_files", "Reserves"), "rep_$(product)_0.csv", rep_system_reserves_data[product])
+ end
+
+ # Create zonal lines
+
+ zonal_lines = ZonalLine[]
+
+ branches = read_data(joinpath(test_system_dir, "RTS_Data", "SourceData", "branch.csv"))
+ dc_branches = read_data(joinpath(test_system_dir, "RTS_Data", "SourceData", "dc_branch.csv"))
+
+ for b in 1:DataFrames.nrow(branches)
+ from_bus = "$(branches[b, "From Bus"])"
+ from_zone = "zone_$(first(from_bus, 1))"
+
+ to_bus = "$(branches[b, "To Bus"])"
+ to_zone = "zone_$(first(to_bus, 1))"
+
+ similar_line = filter(l -> (in(from_zone, [get_from_zone(l), get_to_zone(l)]) && in(to_zone, [get_from_zone(l), get_to_zone(l)])), zonal_lines)
+
+ if length(similar_line) < 1
+ push!(zonal_lines, ZonalLine(branches[b, "UID"], from_zone, to_zone, branches[b, "Cont Rating"]))
+ else
+ set_active_power_limit!(similar_line[1], get_active_power_limit(similar_line[1]) + branches[b, "Cont Rating"])
+ end
+ end
+
+ for b in 1:DataFrames.nrow(dc_branches)
+ from_bus = "$(dc_branches[b, "From Bus"])"
+ from_zone = "zone_$(first(from_bus, 1))"
+
+ to_bus = "$(dc_branches[b, "To Bus"])"
+ to_zone = "zone_$(first(to_bus, 1))"
+
+ similar_line = filter(l -> (in(from_zone, [get_from_zone(l), get_to_zone(l)]) && in(to_zone, [get_from_zone(l), get_to_zone(l)])), zonal_lines)
+
+ if length(similar_line) < 1
+ push!(zonal_lines, ZonalLine(dc_branches[b, "UID"], from_zone, to_zone, dc_branches[b, "MW Load"]))
+ else
+ set_active_power_limit!(similar_line[1], get_active_power_limit(similar_line[1]) + dc_branches[b, "MW Load"])
+ end
+ end
+
+ return zones, representative_days, rep_hour_weight, system_peak_load, test_sys_hour_weight, zonal_lines
+end
diff --git a/src/test_system_parsers/test_system_reader.jl b/src/test_system_parsers/test_system_reader.jl
index 0b950dd..7b4c6f9 100644
--- a/src/test_system_parsers/test_system_reader.jl
+++ b/src/test_system_parsers/test_system_reader.jl
@@ -1,34 +1,34 @@
-"""
-This function reads the test system time series and returns representative hour weight.
-"""
-function read_test_system(data_dir::String,
- test_system_dir::String,
- base_dir::String,
- annual_growth::AxisArrays.AxisArray{Float64, 2},
- start_year::Int64,
- n_rep_days::Int64)
-
- test_sys_hour_weight = nothing
- zones = nothing
- representative_days = nothing
- rep_hour_weight = nothing
- system_peak_load = nothing
- test_sys_hour_weight = nothing
- zonal_lines = nothing
-
- if occursin("RTS", test_system_dir)
- zones,
- representative_days,
- rep_hour_weight,
- system_peak_load,
- test_sys_hour_weight,
- zonal_lines = read_rts(data_dir,
- test_system_dir,
- base_dir,
- annual_growth,
- start_year,
- n_rep_days)
- end
-
- return zones, representative_days, rep_hour_weight, system_peak_load, test_sys_hour_weight, zonal_lines
-end
+"""
+This function reads the test system time series and returns representative hour weight.
+"""
+function read_test_system(data_dir::String,
+ test_system_dir::String,
+ base_dir::String,
+ annual_growth::AxisArrays.AxisArray{Float64, 2},
+ start_year::Int64,
+ n_rep_days::Int64)
+
+ test_sys_hour_weight = nothing
+ zones = nothing
+ representative_days = nothing
+ rep_hour_weight = nothing
+ system_peak_load = nothing
+ test_sys_hour_weight = nothing
+ zonal_lines = nothing
+
+ if occursin("RTS", test_system_dir)
+ zones,
+ representative_days,
+ rep_hour_weight,
+ system_peak_load,
+ test_sys_hour_weight,
+ zonal_lines = read_rts(data_dir,
+ test_system_dir,
+ base_dir,
+ annual_growth,
+ start_year,
+ n_rep_days)
+ end
+
+ return zones, representative_days, rep_hour_weight, system_peak_load, test_sys_hour_weight, zonal_lines
+end
diff --git a/src/utils/conversion_utils.jl b/src/utils/conversion_utils.jl
index 4a247c0..29c93ad 100644
--- a/src/utils/conversion_utils.jl
+++ b/src/utils/conversion_utils.jl
@@ -1,487 +1,487 @@
-function convert(::Type{Project{Existing}}, gen::ThermalGenEMIS{BuildPhase})
- return ThermalGenEMIS{Existing}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Planned}}, gen::ThermalGenEMIS{BuildPhase})
- return ThermalGenEMIS{Planned}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Queue}}, gen::ThermalGenEMIS{BuildPhase})
- return ThermalGenEMIS{Queue}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Option}}, gen::ThermalGenEMIS{BuildPhase})
- return ThermalGenEMIS{Option}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Retired}}, gen::ThermalGenEMIS{BuildPhase})
- return ThermalGenEMIS{Retired}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Existing}}, gen::RenewableGenEMIS{BuildPhase})
- return RenewableGenEMIS{Existing}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Planned}}, gen::RenewableGenEMIS{BuildPhase})
- return RenewableGenEMIS{Planned}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Queue}}, gen::RenewableGenEMIS{BuildPhase})
- return RenewableGenEMIS{Queue}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Option}}, gen::RenewableGenEMIS{BuildPhase})
- return RenewableGenEMIS{Option}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Retired}}, gen::RenewableGenEMIS{BuildPhase})
- return RenewableGenEMIS{Retired}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Existing}}, gen::HydroGenEMIS{BuildPhase})
- return HydroGenEMIS{Existing}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Planned}}, gen::HydroGenEMIS{BuildPhase})
- return HydroGenEMIS{Planned}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Queue}}, gen::HydroGenEMIS{BuildPhase})
- return HydroGenEMIS{Queue}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Option}}, gen::HydroGenEMIS{BuildPhase})
- return HydroGenEMIS{Option}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Retired}}, gen::HydroGenEMIS{BuildPhase})
- return HydroGenEMIS{Retired}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Existing}}, battery::BatteryEMIS{BuildPhase})
- return BatteryEMIS{Existing}(battery.name,
- battery.tech,
- battery.decision_year,
- battery.construction_year,
- battery.retirement_year,
- battery.end_life_year,
- battery.products,
- battery.finance_data)
-end
-
-function convert(::Type{Project{Planned}}, battery::BatteryEMIS{BuildPhase})
- return BatteryEMIS{Planned}(battery.name,
- battery.tech,
- battery.decision_year,
- battery.construction_year,
- battery.retirement_year,
- battery.end_life_year,
- battery.products,
- battery.finance_data)
-end
-
-function convert(::Type{Project{Queue}}, battery::BatteryEMIS{BuildPhase})
- return BatteryEMIS{Queue}(battery.name,
- battery.tech,
- battery.decision_year,
- battery.construction_year,
- battery.retirement_year,
- battery.end_life_year,
- battery.products,
- battery.finance_data)
-end
-
-function convert(::Type{Project{Option}}, battery::BatteryEMIS{BuildPhase})
- return BatteryEMIS{Option}(battery.name,
- battery.tech,
- battery.decision_year,
- battery.construction_year,
- battery.retirement_year,
- battery.end_life_year,
- battery.products,
- battery.finance_data)
-end
-
-function convert(::Type{Project{Retired}}, battery::BatteryEMIS{BuildPhase})
- return BatteryEMIS{Retired}(battery.name,
- battery.tech,
- battery.decision_year,
- battery.construction_year,
- battery.retirement_year,
- battery.end_life_year,
- battery.products,
- battery.finance_data)
-end
-
-
-function convert(::Type{Project{Queue}}, gen::ThermalGenEMIS{Option})
- return ThermalGenEMIS{Queue}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Planned}}, gen::ThermalGenEMIS{Queue})
- return ThermalGenEMIS{Planned}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Existing}}, gen::ThermalGenEMIS{Planned})
- return ThermalGenEMIS{Existing}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-
-function convert(::Type{Project{Retired}}, gen::ThermalGenEMIS{Existing})
- return ThermalGenEMIS{Retired}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Retired}}, gen::ThermalGenEMIS{Planned})
- return ThermalGenEMIS{Retired}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Retired}}, gen::ThermalGenEMIS{Queue})
- return ThermalGenEMIS{Retired}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Queue}}, gen::RenewableGenEMIS{Option})
- return RenewableGenEMIS{Queue}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Planned}}, gen::RenewableGenEMIS{Queue})
- return RenewableGenEMIS{Planned}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Existing}}, gen::RenewableGenEMIS{Planned})
- return RenewableGenEMIS{Existing}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-
-function convert(::Type{Project{Retired}}, gen::RenewableGenEMIS{Existing})
- return RenewableGenEMIS{Retired}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Retired}}, gen::RenewableGenEMIS{Planned})
- return RenewableGenEMIS{Retired}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Retired}}, gen::RenewableGenEMIS{Queue})
- return RenewableGenEMIS{Retired}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Queue}}, gen::HydroGenEMIS{Option})
- return HydroGenEMIS{Queue}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Planned}}, gen::HydroGenEMIS{Queue})
- return HydroGenEMIS{Planned}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Existing}}, gen::HydroGenEMIS{Planned})
- return HydroGenEMIS{Existing}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Retired}}, gen::HydroGenEMIS{Existing})
- return HydroGenEMIS{Retired}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Retired}}, gen::HydroGenEMIS{Planned})
- return HydroGenEMIS{Retired}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Retired}}, gen::HydroGenEMIS{Queue})
- return HydroGenEMIS{Retired}(gen.name,
- gen.tech,
- gen.decision_year,
- gen.construction_year,
- gen.retirement_year,
- gen.end_life_year,
- gen.products,
- gen.finance_data)
-end
-
-function convert(::Type{Project{Queue}}, battery::BatteryEMIS{Option})
- return BatteryEMIS{Queue}(battery.name,
- battery.tech,
- battery.decision_year,
- battery.construction_year,
- battery.retirement_year,
- battery.end_life_year,
- battery.products,
- battery.finance_data)
-end
-
-function convert(::Type{Project{Planned}}, battery::BatteryEMIS{Queue})
- return BatteryEMIS{Planned}(battery.name,
- battery.tech,
- battery.decision_year,
- battery.construction_year,
- battery.retirement_year,
- battery.end_life_year,
- battery.products,
- battery.finance_data)
-end
-
-function convert(::Type{Project{Existing}}, battery::BatteryEMIS{Planned})
- return BatteryEMIS{Existing}(battery.name,
- battery.tech,
- battery.decision_year,
- battery.construction_year,
- battery.retirement_year,
- battery.end_life_year,
- battery.products,
- battery.finance_data)
-end
-
-
-function convert(::Type{Project{Retired}}, battery::BatteryEMIS{Existing})
- return BatteryEMIS{Retired}(battery.name,
- battery.tech,
- battery.decision_year,
- battery.construction_year,
- battery.retirement_year,
- battery.end_life_year,
- battery.products,
- battery.finance_data)
-end
-
-function convert(::Type{Project{Retired}}, battery::BatteryEMIS{Planned})
- return BatteryEMIS{Retired}(battery.name,
- battery.tech,
- battery.decision_year,
- battery.construction_year,
- battery.retirement_year,
- battery.end_life_year,
- battery.products,
- battery.finance_data)
-end
-
-function convert(::Type{Project{Retired}}, battery::BatteryEMIS{Queue})
- return BatteryEMIS{Retired}(battery.name,
- battery.tech,
- battery.decision_year,
- battery.construction_year,
- battery.retirement_year,
- battery.end_life_year,
- battery.products,
- battery.finance_data)
-end
+function convert(::Type{Project{Existing}}, gen::ThermalGenEMIS{BuildPhase})
+ return ThermalGenEMIS{Existing}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Planned}}, gen::ThermalGenEMIS{BuildPhase})
+ return ThermalGenEMIS{Planned}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Queue}}, gen::ThermalGenEMIS{BuildPhase})
+ return ThermalGenEMIS{Queue}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Option}}, gen::ThermalGenEMIS{BuildPhase})
+ return ThermalGenEMIS{Option}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Retired}}, gen::ThermalGenEMIS{BuildPhase})
+ return ThermalGenEMIS{Retired}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Existing}}, gen::RenewableGenEMIS{BuildPhase})
+ return RenewableGenEMIS{Existing}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Planned}}, gen::RenewableGenEMIS{BuildPhase})
+ return RenewableGenEMIS{Planned}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Queue}}, gen::RenewableGenEMIS{BuildPhase})
+ return RenewableGenEMIS{Queue}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Option}}, gen::RenewableGenEMIS{BuildPhase})
+ return RenewableGenEMIS{Option}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Retired}}, gen::RenewableGenEMIS{BuildPhase})
+ return RenewableGenEMIS{Retired}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Existing}}, gen::HydroGenEMIS{BuildPhase})
+ return HydroGenEMIS{Existing}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Planned}}, gen::HydroGenEMIS{BuildPhase})
+ return HydroGenEMIS{Planned}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Queue}}, gen::HydroGenEMIS{BuildPhase})
+ return HydroGenEMIS{Queue}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Option}}, gen::HydroGenEMIS{BuildPhase})
+ return HydroGenEMIS{Option}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Retired}}, gen::HydroGenEMIS{BuildPhase})
+ return HydroGenEMIS{Retired}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Existing}}, battery::BatteryEMIS{BuildPhase})
+ return BatteryEMIS{Existing}(battery.name,
+ battery.tech,
+ battery.decision_year,
+ battery.construction_year,
+ battery.retirement_year,
+ battery.end_life_year,
+ battery.products,
+ battery.finance_data)
+end
+
+function convert(::Type{Project{Planned}}, battery::BatteryEMIS{BuildPhase})
+ return BatteryEMIS{Planned}(battery.name,
+ battery.tech,
+ battery.decision_year,
+ battery.construction_year,
+ battery.retirement_year,
+ battery.end_life_year,
+ battery.products,
+ battery.finance_data)
+end
+
+function convert(::Type{Project{Queue}}, battery::BatteryEMIS{BuildPhase})
+ return BatteryEMIS{Queue}(battery.name,
+ battery.tech,
+ battery.decision_year,
+ battery.construction_year,
+ battery.retirement_year,
+ battery.end_life_year,
+ battery.products,
+ battery.finance_data)
+end
+
+function convert(::Type{Project{Option}}, battery::BatteryEMIS{BuildPhase})
+ return BatteryEMIS{Option}(battery.name,
+ battery.tech,
+ battery.decision_year,
+ battery.construction_year,
+ battery.retirement_year,
+ battery.end_life_year,
+ battery.products,
+ battery.finance_data)
+end
+
+function convert(::Type{Project{Retired}}, battery::BatteryEMIS{BuildPhase})
+ return BatteryEMIS{Retired}(battery.name,
+ battery.tech,
+ battery.decision_year,
+ battery.construction_year,
+ battery.retirement_year,
+ battery.end_life_year,
+ battery.products,
+ battery.finance_data)
+end
+
+
+function convert(::Type{Project{Queue}}, gen::ThermalGenEMIS{Option})
+ return ThermalGenEMIS{Queue}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Planned}}, gen::ThermalGenEMIS{Queue})
+ return ThermalGenEMIS{Planned}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Existing}}, gen::ThermalGenEMIS{Planned})
+ return ThermalGenEMIS{Existing}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+
+function convert(::Type{Project{Retired}}, gen::ThermalGenEMIS{Existing})
+ return ThermalGenEMIS{Retired}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Retired}}, gen::ThermalGenEMIS{Planned})
+ return ThermalGenEMIS{Retired}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Retired}}, gen::ThermalGenEMIS{Queue})
+ return ThermalGenEMIS{Retired}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Queue}}, gen::RenewableGenEMIS{Option})
+ return RenewableGenEMIS{Queue}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Planned}}, gen::RenewableGenEMIS{Queue})
+ return RenewableGenEMIS{Planned}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Existing}}, gen::RenewableGenEMIS{Planned})
+ return RenewableGenEMIS{Existing}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+
+function convert(::Type{Project{Retired}}, gen::RenewableGenEMIS{Existing})
+ return RenewableGenEMIS{Retired}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Retired}}, gen::RenewableGenEMIS{Planned})
+ return RenewableGenEMIS{Retired}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Retired}}, gen::RenewableGenEMIS{Queue})
+ return RenewableGenEMIS{Retired}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Queue}}, gen::HydroGenEMIS{Option})
+ return HydroGenEMIS{Queue}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Planned}}, gen::HydroGenEMIS{Queue})
+ return HydroGenEMIS{Planned}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Existing}}, gen::HydroGenEMIS{Planned})
+ return HydroGenEMIS{Existing}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Retired}}, gen::HydroGenEMIS{Existing})
+ return HydroGenEMIS{Retired}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Retired}}, gen::HydroGenEMIS{Planned})
+ return HydroGenEMIS{Retired}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Retired}}, gen::HydroGenEMIS{Queue})
+ return HydroGenEMIS{Retired}(gen.name,
+ gen.tech,
+ gen.decision_year,
+ gen.construction_year,
+ gen.retirement_year,
+ gen.end_life_year,
+ gen.products,
+ gen.finance_data)
+end
+
+function convert(::Type{Project{Queue}}, battery::BatteryEMIS{Option})
+ return BatteryEMIS{Queue}(battery.name,
+ battery.tech,
+ battery.decision_year,
+ battery.construction_year,
+ battery.retirement_year,
+ battery.end_life_year,
+ battery.products,
+ battery.finance_data)
+end
+
+function convert(::Type{Project{Planned}}, battery::BatteryEMIS{Queue})
+ return BatteryEMIS{Planned}(battery.name,
+ battery.tech,
+ battery.decision_year,
+ battery.construction_year,
+ battery.retirement_year,
+ battery.end_life_year,
+ battery.products,
+ battery.finance_data)
+end
+
+function convert(::Type{Project{Existing}}, battery::BatteryEMIS{Planned})
+ return BatteryEMIS{Existing}(battery.name,
+ battery.tech,
+ battery.decision_year,
+ battery.construction_year,
+ battery.retirement_year,
+ battery.end_life_year,
+ battery.products,
+ battery.finance_data)
+end
+
+
+function convert(::Type{Project{Retired}}, battery::BatteryEMIS{Existing})
+ return BatteryEMIS{Retired}(battery.name,
+ battery.tech,
+ battery.decision_year,
+ battery.construction_year,
+ battery.retirement_year,
+ battery.end_life_year,
+ battery.products,
+ battery.finance_data)
+end
+
+function convert(::Type{Project{Retired}}, battery::BatteryEMIS{Planned})
+ return BatteryEMIS{Retired}(battery.name,
+ battery.tech,
+ battery.decision_year,
+ battery.construction_year,
+ battery.retirement_year,
+ battery.end_life_year,
+ battery.products,
+ battery.finance_data)
+end
+
+function convert(::Type{Project{Retired}}, battery::BatteryEMIS{Queue})
+ return BatteryEMIS{Retired}(battery.name,
+ battery.tech,
+ battery.decision_year,
+ battery.construction_year,
+ battery.retirement_year,
+ battery.end_life_year,
+ battery.products,
+ battery.finance_data)
+end
diff --git a/src/utils/finance_utils.jl b/src/utils/finance_utils.jl
index 97abfa0..33f65c6 100644
--- a/src/utils/finance_utils.jl
+++ b/src/utils/finance_utils.jl
@@ -1,149 +1,149 @@
-"""
-This function checks if value is in input dictionary.
-"""
-function check_input_for_variable(dict, name, default = 0)
- var = default
- if haskey(dict, name)
- var = dict[name]
- end
- return var
-end
-
-"""
-This function calculates the adjusted capital cost of a project.
-"""
-function calculate_capital_costs(inputs)
- #capital costs are the cost of getting the plant to completion, minus incentive benefits, minus tax benefits
- plant_cost = calculate_plant_cost(inputs["cost_inputs"])
- incentives = calculate_incentive_benefits(inputs["incentive_inputs"], plant_cost)
- tax_benefits = calculate_tax_benefits(inputs["tax_inputs"], plant_cost, inputs["incentive_inputs"])
- return(plant_cost - incentives - tax_benefits)
-end
-
-"""
-This function adds the total construction and development costs of a project.
-"""
-function calculate_plant_cost(cost_inputs)
- return sum(cost_inputs)
-end
-
-"""
-This function returns the MACRS depreciation schedule of a project.
-"""
-function get_macrs_schedule(num_depreciation_years, macrs_df)
- return macrs_df[:, Symbol("Y"*string(num_depreciation_years))]
-end
-
-"""
-This function calculates the incentive benefits which can come from grants and investment tax credits
- grants are in dollars/KW of capacity
- itc is in terms of percent of plant cost
- itc_recovery_factor scales the percent of capital costs which can be covered by the itc. This can be due to transaction frictions.
-"""
-function calculate_incentive_benefits(incentive_inputs, plant_cost)
- grants = check_input_for_variable(incentive_inputs, "grants", 0)
- itc = check_input_for_variable(incentive_inputs, "itc", 0)
- itc_recovery_factor = check_input_for_variable(incentive_inputs, "itc_recovery_factor", 1)
- return grants + itc*itc_recovery_factor*plant_cost
-end
-"""
-
-This function calculates the tax benefits which are from depreciating the capital asset. tax inputs include:
- macrs: the depreciation schedule in percent for the asset
- depreciable_percent: fraction of plant costs which can be depreciated
- discount_rate: used to transform future tax benefits to present value
- tax_rate: tax percent which is deducted.
-"""
-function calculate_tax_benefits(tax_inputs, plant_cost, incentive_inputs)
- #
- macrs = check_input_for_variable(tax_inputs, "macrs", [0])
- itc = check_input_for_variable(incentive_inputs, "itc", 0)
- depreciable_percent = check_input_for_variable(tax_inputs, "depreciable_percent", 1)
- tax_rate = check_input_for_variable(tax_inputs, "tax_rate", .25)
- discount_rate = check_input_for_variable(tax_inputs, "discount_rate", 0.08)
- #
- return calculate_present_value(plant_cost*depreciable_percent*(1-itc/2)*tax_rate*macrs, discount_rate)
-end
-
-"""
-This function calculates the present value which takes yearly values and a discount rate and returns the present value.
-"""
-function calculate_present_value(values, discount_rate)
- return sum([values[i]*(1+discount_rate)^(-i) for i in (1:length(values))])
-end
-
-"""
-This function calculates calculates the weighted average cost of capital
- Takes a list with: Tax rate, debt interest rate, debt fraction, and either the equity discount rate or the required values to calculate a CAPM model
-"""
-function calculate_wacc(wacc_inputs)
- tax_rate = check_input_for_variable(wacc_inputs, "tax_rate", 0.25)
- debt_fraction = check_input_for_variable(wacc_inputs, "debt_fraction", 0.6)
- debt_interest_rate = check_input_for_variable(wacc_inputs, "debt_interest_rate", 0.05)
- equity_discount_rate = check_input_for_variable(wacc_inputs, "equity_discount_rate", 0.12)
- #
- debt_contribution = (1- tax_rate)*debt_interest_rate*debt_fraction
- equity_contribution = equity_discount_rate*(1-debt_fraction)
- return (debt_contribution + equity_contribution)
-end
-#_________________________________________________________________________________________________________________________________________________
-"""
-This function calculates a CAPM model equity rate = risk-free rate + beta*(market return - risk_free rate) + project-specific adder
-"""
-function calculate_equity_discount_rate(inputs)
- project_specific_adder = check_input_for_variable(inputs, "project_specific_adder", 0)
- risk_free_rate = check_input_for_variable(inputs, "risk_free_rate", 0.04)
- beta = check_input_for_variable(inputs, "beta", 1)
- market_return = check_input_for_variable(inputs, "market_return", .12)
- return (risk_free_rate + beta*(market_return - risk_free_rate) + project_specific_adder)
-end
-
-"""
-This function utilizes all the finance functions to return the WACC and adjusted capital cost of a project.
-"""
-function calculate_capcost_and_wacc(project_capex::Vector{Float64},
- category::String,
- investor_dir::String,
- online_year::Int64)
- finance_data = read_data(joinpath(investor_dir, "finance_params.csv"))
- investor_characteristics = read_data(joinpath(investor_dir, "characteristics.csv"))
-
- project_row = findfirst(x-> x == category, finance_data[:, "Category"])
- macrs_df = read_data(joinpath(investor_dir, "MACRS Schedule.csv"))
-
- equity_discount_rate = finance_data[project_row, "EQUITY_RATE"]
-
- if online_year <= 2020
- itc = finance_data[project_row, "ITC 2020"]
- elseif online_year == 2021
- itc = finance_data[project_row, "ITC 2021"]
- else
- itc = finance_data[project_row, "ITC post 2021"]
- end
-
- incentive_inputs = Dict("itc" => itc,
- "grants" => finance_data[project_row, "GRANTS"],
- "itc_recovery_factor" => finance_data[project_row, "ITC_RECOVERY_FACTOR"])
-
- macrs_schedule = get_macrs_schedule(finance_data[project_row, "MACRS_DEPRECIATION_YEARS"], macrs_df)
- wacc_inputs = Dict("tax_rate" => finance_data[project_row, "TAX_RATE"],
- "debt_fration" => finance_data[project_row, "DEBT_FRACTION"],
- "debt_interest_rate" => finance_data[project_row, "DEBT_INTEREST_RATE"],
- "equity_discount_rate" => finance_data[project_row, "EQUITY_RATE"])
-
- discount_rate = calculate_wacc(wacc_inputs) + investor_characteristics[1, "discount_rate_adder"]
-
- tax_inputs = Dict("macrs" => macrs_schedule,
- "tax_rate" => finance_data[project_row, "TAX_RATE"],
- "discount_rate" => discount_rate,
- "depreciable_percent"=> finance_data[project_row, "DEPRECIABLE_PERCENT"])
-
- cap_cost = zeros(length(project_capex))
- for i in 1:length(project_capex)
- COSTS = [project_capex[i]]
- capital_cost_inputs = Dict("cost_inputs" => [project_capex[i]], "incentive_inputs" => incentive_inputs, "tax_inputs" => tax_inputs)
- cap_cost[i] = calculate_capital_costs(capital_cost_inputs)
- end
-
- return cap_cost, discount_rate
-end
+"""
+This function checks if value is in input dictionary.
+"""
+function check_input_for_variable(dict, name, default = 0)
+ var = default
+ if haskey(dict, name)
+ var = dict[name]
+ end
+ return var
+end
+
+"""
+This function calculates the adjusted capital cost of a project.
+"""
+function calculate_capital_costs(inputs)
+ #capital costs are the cost of getting the plant to completion, minus incentive benefits, minus tax benefits
+ plant_cost = calculate_plant_cost(inputs["cost_inputs"])
+ incentives = calculate_incentive_benefits(inputs["incentive_inputs"], plant_cost)
+ tax_benefits = calculate_tax_benefits(inputs["tax_inputs"], plant_cost, inputs["incentive_inputs"])
+ return(plant_cost - incentives - tax_benefits)
+end
+
+"""
+This function adds the total construction and development costs of a project.
+"""
+function calculate_plant_cost(cost_inputs)
+ return sum(cost_inputs)
+end
+
+"""
+This function returns the MACRS depreciation schedule of a project.
+"""
+function get_macrs_schedule(num_depreciation_years, macrs_df)
+ return macrs_df[:, Symbol("Y"*string(num_depreciation_years))]
+end
+
+"""
+This function calculates the incentive benefits which can come from grants and investment tax credits
+ grants are in dollars/KW of capacity
+ itc is in terms of percent of plant cost
+ itc_recovery_factor scales the percent of capital costs which can be covered by the itc. This can be due to transaction frictions.
+"""
+function calculate_incentive_benefits(incentive_inputs, plant_cost)
+ grants = check_input_for_variable(incentive_inputs, "grants", 0)
+ itc = check_input_for_variable(incentive_inputs, "itc", 0)
+ itc_recovery_factor = check_input_for_variable(incentive_inputs, "itc_recovery_factor", 1)
+ return grants + itc*itc_recovery_factor*plant_cost
+end
+"""
+
+This function calculates the tax benefits which are from depreciating the capital asset. tax inputs include:
+ macrs: the depreciation schedule in percent for the asset
+ depreciable_percent: fraction of plant costs which can be depreciated
+ discount_rate: used to transform future tax benefits to present value
+ tax_rate: tax percent which is deducted.
+"""
+function calculate_tax_benefits(tax_inputs, plant_cost, incentive_inputs)
+ #
+ macrs = check_input_for_variable(tax_inputs, "macrs", [0])
+ itc = check_input_for_variable(incentive_inputs, "itc", 0)
+ depreciable_percent = check_input_for_variable(tax_inputs, "depreciable_percent", 1)
+ tax_rate = check_input_for_variable(tax_inputs, "tax_rate", .25)
+ discount_rate = check_input_for_variable(tax_inputs, "discount_rate", 0.08)
+ #
+ return calculate_present_value(plant_cost*depreciable_percent*(1-itc/2)*tax_rate*macrs, discount_rate)
+end
+
+"""
+This function calculates the present value which takes yearly values and a discount rate and returns the present value.
+"""
+function calculate_present_value(values, discount_rate)
+ return sum([values[i]*(1+discount_rate)^(-i) for i in (1:length(values))])
+end
+
+"""
+This function calculates calculates the weighted average cost of capital
+ Takes a list with: Tax rate, debt interest rate, debt fraction, and either the equity discount rate or the required values to calculate a CAPM model
+"""
+function calculate_wacc(wacc_inputs)
+ tax_rate = check_input_for_variable(wacc_inputs, "tax_rate", 0.25)
+ debt_fraction = check_input_for_variable(wacc_inputs, "debt_fraction", 0.6)
+ debt_interest_rate = check_input_for_variable(wacc_inputs, "debt_interest_rate", 0.05)
+ equity_discount_rate = check_input_for_variable(wacc_inputs, "equity_discount_rate", 0.12)
+ #
+ debt_contribution = (1- tax_rate)*debt_interest_rate*debt_fraction
+ equity_contribution = equity_discount_rate*(1-debt_fraction)
+ return (debt_contribution + equity_contribution)
+end
+#_________________________________________________________________________________________________________________________________________________
+"""
+This function calculates a CAPM model equity rate = risk-free rate + beta*(market return - risk_free rate) + project-specific adder
+"""
+function calculate_equity_discount_rate(inputs)
+ project_specific_adder = check_input_for_variable(inputs, "project_specific_adder", 0)
+ risk_free_rate = check_input_for_variable(inputs, "risk_free_rate", 0.04)
+ beta = check_input_for_variable(inputs, "beta", 1)
+ market_return = check_input_for_variable(inputs, "market_return", .12)
+ return (risk_free_rate + beta*(market_return - risk_free_rate) + project_specific_adder)
+end
+
+"""
+This function utilizes all the finance functions to return the WACC and adjusted capital cost of a project.
+"""
+function calculate_capcost_and_wacc(project_capex::Vector{Float64},
+ category::String,
+ investor_dir::String,
+ online_year::Int64)
+ finance_data = read_data(joinpath(investor_dir, "finance_params.csv"))
+ investor_characteristics = read_data(joinpath(investor_dir, "characteristics.csv"))
+
+ project_row = findfirst(x-> x == category, finance_data[:, "Category"])
+ macrs_df = read_data(joinpath(investor_dir, "MACRS Schedule.csv"))
+
+ equity_discount_rate = finance_data[project_row, "EQUITY_RATE"]
+
+ if online_year <= 2020
+ itc = finance_data[project_row, "ITC 2020"]
+ elseif online_year == 2021
+ itc = finance_data[project_row, "ITC 2021"]
+ else
+ itc = finance_data[project_row, "ITC post 2021"]
+ end
+
+ incentive_inputs = Dict("itc" => itc,
+ "grants" => finance_data[project_row, "GRANTS"],
+ "itc_recovery_factor" => finance_data[project_row, "ITC_RECOVERY_FACTOR"])
+
+ macrs_schedule = get_macrs_schedule(finance_data[project_row, "MACRS_DEPRECIATION_YEARS"], macrs_df)
+ wacc_inputs = Dict("tax_rate" => finance_data[project_row, "TAX_RATE"],
+ "debt_fration" => finance_data[project_row, "DEBT_FRACTION"],
+ "debt_interest_rate" => finance_data[project_row, "DEBT_INTEREST_RATE"],
+ "equity_discount_rate" => finance_data[project_row, "EQUITY_RATE"])
+
+ discount_rate = calculate_wacc(wacc_inputs) + investor_characteristics[1, "discount_rate_adder"]
+
+ tax_inputs = Dict("macrs" => macrs_schedule,
+ "tax_rate" => finance_data[project_row, "TAX_RATE"],
+ "discount_rate" => discount_rate,
+ "depreciable_percent"=> finance_data[project_row, "DEPRECIABLE_PERCENT"])
+
+ cap_cost = zeros(length(project_capex))
+ for i in 1:length(project_capex)
+ COSTS = [project_capex[i]]
+ capital_cost_inputs = Dict("cost_inputs" => [project_capex[i]], "incentive_inputs" => incentive_inputs, "tax_inputs" => tax_inputs)
+ cap_cost[i] = calculate_capital_costs(capital_cost_inputs)
+ end
+
+ return cap_cost, discount_rate
+end
diff --git a/src/utils/general.jl b/src/utils/general.jl
index 429c243..eb33b03 100644
--- a/src/utils/general.jl
+++ b/src/utils/general.jl
@@ -1,307 +1,318 @@
-
-"""
-This function returns all the subtypes at the leaf nodes (i.e. which do not have any further subtypes) of a given type.
-"""
-function leaftypes(t::Any, level=1, leaves = Vector{Any}())
- for s in InteractiveUtils.subtypes(t)
- if length(InteractiveUtils.subtypes(s)) == 0
- push!(leaves, s)
- else
- leaftypes(s, level+1, leaves)
- end
- end
- return leaves
-end
-
-
-"""
-This function gets the capacity market forward years for the simulation.
-"""
-function get_capacity_forward_years(sim::Union{AgentSimulation, AgentSimulationData})
- forward_years = 1
- if get_markets(sim)[:Capacity]
- capacity_mkt_params = DataFrames.DataFrame(CSV.File(joinpath(get_data_dir(get_case(sim)), "markets_data", "Capacity.csv")))
- forward_years = capacity_mkt_params.forward_years[1]
- end
- return forward_years
-end
-
-
-
-"""
-This function returns the size of the project (in MW)
-based on the project type and size type.
-"""
-function size_in_MW(investor_dir::String,
- projecttype::String,
- sizetype::String)
- filename = joinpath(investor_dir, "sizedict.csv")
- sizedata = read_data(filename)
-
- size = sizedata[findfirst(x-> x == projecttype, sizedata.type),
- findfirst(x -> x == sizetype, names(sizedata))]
- return size
-end
-
-"""
-This function returns the size of the project (in MW)
-if already specified in MW.
-"""
-function size_in_MW(investor_dir::String,
- projecttype::String,
- size::Float64)
- return size
-end
-
-"""
-This function returns the size of the PSY Device.
-"""
-get_device_size(device::PSY.ThermalStandard) = PSY.get_active_power_limits(device)[:max]
-get_device_size(device::PSY.RenewableDispatch) = PSY.get_rating(device)
-get_device_size(device::Union{PSY.HydroEnergyReservoir, PSY.HydroDispatch}) = PSY.get_active_power_limits(device)[:max]
-get_device_size(device::PSY.GenericBattery) = PSY.get_rating(device)
-
-"""
-This function returns line rating for PSY Lines.
-"""
-get_line_rating(line::PSY.Line) = PSY.get_rate(line)
-get_line_rating(line::PSY.HVDCLine) = line.active_power_limits_from[:max]
-
-"""
-This function removes leap days from the data if year is not leap year.
-"""
-function remove_leap_day!(df:: DataFrames.DataFrame, start_year::Int64)
- #=
- if start_year % 4 != 0
- filter!(row -> row["Month"] != 2 || row["Day"] != 29, df)
- end
- =#
- filter!(row -> row["Month"] != 2 || row["Day"] != 29, df)
-
- return
-end
-
-"""
-This function finds the start and end years
-for reading price data.
-"""
-function find_price_years(construction_year::Int64,
- end_life_year::Int64,
- iteration_year::Int64,
- yearly_horizon::Int64)
-
- price_years = (start_year = max((construction_year -
- iteration_year + 1),
- 1),
- end_year = min((end_life_year -
- iteration_year + 1),
- (yearly_horizon)))
-
- return price_years
-end
-
-"""
-This function finds the start and end years
-for updating project profit and npv data.
-"""
-function find_update_years(construction_year::Int64,
- end_life_year::Int64,
- iteration_year::Int64,
- yearly_horizon::Int64)
-
- update_years = (start_year = max(construction_year,
- iteration_year),
- end_year = min(end_life_year,
- yearly_horizon + iteration_year - 1))
-
- return update_years
-end
-
-"""
-This function returns the number of hours modeled in the market clearing problem.
-"""
-function get_num_hours(system::MarketClearingProblem{Z, T}) where {Z, T}
- num_hours = T
-
- return num_hours
-end
-
-"""
-This function returns an O&M cost array filled with the fixed
-annual O&M cost values from update year to the end life year.
-"""
-function create_OMcost_array(finance::Finance,
- scenario_name::String,
- project_life_end::Int64,
- update_start_year::Int64,
- iteration_year::Int64)
-
- OM_cost_array = zeros(size(get_scenario_profit(finance)[scenario_name][iteration_year], 2))
-
- for i in update_start_year:project_life_end
- OM_cost_array[i] = get_fixed_OM_cost(finance)
- end
-
- return OM_cost_array
-
-end
-
-"""
-This function return a queue cost array filled with the queue cost values
-corresponding to each year spent in the queue.
-"""
-function create_queue_cost_array(size_profit::Int64,
- decision_year::Int64,
- queue_time::Int64,
- queue_cost::Vector{Float64})
-
-
- queue_cost_array = zeros(size_profit)
-
- for i in decision_year:(decision_year + queue_time - 1)
- queue_cost_array[i] = queue_cost[i - decision_year + 1]
- end
-
- return queue_cost_array
-
-end
-"""
-This function does nothing if the project is not an Option project.
-"""
-function update_lifecycle!(project::P,
- iteration_year::Int64,
- simulation_years::Int64) where P <: Project{<: BuildPhase}
- return
-end
-
-"""
-This function updates the decision, construction and endlife years of Option projects.
-"""
-function update_lifecycle!(project::P,
- iteration_year::Int64,
- simulation_years::Int64) where P <: Project{Option}
-
-
- finance_data = get_finance_data(project)
-
- queue_time = length(get_queue_cost(finance_data))
-
- if queue_time + iteration_year < simulation_years
- set_decision_year!(project, (iteration_year + 1))
- set_construction_year!(project, (get_decision_year(project) +
- queue_time +
- get_lag_time(finance_data)))
-
- end_life_year = get_construction_year(project) + get_life_time(finance_data) - 1
- set_end_life_year!(project, end_life_year)
-
- set_effective_investment_cost!(project, get_investment_cost(finance_data)[queue_time + iteration_year + 1])
-
- products = get_products(project)
- finance_data.realized_profit = AxisArrays.AxisArray(zeros(length(products), end_life_year),
- AxisArrays.Axis{:prod}(get_name.(products)),
- AxisArrays.Axis{:year}(1:1:end_life_year))
-
- finance_data.annual_cashflow = zeros(end_life_year)
-
- scenario_profit = get_scenario_profit(finance_data)
-
- for scenario_name in keys(scenario_profit)
- rows = size(scenario_profit[scenario_name][iteration_year], 1)
- cols = get_end_life_year(project)
-
- set_expected_npv!(finance_data, zeros(cols))
- set_expected_utility!(finance_data, zeros(cols))
-
- finance_data.scenario_profit[scenario_name] = [AxisArrays.AxisArray(zeros(length(products), cols),
- AxisArrays.Axis{:prod}(get_name.(products)),
- AxisArrays.Axis{:year}(collect(1:1:cols)))
- for y in 1:cols]
- set_scenario_npv!(finance_data, scenario_name, zeros(cols))
- set_scenario_utility!(finance_data, scenario_name, zeros(cols))
- end
- end
-
- return
-end
-
-"""
-This function updates the total installed capacity each year.
-"""
-function update_installed_cap!(installed_capacity::Vector{Float64},
- announced_projects::Vector{<: Project{<: BuildPhase}},
- iteration_year::Int64,
- simulation_years::Int64)
-
- for year in iteration_year:simulation_years
- installed_capacity[year] = 0.0
- for project in announced_projects
- if (get_construction_year(project) <= year) && (get_end_life_year(project) >= year)
- installed_capacity[year] += get_maxcap(project)
- end
- end
- end
- return installed_capacity
-end
-
-"""
-This function does nothing if project is not in Existing phase.
-"""
-function extrapolate_profits!(project::Project{<:BuildPhase}, final_year::Int64)
- return
-end
-
-"""
-This function extrapolates the profits of Existing projects beyond the simulation horizon.
-"""
-function extrapolate_profits!(project::Project{Existing}, final_year::Int64)
- end_life_year = get_end_life_year(project)
- finance_data = get_finance_data(project)
- if end_life_year > final_year
- for y in (final_year + 1):end_life_year
- set_annual_cashflow!(finance_data, y, get_annual_cashflow(finance_data)[final_year])
- for product in get_products(project)
- name = get_name(product)
- set_realized_profit!(finance_data, name, y, get_realized_profit(finance_data)[name, final_year])
- end
- end
- end
- return
-end
-
-function month_lookup(str::Union{String, SubString{String}})
- if str == "Jan" || str == "January"
- month = 1
- elseif str == "Feb" || str == "February"
- month = 2
- elseif str == "Mar" || str == "March"
- month = 3
- elseif str == "Apr" || str == "April"
- month = 4
- elseif str == "May"
- month = 5
- elseif str == "Jun" || str == "June"
- month = 6
- elseif str == "Jul" || str == "July"
- month = 7
- elseif str == "Aug" || str == "August"
- month = 8
- elseif str == "Sep" || str == "September"
- month = 9
- elseif str == "Oct" || str == "October"
- month = 10
- elseif str == "Nov" || str == "November"
- month = 11
- elseif str == "Dec" || str == "December"
- month = 12
- end
- return Int(month)
-end
-
-function find_rt_periods(hours::Vector{Int64}, num_rt_intervals::Int64)
- rt_periods = [];
- for hour in hours
- append!(rt_periods, collect(((hour - 1) * num_rt_intervals + 1):(hour * num_rt_intervals)))
- end
- return rt_periods
-end
+
+"""
+This function returns all the subtypes at the leaf nodes (i.e. which do not have any further subtypes) of a given type.
+"""
+function leaftypes(t::Any, level=1, leaves = Vector{Any}())
+ for s in InteractiveUtils.subtypes(t)
+ if length(InteractiveUtils.subtypes(s)) == 0
+ push!(leaves, s)
+ else
+ leaftypes(s, level+1, leaves)
+ end
+ end
+ return leaves
+end
+
+
+"""
+This function gets the capacity market forward years for the simulation.
+"""
+function get_capacity_forward_years(sim::Union{AgentSimulation, AgentSimulationData})
+ forward_years = 1
+ if get_markets(sim)[:Capacity]
+ capacity_mkt_params = DataFrames.DataFrame(CSV.File(joinpath(get_data_dir(get_case(sim)), "markets_data", "Capacity.csv")))
+ forward_years = capacity_mkt_params.forward_years[1]
+ end
+ return forward_years
+end
+
+
+
+"""
+This function returns the size of the project (in MW)
+based on the project type and size type.
+"""
+function size_in_MW(investor_dir::String,
+ projecttype::String,
+ sizetype::String)
+ filename = joinpath(investor_dir, "sizedict.csv")
+ sizedata = read_data(filename)
+
+ size = sizedata[findfirst(x-> x == projecttype, sizedata.type),
+ findfirst(x -> x == sizetype, names(sizedata))]
+ return size
+end
+
+"""
+This function returns the size of the project (in MW)
+if already specified in MW.
+"""
+function size_in_MW(investor_dir::String,
+ projecttype::String,
+ size::Float64)
+ return size
+end
+
+"""
+This function returns the size of the PSY Device.
+"""
+get_device_size(device::T) where T <: PSY.ThermalGen = PSY.get_active_power_limits(device)[:max]
+get_device_size(device::PSY.RenewableDispatch) = PSY.get_rating(device)
+get_device_size(device::Union{PSY.HydroEnergyReservoir, PSY.HydroDispatch}) = PSY.get_active_power_limits(device)[:max]
+get_device_size(device::PSY.GenericBattery) = PSY.get_rating(device)
+
+"""
+This function returns line rating for PSY Lines.
+"""
+get_line_rating(line::PSY.Line) = PSY.get_rate(line)
+get_line_rating(line::PSY.HVDCLine) = line.active_power_limits_from[:max]
+
+"""
+This function removes leap days from the data if year is not leap year.
+"""
+function remove_leap_day!(df:: DataFrames.DataFrame, start_year::Int64)
+ #=
+ if start_year % 4 != 0
+ filter!(row -> row["Month"] != 2 || row["Day"] != 29, df)
+ end
+ =#
+ #filter!(row -> row["Month"] != 2 || row["Day"] != 29, df)
+
+ return
+end
+
+"""
+This function finds the start and end years
+for reading price data.
+"""
+function find_price_years(construction_year::Int64,
+ end_life_year::Int64,
+ iteration_year::Int64,
+ yearly_horizon::Int64)
+
+ price_years = (start_year = max((construction_year -
+ iteration_year + 1),
+ 1),
+ end_year = min((end_life_year -
+ iteration_year + 1),
+ (yearly_horizon)))
+
+ return price_years
+end
+
+"""
+This function finds the start and end years
+for updating project profit and npv data.
+"""
+function find_update_years(construction_year::Int64,
+ end_life_year::Int64,
+ iteration_year::Int64,
+ yearly_horizon::Int64)
+
+ update_years = (start_year = max(construction_year,
+ iteration_year),
+ end_year = min(end_life_year,
+ yearly_horizon + iteration_year - 1))
+
+ return update_years
+end
+
+"""
+This function returns the number of hours modeled in the market clearing problem.
+"""
+function get_num_hours(system::MarketClearingProblem{Z, T}) where {Z, T}
+ num_hours = T
+
+ return num_hours
+end
+
+"""
+This function returns an O&M cost array filled with the fixed
+annual O&M cost values from update year to the end life year.
+"""
+function create_OMcost_array(finance::Finance,
+ scenario_name::String,
+ project_life_end::Int64,
+ update_start_year::Int64,
+ iteration_year::Int64)
+
+ OM_cost_array = zeros(size(get_scenario_profit(finance)[scenario_name][iteration_year], 2))
+
+ for i in update_start_year:project_life_end
+ OM_cost_array[i] = get_fixed_OM_cost(finance)
+ end
+
+ return OM_cost_array
+
+end
+
+"""
+This function return a queue cost array filled with the queue cost values
+corresponding to each year spent in the queue.
+"""
+function create_queue_cost_array(size_profit::Int64,
+ decision_year::Int64,
+ queue_time::Int64,
+ queue_cost::Vector{Float64})
+
+
+ queue_cost_array = zeros(size_profit)
+
+ for i in decision_year:(decision_year + queue_time - 1)
+ queue_cost_array[i] = queue_cost[i - decision_year + 1]
+ end
+
+ return queue_cost_array
+
+end
+"""
+This function does nothing if the project is not an Option project.
+"""
+function update_lifecycle!(project::P,
+ iteration_year::Int64,
+ simulation_years::Int64) where P <: Project{<: BuildPhase}
+ return
+end
+
+"""
+This function updates the decision, construction and endlife years of Option projects.
+"""
+function update_lifecycle!(project::P,
+ iteration_year::Int64,
+ simulation_years::Int64) where P <: Project{Option}
+
+ finance_data = get_finance_data(project)
+
+ queue_time = length(get_queue_cost(finance_data))
+
+ if queue_time + iteration_year < simulation_years
+ set_decision_year!(project, (iteration_year + 1))
+ set_construction_year!(project, (get_decision_year(project) +
+ queue_time +
+ get_lag_time(finance_data)))
+
+ end_life_year = get_construction_year(project) + get_life_time(finance_data) - 1
+ set_end_life_year!(project, end_life_year)
+
+ set_effective_investment_cost!(project, get_investment_cost(finance_data)[queue_time + iteration_year + 1])
+
+ products = get_products(project)
+ finance_data.realized_profit = AxisArrays.AxisArray(zeros(length(products), end_life_year),
+ AxisArrays.Axis{:prod}(get_name.(products)),
+ AxisArrays.Axis{:year}(1:1:end_life_year))
+
+ finance_data.annual_cashflow = zeros(end_life_year)
+
+ scenario_profit = get_scenario_profit(finance_data)
+
+ for scenario_name in keys(scenario_profit)
+ rows = size(scenario_profit[scenario_name][iteration_year], 1)
+ cols = get_end_life_year(project)
+
+ set_expected_npv!(finance_data, zeros(cols))
+ set_expected_utility!(finance_data, zeros(cols))
+
+ finance_data.scenario_profit[scenario_name] = [AxisArrays.AxisArray(zeros(length(products), cols),
+ AxisArrays.Axis{:prod}(get_name.(products)),
+ AxisArrays.Axis{:year}(collect(1:1:cols)))
+ for y in 1:cols]
+ set_scenario_npv!(finance_data, scenario_name, zeros(cols))
+ set_scenario_utility!(finance_data, scenario_name, zeros(cols))
+ end
+ end
+
+ return
+end
+
+"""
+This function updates the total installed capacity each year.
+"""
+function update_installed_cap!(installed_capacity::Vector{Float64},
+ announced_projects::Vector{<: Project{<: BuildPhase}},
+ iteration_year::Int64,
+ simulation_years::Int64)
+
+ for year in iteration_year:simulation_years
+ installed_capacity[year] = 0.0
+ for project in announced_projects
+ if (get_construction_year(project) <= year) && (get_end_life_year(project) >= year)
+ installed_capacity[year] += get_maxcap(project)
+ end
+ end
+ end
+ return installed_capacity
+end
+
+"""
+This function does nothing if project is not in Existing phase.
+"""
+function extrapolate_profits!(project::Project{<:BuildPhase}, final_year::Int64)
+ return
+end
+
+"""
+This function extrapolates the profits of Existing projects beyond the simulation horizon.
+"""
+function extrapolate_profits!(project::Project{Existing}, final_year::Int64)
+ end_life_year = get_end_life_year(project)
+ finance_data = get_finance_data(project)
+ if end_life_year > final_year
+ for y in (final_year + 1):end_life_year
+ set_annual_cashflow!(finance_data, y, get_annual_cashflow(finance_data)[final_year])
+ for product in get_products(project)
+ name = get_name(product)
+ set_realized_profit!(finance_data, name, y, get_realized_profit(finance_data)[name, final_year])
+ end
+ end
+ end
+ return
+end
+
+function month_lookup(str::Union{String, SubString{String}})
+ if str == "Jan" || str == "January"
+ month = 1
+ elseif str == "Feb" || str == "February"
+ month = 2
+ elseif str == "Mar" || str == "March"
+ month = 3
+ elseif str == "Apr" || str == "April"
+ month = 4
+ elseif str == "May"
+ month = 5
+ elseif str == "Jun" || str == "June"
+ month = 6
+ elseif str == "Jul" || str == "July"
+ month = 7
+ elseif str == "Aug" || str == "August"
+ month = 8
+ elseif str == "Sep" || str == "September"
+ month = 9
+ elseif str == "Oct" || str == "October"
+ month = 10
+ elseif str == "Nov" || str == "November"
+ month = 11
+ elseif str == "Dec" || str == "December"
+ month = 12
+ end
+ return Int(month)
+end
+
+function find_rt_periods(hours::Vector{Int64}, num_rt_intervals::Int64)
+ rt_periods = [];
+ for hour in hours
+ append!(rt_periods, collect(((hour - 1) * num_rt_intervals + 1):(hour * num_rt_intervals)))
+ end
+ return rt_periods
+end
+
+function parsebool(s)
+ return lowercase(s) == "true" ? true : false
+end
+
+function parseint(s)
+ return parse(Int64, s)
+end
+
+function parsefloat(s)
+ return parse(Float64, s)
+end
diff --git a/src/utils/market_utils.jl b/src/utils/market_utils.jl
index 0ebe02a..25ed919 100644
--- a/src/utils/market_utils.jl
+++ b/src/utils/market_utils.jl
@@ -1,377 +1,378 @@
-"""
-This functions returns an AxisArray of project parameters
-included in CEM for price projection and endogeneous Economic Dispatch.
-"""
-function make_parameter_vector(
- structs::Vector, id::Symbol, parameter::Symbol)
- ids = getproperty.(structs, id)
- vals = getproperty.(structs, parameter)
- return AxisArrays.AxisArray(vals, ids)
-end
-
-"""
-This functions returns an array of market parameters
-included in CEM for price projection and endogeneous Economic Dispatch.
-"""
-function make_parameter_vector(
- structs::Vector{<:MarketCollection}, market::Symbol, parameter::Symbol)
- markets = getproperty.(structs, market)
- return getproperty.(markets, parameter)
-end
-
-"""
-This functions returns a matrix of demand parameters
-included in CEM for price projection and endogeneous Economic Dispatch.
-"""
-function make_demand_matrix(structs::Vector{<:MarketCollection}, market::Symbol)
- markets = getproperty.(structs, market)
- matrix = hcat(getproperty.(markets, :demand)...)
- return permutedims(matrix)
-end
-
-"""
-This functions returns capacity market demand curve parameters
-included in CEM for price projection and endogeneous Economic Dispatch.
-"""
-function make_capacity_demand_vectors(capmarkets::Vector{CapacityMarket})
- break_points = getproperty.(capmarkets, :break_points)
- price_points = getproperty.(capmarkets, :price_points)
- num_segments = length.(break_points) .- 1
- segment_size = [zeros(num_segments[i]) for i in 1:length(capmarkets)]
- segment_grad = [zeros(num_segments[i]) for i in 1:length(capmarkets)]
- for p in 1:length(capmarkets)
- for segment in 1:num_segments[p]
- segment_size[p][segment] = break_points[p][segment + 1] - break_points[p][segment]
- if segment_size[p][segment] != 0
- segment_grad[p][segment] = (price_points[p][segment + 1] - price_points[p][segment]) / segment_size[p][segment]
- end
- end
- end
-
- return segment_size, segment_grad, price_points, num_segments
-end
-
-"""
-This functions returns operating reserve demand curve parameters
-included in CEM for price projection and endogeneous Economic Dispatch.
-"""
-function make_ORDC_vectors(ordc_markets::Vector{Dict{String, ReserveORDCMarket{T}}}) where T
-
- inv_periods = length(ordc_markets)
-
- products = unique(keys(ordc_markets[p]) for p in inv_periods)[1]
-
- num_segments = Dict(product => AxisArrays.AxisArray(Array{Int64, 2}(undef, inv_periods, T), 1:inv_periods, 1:T) for product in products)
- segment_size = Dict(product => AxisArrays.AxisArray(Array{Vector{Float64}, 2}(undef, inv_periods, T), 1:inv_periods, 1:T) for product in products)
- segment_grad = Dict(product => AxisArrays.AxisArray(Array{Vector{Float64}, 2}(undef, inv_periods, T), 1:inv_periods, 1:T) for product in products)
- price_points = Dict(product => AxisArrays.AxisArray(Array{Vector{Float64}, 2}(undef, inv_periods, T), 1:inv_periods, 1:T) for product in products)
-
- for product in products
- break_points = [getproperty(ordc_markets[p][product], :break_points) for p in 1:inv_periods]
- price_points_raw = [getproperty(ordc_markets[p][product], :price_points) for p in 1:inv_periods]
-
- stepped = getproperty(ordc_markets[1][product], :stepped)
-
- println(stepped)
-
- for p in 1:inv_periods
- for t in 1:T
- num_segments[product][p, t] = length(break_points[p][t]) - 1
- segment_size[product][p, t] = zeros(num_segments[product][p, t])
- segment_grad[product][p, t] = zeros(num_segments[product][p, t])
- price_points[product][p, t] = price_points_raw[p][t]
-
- if stepped
-
- for segment in 1:num_segments[product][p, t]
- segment_size[product][p, t][segment] = break_points[p][t][segment + 1] - break_points[p][t][segment]
- price_points[product][p, t][segment] = price_points[product][p, t][segment + 1]
- end
-
- else
-
- for segment in 1:num_segments[product][p, t]
- segment_size[product][p, t][segment] = break_points[p][t][segment + 1] - break_points[p][t][segment]
- if segment_size[product][p, t][segment] != 0
- segment_grad[product][p, t][segment] = (price_points[product][p, t][segment + 1] - price_points[product][p, t][segment]) / segment_size[product][p, t][segment]
- end
- end
-
- end
- end
- end
-
- end
-
- return segment_size, segment_grad, price_points, num_segments
- end
-
- """
- This functions returns capacity market demand curve parameters
- included in the actual clearing of capacity markets.
- """
-function make_capacity_demand(capmarket::CapacityMarket)
- break_points = getproperty(capmarket, :break_points)
- price_points = getproperty(capmarket, :price_points)
- num_segments = length(break_points) - 1
- segment_size = zeros(num_segments)
- segment_grad = zeros(num_segments)
- for segment in 1:num_segments
- segment_size[segment] = break_points[segment + 1] - break_points[segment]
- segment_grad[segment] = (price_points[segment + 1] - price_points[segment]) / segment_size[segment]
- end
-
- return segment_size, segment_grad, price_points
- end
-
- """
-This functions returns capital cost multiplier curve parameters
-included in CEM for price projection and endogeneous Economic Dispatch.
-"""
-function make_capital_cost_curve(options_by_type::Dict{String, Vector{String}},
- annualized_cap_cost::AxisArrays.AxisArray{Float64, 2},
- basecostunits::AxisArrays.AxisArray{Int64, 1},
- maxnewoptions::AxisArrays.AxisArray{Int64, 1},
- capital_cost_multiplier::Float64)
-
- invperiods = size(annualized_cap_cost, 2)
- types = collect(keys(options_by_type))
-
- num_segments = AxisArrays.AxisArray(round.(Int, 2 * ones(length(types), invperiods)), types, 1:invperiods)
-
- segment_size = AxisArrays.AxisArray(Array{Vector{Float64}, 2}(undef, length(types), invperiods), types, 1:invperiods)
- segment_grad = AxisArrays.AxisArray(Array{Vector{Float64}, 2}(undef, length(types), invperiods), types, 1:invperiods)
- costpoints = AxisArrays.AxisArray(Array{Vector{Float64}, 2}(undef, length(types), invperiods), types, 1:invperiods)
-
- for t in types
- for p in 1:invperiods
- segment_size[t, p] = zeros(num_segments[t, p])
- segment_grad[t, p] = zeros(num_segments[t, p])
- costpoints[t, p] = zeros(num_segments[t, p])
-
- if length(options_by_type[t]) >= 1
- total_max_new_options = sum(maxnewoptions[g] for g in options_by_type[t])
- g = options_by_type[t][1]
- segment_size[t, p][1] = basecostunits[g]
- segment_size[t, p][2] = total_max_new_options - basecostunits[g]
- costpoints[t, p] = [annualized_cap_cost[g, p], annualized_cap_cost[g, p]]
- segment_grad[t, p][1] = 0.0
- segment_grad[t, p][2] = costpoints[t, p][2] * capital_cost_multiplier
- end
- end
- end
-
- return segment_size, segment_grad, costpoints, num_segments
-end
-
-"""
-This function updates the expected capacity factors of projects which are not in Option phase.
-"""
-function update_capacity_factors!(project::P,
- scenario_name::String,
- capacity_factors::Dict{String, Array{Float64, 2}}) where P <: Project{<: BuildPhase}
- for product in get_products(project)
- set_capacity_factors!(product, scenario_name, capacity_factors[get_name(project)])
- end
- return
-end
-
-"""
-This function updates the expected capacity factors of Option projects.
-"""
-function update_capacity_factors!(project::P,
- scenario_name::String,
- capacity_factors::Dict{String, Array{Float64, 2}}) where P <: Project{Option}
-
- tech = get_tech(project)
- for product in get_products(project)
- set_capacity_factors!(product, scenario_name, capacity_factors["option_$(get_type(tech))_$(get_zone(tech))"])
- end
- return
-end
-
-"""
-This function updates the expected capacity factors of projects which are not in Option phase.
-"""
-function update_total_utilization!(project::P,
- scenario_name::String,
- total_utilization::Dict{String, Array{Float64, 2}}) where P <: Project{<: BuildPhase}
-
- project_total_utilization = total_utilization[get_name(project)]
- project.finance_data.scenario_total_utilization[scenario_name] = project_total_utilization
-
- return
-end
-
-"""
-This function updates the expected capacity factors of Option projects.
-"""
-function update_total_utilization!(project::P,
- scenario_name::String,
- total_utilization::Dict{String, Array{Float64, 2}}) where P <: Project{Option}
-
- tech = get_tech(project)
- project_total_utilization = total_utilization["option_$(get_type(tech))_$(get_zone(tech))"]
- project.finance_data.scenario_total_utilization[scenario_name] = project_total_utilization
-
- return
-end
-
-"""
-This function updates the expected accepted capacity percentage of projects which are not in Option phase.
-"""
-function update_capacity_accepted_perc!(project::P,
- scenario_name::String,
- capacity_accepted_perc::Dict{String, Array{Float64, 1}}) where P <: Project{<: BuildPhase}
- for product in get_products(project)
- set_accepted_perc!(product, scenario_name, capacity_accepted_perc[get_name(project)])
- end
- return
-end
-
-"""
-This function updates the expected accepted capacity percentage of Option projects.
-"""
-function update_capacity_accepted_perc!(project::P,
- scenario_name::String,
- capacity_accepted_perc::Dict{String, Array{Float64, 1}}) where P <: Project{Option}
-
- tech = get_tech(project)
- for product in get_products(project)
- set_accepted_perc!(product, scenario_name, capacity_accepted_perc["option_$(get_type(tech))_$(get_zone(tech))"])
- end
- return
-end
-
-"""
-This function does nothing if the product is not of Capacity type.
-"""
-function update_initial_capacity_revenues!(project::P,
- product::T,
- initial_capacity_prices::Vector{Float64},
- year::Int64) where {P <: Project{<: BuildPhase}, T <: Product}
- return
-end
-
-"""
-This function updates the capacity market revenues of project at the start of the simulation.
-"""
-function update_initial_capacity_revenues!(project::P,
- product::Capacity,
- initial_capacity_prices::Vector{Float64},
- year::Int64) where P <: Project{<: BuildPhase}
- capacity_revenue = initial_capacity_prices[year] * get_maxcap(project) * get_project_derating(project)
-
- finance_data = get_finance_data(project)
- set_realized_profit!(get_finance_data(project),
- get_name(product),
- year,
- capacity_revenue)
-
- for scenario_name in keys(get_scenario_profit(finance_data))
- update_forward_profit!(product, finance_data, scenario_name, year, capacity_revenue)
- end
-
- return
-end
-
-"""
-This function does nothing if the product is not of Capacity or REC type.
-"""
-function update_bid!(product::T,
- capacity_market_bid::Float64,
- rec_market_bid::Float64,
- energy_production::Float64) where T <: Product
- return
-end
-
-"""
-This function updates the Capacity market bid of projects.
-"""
-function update_bid!(product::Capacity,
- capacity_market_bid::Float64,
- rec_market_bid::Float64,
- energy_production::Float64)
-
- set_capacity_bid!(product, capacity_market_bid)
-
- return
-end
-
-"""
-This function updates the REC market bid of projects.
-"""
-function update_bid!(product::REC,
- capacity_market_bid::Float64,
- rec_market_bid::Float64,
- energy_production::Float64)
-
- set_rec_bid!(product, rec_market_bid)
- set_rec_certificates!(product, energy_production)
- return
-end
-
-function calculate_carbon_cost_ratio(product::Product, carbon_cost_ratio::Float64, carbon_tax::Vector{Float64}, year::Int64)
- return carbon_cost_ratio
-end
-
-function calculate_carbon_cost_ratio(product::CarbonTax, carbon_cost_ratio::Float64, carbon_tax::Vector{Float64}, year::Int64)
- carbon_cost = get_emission(product) * carbon_tax[year]
- ratio = carbon_cost / get_fuel_cost(product)
- if !isnan(ratio)
- carbon_cost_ratio = ratio
- end
- return carbon_cost_ratio
-end
-
-function update_device_operation_cost!(project::P, sys_UC::PSY.System, var_cost, fixed::Float64) where P <: Project{<:BuildPhase}
- return
-end
-
-function update_device_operation_cost!(project::P, sys_UC::PSY.System, var_cost, fixed::Float64) where P <: Project{Existing}
- name = get_name(project)
- device = PSY.get_components_by_name(PSY.Device, sys_UC, name)[1]
- device.operation_cost.variable.cost = var_cost
- device.operation_cost.fixed = fixed
- return
-end
-
-function update_operation_cost!(project::P, sys_UC::PSY.System, carbon_tax::Vector{Float64}, year::Int64) where P <: Project{<:BuildPhase}
- return
-end
-
-function update_operation_cost!(project::P, sys_UC::Nothing, carbon_tax::Vector{Float64}, year::Int64) where P <: Project{<:BuildPhase}
- return
-end
-
-function update_operation_cost!(project::P, sys_UC::PSY.System, carbon_tax::Vector{Float64}, year::Int64) where P <: Project{Existing}
- operation_cost = get_operation_cost(get_tech(project))
- if !(isnothing(operation_cost))
- products = get_products(project)
- carbon_cost_ratio = 0.0
-
- for product in products
- carbon_cost_ratio = calculate_carbon_cost_ratio(product, carbon_cost_ratio, carbon_tax, year)
- end
-
- total_cost_scalar = 1 + carbon_cost_ratio
-
- var_cost = deepcopy(PSY.get_variable(operation_cost).cost)
-
- fixed = deepcopy(PSY.get_fixed(operation_cost))
-
- if length(var_cost) > 1
- for i in 1:length(var_cost)
- var_cost[i] = (var_cost[i][1] * total_cost_scalar, var_cost[i][2])
- end
- elseif length(var_cost) == 1
- var_cost = var_cost * total_cost_scalar
- end
-
- fixed = fixed * total_cost_scalar
-
- update_device_operation_cost!(project, sys_UC, var_cost, fixed)
- end
-
- return
-end
+"""
+This functions returns an AxisArray of project parameters
+included in CEM for price projection and endogeneous Economic Dispatch.
+"""
+function make_parameter_vector(
+ structs::Vector, id::Symbol, parameter::Symbol)
+ ids = getproperty.(structs, id)
+ vals = getproperty.(structs, parameter)
+ return AxisArrays.AxisArray(vals, ids)
+end
+
+"""
+This functions returns an array of market parameters
+included in CEM for price projection and endogeneous Economic Dispatch.
+"""
+function make_parameter_vector(
+ structs::Vector{<:MarketCollection}, market::Symbol, parameter::Symbol)
+ markets = getproperty.(structs, market)
+ return getproperty.(markets, parameter)
+end
+
+"""
+This functions returns a matrix of demand parameters
+included in CEM for price projection and endogeneous Economic Dispatch.
+"""
+function make_demand_matrix(structs::Vector{<:MarketCollection}, market::Symbol)
+ markets = getproperty.(structs, market)
+ matrix = hcat(getproperty.(markets, :demand)...)
+ return permutedims(matrix)
+end
+
+"""
+This functions returns capacity market demand curve parameters
+included in CEM for price projection and endogeneous Economic Dispatch.
+"""
+function make_capacity_demand_vectors(capmarkets::Vector{CapacityMarket})
+ break_points = getproperty.(capmarkets, :break_points)
+ price_points = getproperty.(capmarkets, :price_points)
+ num_segments = length.(break_points) .- 1
+ segment_size = [zeros(num_segments[i]) for i in 1:length(capmarkets)]
+ segment_grad = [zeros(num_segments[i]) for i in 1:length(capmarkets)]
+ for p in 1:length(capmarkets)
+ for segment in 1:num_segments[p]
+ segment_size[p][segment] = break_points[p][segment + 1] - break_points[p][segment]
+ if segment_size[p][segment] != 0
+ segment_grad[p][segment] = (price_points[p][segment + 1] - price_points[p][segment]) / segment_size[p][segment]
+ end
+ end
+ end
+
+ return segment_size, segment_grad, price_points, num_segments
+end
+
+"""
+This functions returns operating reserve demand curve parameters
+included in CEM for price projection and endogeneous Economic Dispatch.
+"""
+function make_ORDC_vectors(ordc_markets::Vector{Dict{String, ReserveORDCMarket{T}}}) where T
+
+ inv_periods = length(ordc_markets)
+
+ products = unique(keys(ordc_markets[p]) for p in inv_periods)[1]
+
+ num_segments = Dict(product => AxisArrays.AxisArray(Array{Int64, 2}(undef, inv_periods, T), 1:inv_periods, 1:T) for product in products)
+ segment_size = Dict(product => AxisArrays.AxisArray(Array{Vector{Float64}, 2}(undef, inv_periods, T), 1:inv_periods, 1:T) for product in products)
+ segment_grad = Dict(product => AxisArrays.AxisArray(Array{Vector{Float64}, 2}(undef, inv_periods, T), 1:inv_periods, 1:T) for product in products)
+ price_points = Dict(product => AxisArrays.AxisArray(Array{Vector{Float64}, 2}(undef, inv_periods, T), 1:inv_periods, 1:T) for product in products)
+
+ for product in products
+ break_points = [getproperty(ordc_markets[p][product], :break_points) for p in 1:inv_periods]
+ price_points_raw = [getproperty(ordc_markets[p][product], :price_points) for p in 1:inv_periods]
+
+ stepped = getproperty(ordc_markets[1][product], :stepped)
+
+ for p in 1:inv_periods
+ for t in 1:T
+ num_segments[product][p, t] = length(break_points[p][t]) - 1
+ segment_size[product][p, t] = zeros(num_segments[product][p, t])
+ segment_grad[product][p, t] = zeros(num_segments[product][p, t])
+ price_points[product][p, t] = price_points_raw[p][t]
+
+ if stepped
+
+ for segment in 1:num_segments[product][p, t]
+ segment_size[product][p, t][segment] = break_points[p][t][segment + 1] - break_points[p][t][segment]
+ price_points[product][p, t][segment] = price_points[product][p, t][segment + 1]
+ end
+
+ else
+
+ for segment in 1:num_segments[product][p, t]
+ segment_size[product][p, t][segment] = break_points[p][t][segment + 1] - break_points[p][t][segment]
+ if segment_size[product][p, t][segment] != 0
+ segment_grad[product][p, t][segment] = (price_points[product][p, t][segment + 1] - price_points[product][p, t][segment]) / segment_size[product][p, t][segment]
+ end
+ end
+
+ end
+ end
+ end
+
+ end
+
+ return segment_size, segment_grad, price_points, num_segments
+ end
+
+ """
+ This functions returns capacity market demand curve parameters
+ included in the actual clearing of capacity markets.
+ """
+function make_capacity_demand(capmarket::CapacityMarket)
+ break_points = getproperty(capmarket, :break_points)
+ price_points = getproperty(capmarket, :price_points)
+ num_segments = length(break_points) - 1
+ segment_size = zeros(num_segments)
+ segment_grad = zeros(num_segments)
+ for segment in 1:num_segments
+ segment_size[segment] = break_points[segment + 1] - break_points[segment]
+ segment_grad[segment] = (price_points[segment + 1] - price_points[segment]) / segment_size[segment]
+ end
+
+ return segment_size, segment_grad, price_points
+ end
+
+ """
+This functions returns capital cost multiplier curve parameters
+included in CEM for price projection and endogeneous Economic Dispatch.
+"""
+function make_capital_cost_curve(options_by_type::Dict{String, Vector{String}},
+ annualized_cap_cost::AxisArrays.AxisArray{Float64, 2},
+ basecostunits::AxisArrays.AxisArray{Int64, 1},
+ maxnewoptions::AxisArrays.AxisArray{Int64, 1},
+ capital_cost_multiplier::Float64)
+
+ invperiods = size(annualized_cap_cost, 2)
+ types = collect(keys(options_by_type))
+
+ num_segments = AxisArrays.AxisArray(round.(Int, 2 * ones(length(types), invperiods)), types, 1:invperiods)
+
+ segment_size = AxisArrays.AxisArray(Array{Vector{Float64}, 2}(undef, length(types), invperiods), types, 1:invperiods)
+ segment_grad = AxisArrays.AxisArray(Array{Vector{Float64}, 2}(undef, length(types), invperiods), types, 1:invperiods)
+ costpoints = AxisArrays.AxisArray(Array{Vector{Float64}, 2}(undef, length(types), invperiods), types, 1:invperiods)
+
+ for t in types
+ for p in 1:invperiods
+ segment_size[t, p] = zeros(num_segments[t, p])
+ segment_grad[t, p] = zeros(num_segments[t, p])
+ costpoints[t, p] = zeros(num_segments[t, p])
+
+ if length(options_by_type[t]) >= 1
+ total_max_new_options = sum(maxnewoptions[g] for g in options_by_type[t])
+ g = options_by_type[t][1]
+ segment_size[t, p][1] = basecostunits[g]
+ segment_size[t, p][2] = total_max_new_options - basecostunits[g]
+ costpoints[t, p] = [annualized_cap_cost[g, p], annualized_cap_cost[g, p]]
+ segment_grad[t, p][1] = 0.0
+ segment_grad[t, p][2] = costpoints[t, p][2] * capital_cost_multiplier
+ end
+ end
+ end
+
+ return segment_size, segment_grad, costpoints, num_segments
+end
+
+"""
+This function updates the expected capacity factors of projects which are not in Option phase.
+"""
+function update_capacity_factors!(project::P,
+ scenario_name::String,
+ capacity_factors::Dict{String, Array{Float64, 2}}) where P <: Project{<: BuildPhase}
+ for product in get_products(project)
+ set_capacity_factors!(product, scenario_name, capacity_factors[get_name(project)])
+ end
+ return
+end
+
+"""
+This function updates the expected capacity factors of Option projects.
+"""
+function update_capacity_factors!(project::P,
+ scenario_name::String,
+ capacity_factors::Dict{String, Array{Float64, 2}}) where P <: Project{Option}
+
+ tech = get_tech(project)
+ for product in get_products(project)
+ set_capacity_factors!(product, scenario_name, capacity_factors["option_$(get_type(tech))_$(get_zone(tech))"])
+ end
+ return
+end
+
+"""
+This function updates the expected capacity factors of projects which are not in Option phase.
+"""
+function update_total_utilization!(project::P,
+ scenario_name::String,
+ total_utilization::Dict{String, Array{Float64, 2}}) where P <: Project{<: BuildPhase}
+
+ project_total_utilization = total_utilization[get_name(project)]
+ project.finance_data.scenario_total_utilization[scenario_name] = project_total_utilization
+
+ return
+end
+
+"""
+This function updates the expected capacity factors of Option projects.
+"""
+function update_total_utilization!(project::P,
+ scenario_name::String,
+ total_utilization::Dict{String, Array{Float64, 2}}) where P <: Project{Option}
+
+ tech = get_tech(project)
+ project_total_utilization = total_utilization["option_$(get_type(tech))_$(get_zone(tech))"]
+ project.finance_data.scenario_total_utilization[scenario_name] = project_total_utilization
+
+ return
+end
+
+"""
+This function updates the expected accepted capacity percentage of projects which are not in Option phase.
+"""
+function update_capacity_accepted_perc!(project::P,
+ scenario_name::String,
+ capacity_accepted_perc::Dict{String, Array{Float64, 1}}) where P <: Project{<: BuildPhase}
+ for product in get_products(project)
+ set_accepted_perc!(product, scenario_name, capacity_accepted_perc[get_name(project)])
+ end
+ return
+end
+
+"""
+This function updates the expected accepted capacity percentage of Option projects.
+"""
+function update_capacity_accepted_perc!(project::P,
+ scenario_name::String,
+ capacity_accepted_perc::Dict{String, Array{Float64, 1}}) where P <: Project{Option}
+
+ tech = get_tech(project)
+ for product in get_products(project)
+ set_accepted_perc!(product, scenario_name, capacity_accepted_perc["option_$(get_type(tech))_$(get_zone(tech))"])
+ end
+ return
+end
+
+"""
+This function does nothing if the product is not of Capacity type.
+"""
+function update_initial_capacity_revenues!(project::P,
+ product::T,
+ initial_capacity_prices::Vector{Float64},
+ year::Int64) where {P <: Project{<: BuildPhase}, T <: Product}
+ return
+end
+
+"""
+This function updates the capacity market revenues of project at the start of the simulation.
+"""
+function update_initial_capacity_revenues!(project::P,
+ product::Capacity,
+ initial_capacity_prices::Vector{Float64},
+ year::Int64) where P <: Project{<: BuildPhase}
+ capacity_revenue = initial_capacity_prices[year] * get_maxcap(project) * get_project_derating(project)
+
+ finance_data = get_finance_data(project)
+ set_realized_profit!(get_finance_data(project),
+ get_name(product),
+ year,
+ capacity_revenue)
+
+ for scenario_name in keys(get_scenario_profit(finance_data))
+ update_forward_profit!(product, finance_data, scenario_name, year, capacity_revenue)
+ end
+
+ return
+end
+
+"""
+This function does nothing if the product is not of Capacity or REC type.
+"""
+function update_bid!(product::T,
+ capacity_market_bid::Float64,
+ rec_market_bid::Float64,
+ energy_production::Float64,
+ iteration_year::Int64) where T <: Product
+ return
+end
+
+"""
+This function updates the Capacity market bid of projects.
+"""
+function update_bid!(product::Capacity,
+ capacity_market_bid::Float64,
+ rec_market_bid::Float64,
+ energy_production::Float64,
+ iteration_year::Int64)
+
+ set_capacity_bid!(product, capacity_market_bid)
+
+ return
+end
+
+"""
+This function updates the REC market bid of projects.
+"""
+function update_bid!(product::REC,
+ capacity_market_bid::Float64,
+ rec_market_bid::Float64,
+ energy_production::Float64,
+ iteration_year::Int64)
+
+ set_rec_bid!(product, rec_market_bid)
+ set_expected_rec_certificates!(product, energy_production * get_rec_correction_factor(product, iteration_year))
+ return
+end
+
+function calculate_carbon_cost_ratio(product::Product, carbon_cost_ratio::Float64, carbon_tax::Vector{Float64}, year::Int64)
+ return carbon_cost_ratio
+end
+
+function calculate_carbon_cost_ratio(product::CarbonTax, carbon_cost_ratio::Float64, carbon_tax::Vector{Float64}, year::Int64)
+ carbon_cost = get_emission_intensity(product) * carbon_tax[year]
+ ratio = carbon_cost / get_fuel_cost(product)
+ if !isnan(ratio)
+ carbon_cost_ratio = ratio
+ end
+ return carbon_cost_ratio
+end
+
+function update_device_operation_cost!(project::P, sys_UC::PSY.System, var_cost, fixed::Float64) where P <: Project{<:BuildPhase}
+ return
+end
+
+function update_device_operation_cost!(project::P, sys_UC::PSY.System, var_cost, fixed::Float64) where P <: Project{Existing}
+ name = get_name(project)
+ device = PSY.get_components_by_name(PSY.Device, sys_UC, name)[1]
+ device.operation_cost.variable.cost = var_cost
+ device.operation_cost.fixed = fixed
+ return
+end
+
+function update_operation_cost!(project::P, sys_UC::PSY.System, carbon_tax::Vector{Float64}, year::Int64) where P <: Project{<:BuildPhase}
+ return
+end
+
+function update_operation_cost!(project::P, sys_UC::Nothing, carbon_tax::Vector{Float64}, year::Int64) where P <: Project{<:BuildPhase}
+ return
+end
+
+function update_operation_cost!(project::P, sys_UC::PSY.System, carbon_tax::Vector{Float64}, year::Int64) where P <: Project{Existing}
+ operation_cost = get_operation_cost(get_tech(project))
+ if !(isnothing(operation_cost))
+ products = get_products(project)
+ carbon_cost_ratio = 0.0
+
+ for product in products
+ carbon_cost_ratio = calculate_carbon_cost_ratio(product, carbon_cost_ratio, carbon_tax, year)
+ end
+
+ total_cost_scalar = 1 + carbon_cost_ratio
+
+ var_cost = deepcopy(PSY.get_variable(operation_cost).cost)
+
+ fixed = deepcopy(PSY.get_fixed(operation_cost))
+
+ if length(var_cost) > 1
+ for i in 1:length(var_cost)
+ var_cost[i] = (var_cost[i][1] * total_cost_scalar, var_cost[i][2])
+ end
+ elseif length(var_cost) == 1
+ var_cost = var_cost * total_cost_scalar
+ end
+
+ fixed = fixed * total_cost_scalar
+
+ update_device_operation_cost!(project, sys_UC, var_cost, fixed)
+ end
+
+ return
+end
diff --git a/src/utils/parallel_utils.jl b/src/utils/parallel_utils.jl
index 4e90b84..3bd4b7c 100644
--- a/src/utils/parallel_utils.jl
+++ b/src/utils/parallel_utils.jl
@@ -49,12 +49,12 @@ function create_parallel_workers(case::CaseDefinition, hpc::Bool)
if num_workers_required > 0
if hpc
nodes = split(ENV["SLURM_NODELIST"], ",")
- num_procs = min(Int(ceil(num_workers_required / length(nodes))), 3)
+ num_procs = min(Int(ceil(num_workers_required / length(nodes))), 4)
node_pairs = [(n, num_procs) for n in nodes]
Distributed.addprocs(node_pairs)
else
- Distributed.addprocs(min(Int(num_workers_required), 3), lazy=false)
+ Distributed.addprocs(min(Int(num_workers_required), 4), lazy=false)
end
end
@@ -67,6 +67,10 @@ This function runs price prediction if investors are parallelized but scenarios
function parallelize_only_investors(investor::Investor,
sys_data_dir::String,
expected_portfolio::Vector{<: Project{<: BuildPhase}},
+ rps_target::String,
+ reserve_penalty::String,
+ resource_adequacy::ResourceAdequacy,
+ irm_scalar::Float64,
zones::Vector{String},
lines::Vector{ZonalLine},
peak_load::Float64,
@@ -92,6 +96,10 @@ function parallelize_only_investors(investor::Investor,
carbon_tax,
reserve_products,
ordc_products,
+ rps_target,
+ reserve_penalty,
+ resource_adequacy,
+ irm_scalar,
expected_portfolio,
zones,
lines,
diff --git a/src/utils/product_utils.jl b/src/utils/product_utils.jl
index 555f3b4..ddb9a8e 100644
--- a/src/utils/product_utils.jl
+++ b/src/utils/product_utils.jl
@@ -1,70 +1,77 @@
-
-"""
-This funtion does nothing if the product is not an operating market product.
-"""
-function get_maxperc(product::T, scenario_name::String, num_years::Int64, num_hours::Int64) where T <: Product
- return
-end
-
-"""
-This funtion returns the array of capacity factors if product is energy.
-"""
-function get_maxperc(product::Energy, scenario_name::String, num_years::Int64, num_hours::Int64)
- return get_capacity_factors(product)[scenario_name]
-end
-
-
-"""
-This funtion returns the array of maximum percentage limit if product is operating reserve.
-"""
-function get_maxperc(product::OperatingReserve, scenario_name::String, num_years::Int64, num_hours::Int64)
- return ones(num_years, num_hours) * get_max_limit(product)
-end
-
-"""
-This function does nothing if the product is not an operating market product.
-"""
-function push_operating!(array::Array{OperatingProduct}, product::T) where T <: Product
- return
-end
-
-"""
-This function pushes the product to the array if product is an operating market product.
-"""
-function push_operating!(array::Array{OperatingProduct}, product::OperatingProduct)
- push!(array, product)
- return
-end
-
-
-function push_energy_product!(array::Array{Energy}, product::OperatingProduct)
- return
-end
-
-function push_energy_product!(array::Array{Energy}, product::Energy)
- push!(array, product)
- return
-end
-
-function find_energy_product(products::Array{OperatingProduct})
- array = Energy[]
-
- for product in products
- push_energy_product!(array, product)
- end
-
- return array
-end
-
-"""
-This function returns an array of only operating market products from an array of all products.
-"""
-function find_operating_products(products::Array{Product})
- operating_products = OperatingProduct[]
-
- for i = 1:length(products)
- push_operating!(operating_products, products[i])
- end
-
-return operating_products
-end
+
+"""
+This funtion does nothing if the product is not an operating market product.
+"""
+function get_maxperc(product::T, scenario_name::String, num_years::Int64, num_hours::Int64) where T <: Product
+ return
+end
+
+"""
+This funtion returns the array of capacity factors if product is energy.
+"""
+function get_maxperc(product::Energy, scenario_name::String, num_years::Int64, num_hours::Int64)
+ return get_capacity_factors(product)[scenario_name]
+end
+
+
+"""
+This funtion returns the array of maximum percentage limit if product is operating reserve.
+"""
+function get_maxperc(product::OperatingReserve, scenario_name::String, num_years::Int64, num_hours::Int64)
+ return ones(num_years, num_hours) * get_max_limit(product)
+end
+
+"""
+This funtion returns the array of ones if product is inertia.
+"""
+function get_maxperc(product::Inertia, scenario_name::String, num_years::Int64, num_hours::Int64)
+ return ones(num_years, num_hours)
+end
+
+"""
+This function does nothing if the product is not an operating market product.
+"""
+function push_operating!(array::Array{OperatingProduct}, product::T) where T <: Product
+ return
+end
+
+"""
+This function pushes the product to the array if product is an operating market product.
+"""
+function push_operating!(array::Array{OperatingProduct}, product::OperatingProduct)
+ push!(array, product)
+ return
+end
+
+
+function push_energy_product!(array::Array{Energy}, product::OperatingProduct)
+ return
+end
+
+function push_energy_product!(array::Array{Energy}, product::Energy)
+ push!(array, product)
+ return
+end
+
+function find_energy_product(products::Array{OperatingProduct})
+ array = Energy[]
+
+ for product in products
+ push_energy_product!(array, product)
+ end
+
+ return array
+end
+
+"""
+This function returns an array of only operating market products from an array of all products.
+"""
+function find_operating_products(products::Array{Product})
+ operating_products = OperatingProduct[]
+
+ for i = 1:length(products)
+ push_operating!(operating_products, products[i])
+ end
+
+return operating_products
+end
diff --git a/src/utils/read_and_write_utils.jl b/src/utils/read_and_write_utils.jl
index 389c9e5..3ea3d40 100644
--- a/src/utils/read_and_write_utils.jl
+++ b/src/utils/read_and_write_utils.jl
@@ -1,32 +1,32 @@
-"""
-This function returns a dataframe of given csv data.
-"""
-function read_data(file_name::String)
- projectdata = DataFrames.DataFrame(CSV.File(file_name;
- truestrings=["T", "TRUE", "true"],
- falsestrings=["F", "FALSE", "false"]));
- return projectdata
-end
-
-function write_data(dir:: String, file_name::String, data::DataFrames.DataFrame)
-
- dir_exists(dir::String)
-
- CSV.write(joinpath(dir, file_name), data)
-
- return
-end
-
-"""
-This function makes the directory specified in the argument, if it doesn't exist.
-Returns nothing.
-"""
-function dir_exists(dir::String)
- try
- readdir(dir)
- catch err
- mkpath(dir)
- end
- return
-end
-
+"""
+This function returns a dataframe of given csv data.
+"""
+function read_data(file_name::String)
+ projectdata = DataFrames.DataFrame(CSV.File(file_name;
+ truestrings=["T", "TRUE", "true"],
+ falsestrings=["F", "FALSE", "false"]));
+
+ return projectdata
+end
+
+function write_data(dir:: String, file_name::String, data::DataFrames.DataFrame)
+
+ dir_exists(dir::String)
+ CSV.write(joinpath(dir, file_name), data)
+
+ return
+end
+
+"""
+This function makes the directory specified in the argument, if it doesn't exist.
+Returns nothing.
+"""
+function dir_exists(dir::String)
+ try
+ readdir(dir)
+ catch err
+ mkpath(dir)
+ end
+ return
+end
+
diff --git a/src/utils/siip_psy_utils.jl b/src/utils/siip_psy_utils.jl
index 8d6d854..25c8369 100644
--- a/src/utils/siip_psy_utils.jl
+++ b/src/utils/siip_psy_utils.jl
@@ -1,267 +1,552 @@
-function get_name(device::D) where D <: PSY.Device
- return device.name
-end
-
-"""
-This function returns all generation and storage technologies included in the PSY System.
-"""
-function get_all_techs(sys::PSY.System)
- sys_gens = PSY.get_components(PSY.Generator, sys)
- sys_storage = PSY.get_components(PSY.Storage, sys)
- sys_techs = union(sys_gens, sys_storage)
- return sys_techs
-end
-
-"""
-This function does nothing if the PSY System is not defined.
-"""
-function get_system_services(sys::Nothing)
- services = PSY.Service[]
- return services
-end
-
-"""
-This function returns all Services modeled in the PSY System.
-"""
-function get_system_services(sys::PSY.System)
- sys_techs = get_all_techs(sys)
-
- services = PSY.Service[]
- for tech in sys_techs
- for service in PSY.get_services(tech)
- if !in(service, services)
- push!(services, service)
- end
- end
- end
-
-
- return services
-end
-
-"""
-This function does nothing if Service is not of ReserveUp type.
-"""
-function add_device_services!(services::Vector{PSY.Service},
- device::PSY.Device,
- product::P) where P <: Product
- return
-end
-
-"""
-This function adds ReserveUp service for PSY Devices.
-"""
-function add_device_services!(services::Vector{PSY.Service},
- device::PSY.Device,
- product::OperatingReserve{ReserveUpEMIS})
-
- device_zone = PSY.get_name(PSY.get_load_zone(PSY.get_bus(device)))
-
- for service in services
- if typeof(service) == PSY.VariableReserve{PSY.ReserveUp}
- service_zone = "zone_$(split(PSY.get_name(service), "_")[end])"
- if device_zone == service_zone
- PSY.add_service!(device, service)
- end
- end
- end
-
- return
-end
-
-"""
-This function adds ReserveDown service for PSY Devices.
-"""
-function add_device_services!(services::Vector{PSY.Service},
- device::PSY.Device,
- product::OperatingReserve{ReserveDownEMIS})
-
- device_zone = PSY.get_name(PSY.get_load_zone(PSY.get_bus(device)))
-
- for service in services
- if typeof(service) == PSY.VariableReserve{PSY.ReserveDown}
- service_zone = "zone_$(split(PSY.get_name(service), "_")[end])"
- if device_zone == service_zone
- PSY.add_service!(device, service)
- end
- end
- end
-
- return
-end
-
-"""
-This function does nothing if Device is not of RenewableGen type.
-"""
-function add_device_forecast!(simulation_dir::String,
- sys::PSY.System,
- device::D,
- availability_df::Vector{Float64},
- availability_raw_rt::Vector{Float64},
- start_year::Int64
- ) where D <: Union{PSY.ThermalGen, PSY.HydroGen, PSY.Storage}
- return
-end
-
-"""
-This function adds forecast timeseries if Device is of RenewableGen type.
-"""
-function add_device_forecast!(simulation_dir::String,
- sys::PSY.System,
- device::D,
- availability_raw::Vector{Float64},
- availability_raw_rt::Vector{Float64},
- start_year::Int64) where D <: PSY.RenewableGen
-
-
- YearAhead = collect(
- Dates.DateTime("1/1/$(start_year) 0:00:00", "d/m/y H:M:S"):Dates.Hour(1):Dates.DateTime(
- "31/12/$(start_year) 23:00:00", "d/m/y H:M:S",),)
-
- timeseries = TS.TimeArray(YearAhead, availability_raw)
-
- PSY.add_forecast!(sys, device, IS.Deterministic("get_rating", timeseries))
-
- load_n_vg_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data.csv"))
- load_n_vg_df[:, get_name(device)] = availability_raw * get_device_size(device) * PSY.get_base_power(sys)
-
- load_n_vg_df_rt = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data_rt.csv"))
- load_n_vg_df_rt[:, get_name(device)] = availability_raw_rt * get_device_size(device) * PSY.get_base_power(sys)
-
- write_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data"), "load_n_vg_data.csv", load_n_vg_df)
- write_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data"), "load_n_vg_data_rt.csv", load_n_vg_df_rt)
-
- return
-end
-
-"""
-This function does nothing if PSY System is not defined.
-"""
-function update_PSY_timeseries!(sys_UC::Nothing,
- load_growth::AxisArrays.AxisArray{Float64, 1})
- return
-end
-
-"""
-This function updates the PSY load and reserve requirment timeseries each year.
-"""
-function update_PSY_timeseries!(sys::PSY.System,
- load_growth::AxisArrays.AxisArray{Float64, 1},
- simulation_dir::String)
-
- # update load timeseries.
- nodal_loads = PSY.get_components(PSY.ElectricLoad, sys)
- println("FDSFSdfsd")
- quit()
-
- for load in nodal_loads
- zone = "zone_$(PSY.get_name(PSY.get_area(PSY.get_bus(load))))"
-
- #=
- ts_data = PSY.get_data(PSY.get_time_series(
- PSY.SingleTimeSeries,
- load,
- "max_active_power"
- )
- )
-
- println(ts_data)
-
- ts_timestamps = TS.timestamp(ts_data)
- ts_values = TS.values(ts_data)
-
- PSY.remove_time_series!(sys,
- PSY.SingleTimeSeries,
- load,
- "max_active_power"
- )
-
- new_ts = TS.TimeArray(ts_timestamps, ts_values * (1 + load_growth[zone]))
- PSY.add_time_series!(sys, load, PSY.SingleTimeSeries("max_active_power", new_ts))
- =#
- scaled_active_power = deepcopy(PSY.get_max_active_power(load)) * (1 + load_growth[zone])
-
- PSY.set_max_active_power!(load, scaled_active_power)
-
- end
-
- average_load_growth = Statistics.mean(load_growth)
-
- # update service requirement timeseries.
- services = get_system_services(sys)
- ordc_products = split(read_data(joinpath(simulation_dir, "markets_data", "reserve_products.csv"))[1,"ordc_products"], "; ")
- for service in services
- #=
- try
- ts_data= PSY.get_data(PSY.get_time_series(
- PSY.SingleTimeSeries,
- service,
- "requirement"
- )
- )
- ts_timestamps = TS.timestamp(ts_data)
- ts_values = TS.values(ts_data)
-
- PSY.remove_time_series!(
- sys,
- PSY.SingleTimeSeries,
- service,
- "requirement",
- )
-
- new_ts = TS.TimeArray(ts_timestamps, ts_values * (1 + average_load_growth))
-
- PSY.add_time_series!(sys, service, PSY.SingleTimeSeries("requirement", new_ts))
- catch
-
- end
- =#
-
- if !(PSY.get_name(service) in ordc_products)
- scaled_requirement = deepcopy(PSY.get_requirement(service)) * (1 + average_load_growth)
-
- PSY.set_requirement!(service, scaled_requirement)
- end
- end
-
- return
-end
-
-"""
-This function finds the buses located in each zone.
-"""
-function find_zonal_bus(zone::String, sys::PSY.System)
- buses = PSY.get_components(PSY.Bus, sys)
-
- zonal_bus = nothing
-
- for b in buses
- if "zone_$(PSY.get_name(PSY.get_area(b)))" == zone
- zonal_bus = b
- end
- end
-
- return zonal_bus
-
-end
-
-"""
-This function transforms the timeseries of PSY Systems.
-"""
-
-function transform_psy_timeseries!(sys_UC::Nothing,
- sys_ED::Nothing,
- da_resolution::Int64,
- rt_resolution::Int64)
- return
-end
-
-function transform_psy_timeseries!(sys_UC::PSY.System,
- sys_ED::PSY.System,
- da_resolution::Int64,
- rt_resolution::Int64)
-
- PSY.transform_single_time_series!(sys_UC, Int(24 * 60 / da_resolution), Dates.Hour(24))
- PSY.transform_single_time_series!(sys_ED, Int(60 / rt_resolution), Dates.Hour(1))
- return
-end
+function get_name(device::D) where D <: PSY.Device
+ return device.name
+end
+
+"""
+This function returns all generation and storage technologies included in the PSY System.
+"""
+function get_all_techs(sys::PSY.System)
+ # sys_gens = PSY.get_components(PSY.Generator, sys)
+ sys_gens = PSY.get_components(PSY.Generator, sys, x -> PSY.get_available(x) == true)
+ sys_storage = PSY.get_components(PSY.Storage, sys)
+ sys_techs = union(sys_gens, sys_storage)
+ return sys_techs
+end
+
+"""
+This function does nothing if the PSY System is not defined.
+"""
+function get_system_services(sys::Nothing)
+ services = PSY.Service[]
+ return services
+end
+
+"""
+This function returns all Services modeled in the PSY System.
+"""
+function get_system_services(sys::PSY.System)
+ sys_techs = get_all_techs(sys)
+
+ services = PSY.Service[]
+ for tech in sys_techs
+ for service in PSY.get_services(tech)
+ if !in(service, services)
+ push!(services, service)
+ end
+ end
+ end
+
+
+ return services
+end
+
+"""
+This function does nothing if Service is not of ReserveUp type.
+"""
+function add_device_services!(sys::PSY.System,
+ device::PSY.Device,
+ product::P) where P <: Product
+ return
+end
+
+"""
+This function adds ReserveUp service for PSY Devices.
+"""
+function add_device_services!(sys::PSY.System,
+ device::PSY.Device,
+ product::Union{OperatingReserve{ReserveUpEMIS}, OperatingReserve{ReserveDownEMIS}, Inertia})
+
+
+ for service in get_system_services(sys)
+ if PSY.get_name(service) == String(get_name(product))
+ PSY.add_service!(device, service, sys)
+ end
+ end
+
+ return
+end
+
+function add_clean_energy_contribution!(sys_UC::PSY.System,
+ PSY_project_UC::PSY.Device)
+ return
+end
+
+function add_clean_energy_contribution!(sys::PSY.System,
+ device::T) where T <:Union{PSY.ThermalStandard, PSY.RenewableDispatch, PSY.HydroEnergyReservoir, PSY.HydroDispatch}
+
+ services = get_system_services(sys)
+ for service in services
+ if PSY.get_name(service) == "Clean_Energy"
+ PSY.add_service!(device, service, sys)
+ end
+ end
+ return
+end
+
+
+"""
+This function does nothing if Device is not of RenewableGen type.
+"""
+function add_device_forecast!(simulation_dir::String,
+ sys_UC::PSY.System,
+ sys_ED::PSY.System,
+ device_UC::D,
+ device_ED::D,
+ availability_df::Vector{Float64},
+ availability_raw_rt::Vector{Float64},
+ da_resolution::Int64,
+ rt_resolution::Int64
+ ) where D <: Union{PSY.ThermalGen, PSY.HydroGen, PSY.Storage}
+ return
+end
+
+"""
+This function adds forecast timeseries if Device is of RenewableGen type.
+"""
+function add_device_forecast!(simulation_dir::String,
+ sys_UC::PSY.System,
+ sys_ED::PSY.System,
+ device_UC::D,
+ device_ED::D,
+ availability_raw::Vector{Float64},
+ availability_raw_rt::Vector{Float64},
+ da_resolution::Int64,
+ rt_resolution::Int64) where D <: PSY.RenewableGen
+
+
+
+ ######### Adding to UC##########
+ # time_stamps = TS.timestamp(PSY.get_data(PSY.get_time_series(
+ # PSY.SingleTimeSeries,
+ # first(PSY.get_components(PSY.ElectricLoad, sys_UC)),
+ # "max_active_power"
+ # )))
+ # time_stamps = StepRange(Dates.DateTime("2018-01-01T00:00:00"), Dates.Hour(1), Dates.DateTime("2018-12-31T23:00:00"));
+ # intervals = Int(24 * 60 / da_resolution)
+ # append!(availability_raw, availability_raw[(length(availability_raw) - intervals + 1):end])
+ # data = Dict(time_stamps[i] => availability_raw[i:(i + intervals - 1)] for i in 1:intervals:length(time_stamps))
+ # forecast = PSY.Deterministic("max_active_power", data, Dates.Minute(da_resolution))
+ # PSY.add_time_series!(sys_UC, device_UC, forecast)
+
+ time_stamps = StepRange(Dates.DateTime("2018-01-01T00:00:00"), Dates.Hour(1), Dates.DateTime("2019-01-01T11:00:00"));
+ intervals = Int(36 * 60 / da_resolution)
+ append!(availability_raw, availability_raw[(length(availability_raw) - intervals + 25):end])
+ data = Dict(time_stamps[i] => availability_raw[i:(i + intervals - 1)] for i in 1:Int(24 * 60 / da_resolution):8760)
+ forecast = PSY.Deterministic("max_active_power", data, Dates.Minute(da_resolution))
+ PSY.add_time_series!(sys_UC, device_UC, forecast)
+
+ ######### Adding to ED##########
+ # time_stamps = TS.timestamp(PSY.get_data(PSY.get_time_series(
+ # PSY.SingleTimeSeries,
+ # first(PSY.get_components(PSY.ElectricLoad, sys_ED)),
+ # "max_active_power"
+ # )))
+ time_stamps = StepRange(Dates.DateTime("2018-01-01T00:00:00"), Dates.Hour(1), Dates.DateTime("2019-01-01T01:00:00"));
+ intervals = Int(2*60 / rt_resolution)
+ append!(availability_raw_rt, availability_raw_rt[(length(availability_raw_rt) - intervals + 1):end])
+ data = Dict(time_stamps[i] => availability_raw_rt[i:(i + intervals - 1)] for i in 1:Int(60 / rt_resolution):8760)
+ forecast = PSY.Deterministic("max_active_power", data, Dates.Minute(rt_resolution))
+ PSY.add_time_series!(sys_ED, device_ED, forecast)
+
+ ########## Adding to Net Load Data ##############
+ load_n_vg_df = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data.csv"))
+ load_n_vg_df[:, get_name(device_UC)] = availability_raw[1:DataFrames.nrow(load_n_vg_df)] * get_device_size(device_UC) * PSY.get_base_power(sys_UC)
+
+ load_n_vg_df_rt = read_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data", "load_n_vg_data_rt.csv"))
+ load_n_vg_df_rt[:, get_name(device_UC)] = availability_raw_rt[1:DataFrames.nrow(load_n_vg_df_rt)] * get_device_size(device_UC) * PSY.get_base_power(sys_UC)
+
+ write_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data"), "load_n_vg_data.csv", load_n_vg_df)
+ write_data(joinpath(simulation_dir, "timeseries_data_files", "Net Load Data"), "load_n_vg_data_rt.csv", load_n_vg_df_rt)
+
+ return
+end
+
+
+"""
+This function does nothing if PSY System is not defined.
+"""
+function update_PSY_timeseries!(sys_UC::Nothing,
+ load_growth::AxisArrays.AxisArray{Float64, 1},
+ rec_requirement::Float64,
+ simulation_dir::String,
+ type::String,
+ iteration_year::Int64,
+ da_resolution::Int64,
+ rt_resolution::Int64)
+ return
+end
+
+"""
+This function updates the PSY load and reserve requirment timeseries each year.
+"""
+function update_PSY_timeseries!(sys::PSY.System,
+ load_growth::AxisArrays.AxisArray{Float64, 1},
+ rec_requirement::Float64,
+ simulation_dir::String,
+ type::String,
+ iteration_year::Int64,
+ da_resolution::Int64,
+ rt_resolution::Int64)
+
+ total_active_power = 0.0
+
+ # update load timeseries.
+ nodal_loads = PSY.get_components(PSY.PowerLoad, sys)
+
+ for load in nodal_loads
+ zone = "zone_$(PSY.get_name(PSY.get_area(PSY.get_bus(load))))"
+ scaled_active_power = deepcopy(PSY.get_max_active_power(load)) * (1 + load_growth[zone])
+ println(PSY.get_name(load))
+ println(scaled_active_power)
+
+ PSY.set_max_active_power!(load, scaled_active_power)
+
+ total_active_power += scaled_active_power
+ end
+
+ println(total_active_power)
+
+ average_load_growth = Statistics.mean(load_growth)
+
+ # update service requirement timeseries.
+ services = get_system_services(sys)
+ ordc_products = split(read_data(joinpath(simulation_dir, "markets_data", "reserve_products.csv"))[1,"ordc_products"], "; ")
+ for service in services
+ service_name = PSY.get_name(service)
+ println("Current service is $(service_name)")
+ if service_name in ordc_products
+ # time_stamps = TS.timestamp(PSY.get_data(PSY.get_time_series(
+ # PSY.SingleTimeSeries,
+ # first(PSY.get_components(PSY.ElectricLoad, sys)),
+ # "max_active_power"
+ # )))
+ # time_stamps = StepRange(Dates.DateTime("2018-01-01T00:00:00"), Dates.Hour(1), Dates.DateTime("2018-12-31T23:00:00"));
+
+ PSY.remove_time_series!(sys, PSY.Deterministic, service, "variable_cost")
+ if type == "UC"
+ # product_ts_raw = read_data(joinpath(simulation_dir, "timeseries_data_files", "Reserves", "$(service_name)_$(iteration_year - 1).csv"))[:, service_name]
+ # product_data_ts = process_ordc_data_for_siip(product_ts_raw)
+ # intervals = Int(24 * 60 / da_resolution)
+ # append!(product_data_ts, product_data_ts[(length(product_data_ts) - intervals + 1):end])
+ # data = Dict(time_stamps[i] => product_data_ts[i:(i + intervals - 1)] for i in 1:intervals:length(time_stamps))
+ # forecast = PSY.Deterministic("variable_cost", data, Dates.Minute(da_resolution))
+
+ time_stamps = StepRange(Dates.DateTime("2018-01-01T00:00:00"), Dates.Hour(1), Dates.DateTime("2019-01-01T11:00:00"));
+ product_ts_raw = read_data(joinpath(simulation_dir, "timeseries_data_files", "Reserves", "$(service_name)_$(iteration_year - 1).csv"))[:, service_name]
+ product_data_ts = process_ordc_data_for_siip(product_ts_raw)
+ intervals = Int(36 * 60 / da_resolution)
+ append!(product_data_ts, product_data_ts[(length(product_data_ts) - intervals + 25):end])
+ data = Dict(time_stamps[i] => product_data_ts[i:(i + intervals - 1)] for i in 1:Int(24 * 60 / da_resolution):8760)
+ forecast = PSY.Deterministic("variable_cost", data, Dates.Minute(da_resolution))
+
+ elseif type == "ED"
+ # product_ts_raw = read_data(joinpath(simulation_dir, "timeseries_data_files", "Reserves", "$(service_name)_REAL_TIME_$(iteration_year - 1).csv"))[:, service_name]
+ # product_data_ts = process_ordc_data_for_siip(product_ts_raw)
+ # intervals = Int(60 / rt_resolution)
+ # append!(product_data_ts, product_data_ts[(length(product_data_ts) - intervals + 1):end])
+ # data = Dict(time_stamps[i] => product_data_ts[i:(i + intervals - 1)] for i in 1:intervals:length(time_stamps))
+ # forecast = PSY.Deterministic("variable_cost", data, Dates.Minute(rt_resolution))
+
+ time_stamps = StepRange(Dates.DateTime("2018-01-01T00:00:00"), Dates.Hour(1), Dates.DateTime("2019-01-01T01:00:00"));
+ product_ts_raw = read_data(joinpath(simulation_dir, "timeseries_data_files", "Reserves", "$(service_name)_REAL_TIME_$(iteration_year - 1).csv"))[:, service_name]
+ product_data_ts = process_ordc_data_for_siip(product_ts_raw)
+ intervals = Int(2*60 / rt_resolution)
+ append!(product_data_ts, product_data_ts[(length(product_data_ts) - intervals + 1):end])
+ data = Dict(time_stamps[i] => product_data_ts[i:(i + intervals - 1)] for i in 1:Int(60 / rt_resolution):8760)
+ forecast = PSY.Deterministic("variable_cost", data, Dates.Minute(rt_resolution))
+ else
+ error("Type should be UC or ED")
+ end
+
+ PSY.add_time_series!(sys, service, forecast)
+ elseif service_name == "Clean_Energy"
+ clean_energy_requirement = total_active_power * rec_requirement * 1.0
+ println(rec_requirement)
+ println(clean_energy_requirement)
+ PSY.set_requirement!(service, clean_energy_requirement)
+ else
+ scaled_requirement = deepcopy(PSY.get_requirement(service)) * (1 + average_load_growth)
+ PSY.set_requirement!(service, scaled_requirement)
+ end
+
+ end
+
+ return
+end
+
+"""
+This function finds the buses located in each zone.
+"""
+function find_zonal_bus(zone::String, sys::PSY.System)
+ buses = PSY.get_components(PSY.Bus, sys)
+
+ zonal_bus = nothing
+
+ for b in buses
+ if "zone_$(PSY.get_name(PSY.get_area(b)))" == zone
+ zonal_bus = b
+ end
+ end
+
+ return zonal_bus
+
+end
+
+"""
+This function transforms the timeseries of PSY Systems.
+"""
+
+function transform_psy_timeseries!(sys_UC::Nothing,
+ sys_ED::Nothing,
+ da_resolution::Int64,
+ rt_resolution::Int64,
+ da_horizon::Int64,
+ rt_horizon::Int64)
+ return
+end
+
+function transform_psy_timeseries!(sys_UC::PSY.System,
+ sys_ED::PSY.System,
+ da_resolution::Int64,
+ rt_resolution::Int64,
+ da_horizon::Int64,
+ rt_horizon::Int64)
+
+ PSY.transform_single_time_series!(sys_UC, Int(da_horizon * 60 / da_resolution), Dates.Hour(24))
+ PSY.transform_single_time_series!(sys_ED, Int(rt_horizon * 60/ rt_resolution), Dates.Hour(1))
+ return
+end
+
+function add_psy_inertia!(simulation_dir::String,
+ sys::Nothing,
+ reserve_penalty::String,
+ system_peak_load::Float64)
+
+ return
+
+end
+
+function add_psy_inertia!(simulation_dir::String,
+ sys::PSY.System,
+ type::String,
+ reserve_penalty::String,
+ system_peak_load::Float64)
+
+ inertia_data = read_data(joinpath(simulation_dir, "markets_data", "$(reserve_penalty)_reserve_penalty", "Inertia.csv"))
+
+ inertia_requirement = system_peak_load * inertia_data[1, "requirement_multiplier"] / PSY.get_base_power(sys)
+
+ ####### Adding Inertia reserve
+
+ inertia_reserve = PSY.VariableReserve{PSY.ReserveUp}(
+ "Inertia",
+ true,
+ 3600,
+ inertia_requirement,
+ )
+ contri_devices =
+ vcat(collect(PSY.get_components(PSY.ThermalStandard, sys)),
+ collect(PSY.get_components(ThermalFastStartSIIP, sys)),
+ collect(PSY.get_components(PSY.RenewableDispatch, sys)),
+ collect(PSY.get_components(PSY.HydroDispatch, sys)),
+ collect(PSY.get_components(PSY.HydroEnergyReservoir, sys)),
+ collect(PSY.get_components(PSY.GenericBattery, sys))
+ );
+
+ PSY.add_service!(sys, inertia_reserve, contri_devices)
+
+ # time_stamps = TS.timestamp(PSY.get_data(PSY.get_time_series(
+ # PSY.SingleTimeSeries,
+ # first(PSY.get_components(PSY.ElectricLoad, sys)),
+ # "max_active_power"
+ # )))
+ if type == "UC"
+ time_stamps = StepRange(Dates.DateTime("2018-01-01T00:00:00"), Dates.Hour(1), Dates.DateTime("2019-01-01T11:00:00"));
+ elseif type == "ED"
+ time_stamps = StepRange(Dates.DateTime("2018-01-01T00:00:00"), Dates.Hour(1), Dates.DateTime("2019-01-01T00:00:00"));
+ else
+ error("Type should be UC or ED")
+ end
+
+ ts_data = ones(length(time_stamps))
+ ts = TimeSeries.TimeArray(time_stamps, ts_data);
+ forecast = PSY.SingleTimeSeries("requirement", ts)
+ PSY.add_time_series!(sys, inertia_reserve, forecast)
+
+end
+
+function add_psy_clean_energy_constraint!(simulation_dir::String,
+ sys::Nothing,
+ requirement::Float64)
+
+ return
+
+end
+
+function add_psy_clean_energy_constraint!(sys::PSY.System,
+ requirement::Float64)
+
+ ####### Adding Clean Energy Constraint
+ total_active_power = 0.0
+ # time_stamps = TS.timestamp(PSY.get_data(PSY.get_time_series(
+ # PSY.SingleTimeSeries,
+ # first(PSY.get_components(PSY.ElectricLoad, sys)),
+ # "max_active_power"
+ # )))
+ time_stamps = StepRange(Dates.DateTime("2018-01-01T00:00:00"), Dates.Hour(1), Dates.DateTime("2019-01-01T11:00:00"));
+ #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+ clean_energy_ts_data = zeros(length(time_stamps))
+
+ nodal_loads = PSY.get_components(PSY.PowerLoad, sys)
+ for load in nodal_loads
+ load_active_power = PSY.get_max_active_power(load)
+ total_active_power += load_active_power
+ # ts_data = deepcopy(PSY.get_data(PSY.get_time_series(PSY.SingleTimeSeries,
+ # load,
+ # "max_active_power"
+ # )))
+ loadts_raw=PSY.get_data(PSY.get_time_series(PSY.Deterministic,
+ load,
+ "max_active_power"
+ ))
+ ts_data =[]
+ for timestep in keys(loadts_raw)
+ ts_data=[ts_data;loadts_raw[timestep][1:24]]
+ end
+ ts_data=[ts_data;loadts_raw[Dates.DateTime("2018-12-31T00:00:00")][25:36]]
+ clean_energy_ts_data .+= (TS.values(ts_data) .* load_active_power)
+ end
+
+ clean_energy_ts_data = clean_energy_ts_data / total_active_power
+
+ clean_energy_reserve = PSY.VariableReserve{PSY.ReserveUp}(
+ "Clean_Energy",
+ true,
+ 3600,
+ total_active_power * requirement,
+ )
+ contri_devices =
+ vcat(collect(PSY.get_components(PSY.ThermalStandard, sys, x -> (occursin("NUC", PSY.get_name(x)) || occursin("RECT", PSY.get_name(x))))),
+ collect(PSY.get_components(PSY.RenewableDispatch, sys)),
+ collect(PSY.get_components(PSY.HydroDispatch, sys)),
+ collect(PSY.get_components(PSY.HydroEnergyReservoir, sys)),
+ );
+
+ PSY.add_service!(sys, clean_energy_reserve, contri_devices)
+
+ ts_data = TimeSeries.TimeArray(time_stamps, clean_energy_ts_data)
+ forecast = PSY.SingleTimeSeries("requirement", ts_data)
+ PSY.add_time_series!(sys, clean_energy_reserve, forecast)
+
+end
+
+function calculate_total_load(sys::PSY.System, time_resolution::Int64)
+ total_load = 0.0
+
+ nodal_loads = PSY.get_components(PSY.PowerLoad, sys)
+ for load in nodal_loads
+ zone = "zone_$(PSY.get_name(PSY.get_area(PSY.get_bus(load))))"
+ # ts_data = PSY.get_data(PSY.get_time_series(
+ # PSY.SingleTimeSeries,
+ # load,
+ # "max_active_power"
+ # )
+ # )
+ loadts_raw=PSY.get_data(PSY.get_time_series(PSY.Deterministic,
+ load,
+ "max_active_power"
+ ))
+ ts_data =[]
+ for timestep in keys(loadts_raw)
+ ts_data=[ts_data;loadts_raw[timestep][1:Int(8760/length(keys(loadts_raw)))]]
+ end
+ total_load += sum(TS.values(ts_data) * PSY.get_max_active_power(load)) * time_resolution / 60
+
+ end
+ return total_load
+end
+
+function convert_to_thermal_clean_energy!(d::PSY.ThermalStandard, system::PSY.System)
+ new = PSYE.ThermalCleanEnergy(;
+ name = d.name,
+ available = d.available,
+ status = d.status,
+ bus = d.bus,
+ active_power = d.active_power,
+ reactive_power = d.reactive_power,
+ rating = d.rating,
+ active_power_limits = d.active_power_limits,
+ reactive_power_limits = d.reactive_power_limits,
+ ramp_limits = d.ramp_limits,
+ operation_cost = d.operation_cost,
+ base_power = d.base_power,
+ time_limits = d.time_limits,
+ prime_mover = d.prime_mover,
+ fuel = d.fuel,
+ ext = d.ext
+ )
+
+ PSY.add_component!(system, new)
+ for service in PSY.get_services(d)
+ PSY.add_service!(new, service, system)
+ end
+
+ PSY.remove_component!(system, d)
+ return
+end
+
+function convert_thermal_clean_energy!(system::PSY.System)
+ for gen in PSY.get_components(PSY.ThermalStandard, system)
+ name = PSY.get_name(gen)
+ if occursin("RECT", name) #occursin("NUC", name) || occursin("RECT", name)
+ convert_to_thermal_clean_energy!(gen, system)
+ end
+ if occursin("NUCLEAR", string(PSY.get_fuel(gen))) && occursin("ST", string(PSY.get_prime_mover(gen)))
+ convert_to_thermal_clean_energy!(gen, system)
+ end
+ end
+end
+
+function convert_to_thermal_fast_start!(d::PSY.ThermalStandard, system::PSY.System)
+ new = ThermalFastStartSIIP(;
+ name = d.name,
+ available = d.available,
+ status = d.status,
+ bus = d.bus,
+ active_power = d.active_power,
+ reactive_power = d.reactive_power,
+ rating = d.rating,
+ active_power_limits = d.active_power_limits,
+ reactive_power_limits = d.reactive_power_limits,
+ ramp_limits = d.ramp_limits,
+ operation_cost = d.operation_cost,
+ base_power = d.base_power,
+ time_limits = d.time_limits,
+ prime_mover = d.prime_mover,
+ fuel = d.fuel,
+ ext = d.ext
+ )
+
+ PSY.add_component!(system, new)
+ for service in PSY.get_services(d)
+ PSY.add_service!(new, service, system)
+ end
+
+ PSY.remove_component!(system, d)
+ return
+end
+
+function convert_thermal_fast_start!(system::PSY.System)
+ for gen in PSY.get_components(PSY.ThermalStandard, system)
+ prime_mover = PSY.get_prime_mover(gen)
+ fuel = PSY.get_fuel(gen)
+
+ target_prime_mover = PSY.PrimeMovers.CT
+ target_fuel = PSY.ThermalFuels.NATURAL_GAS
+
+ if prime_mover == target_prime_mover && fuel == target_fuel
+ convert_to_thermal_fast_start!(gen, system)
+ end
+ end
+end