diff --git a/CHANGELOG.md b/CHANGELOG.md index 18ea0f2e09..24d7c91ba0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 non-zero in multi-stage GenX (#666) - Added condition number scaling added to objective function (#667) - Added versioned doc-pages for v0.3.6 and v0.4.0 +- Add constraint in mga to compute total capacity in each zone from a given technology type (#681) +- New settings parameter MGAAnnualGeneration to switch between different MGA formulations (#681) ### Fixed - Set MUST_RUN=1 for RealSystemExample/small_hydro plants (#517). diff --git a/docs/src/User_Guide/generate_alternatives.md b/docs/src/User_Guide/generate_alternatives.md index 580629af75..f0fe836362 100644 --- a/docs/src/User_Guide/generate_alternatives.md +++ b/docs/src/User_Guide/generate_alternatives.md @@ -4,12 +4,10 @@ GenX includes a modeling to generate alternatives (MGA) package that can be used 1. Add a `Resource_Type` column in all the resource `.csv` files denoting the type of each technology. 2. Add a `MGA` column in all the resource `.csv` files denoting the availability of the technology. -3. Set the `ModelingToGenerateAlternatives` flag in the `GenX_Settings.yml` file to 1. -4. Set the `ModelingtoGenerateAlternativeSlack` flag in the `GenX_Settings.yml` file to the desirable level of slack. -5. Create a `Rand_mga_objective_coefficients.csv` file to provide random objective function coefficients for each MGA iteration. +3. Set the `ModelingToGenerateAlternatives` flag in the `genx_settings.yml` file to 1. +4. Set the `ModelingtoGenerateAlternativeSlack` flag in the `genx_settings.yml` file to the desirable level of slack. +5. Set the `ModelingToGenerateAlternativesIterations` flag to half the total number of desired solutions, as each iteration provides 2 solutions. +6. Set the `MGAAnnualGeneration` flag in the `genx_settings.yml` file to the desired MGA formulation. +7. Solve the model using `Run.jl` file. -For each iteration, number of rows in the `Rand_mga_objective_coefficients`.csv file represents the number of distinct technology types while number of columns represent the number of model zones. - -Solve the model using `Run.jl` file. - -Results from the MGA algorithm would be saved in MGA_max and MGA_min folders in the `Example_Systems/` folder. \ No newline at end of file +Results from the MGA algorithm would be saved in MGA_max and MGA_min folders in the case folder. \ No newline at end of file diff --git a/src/additional_tools/modeling_to_generate_alternatives.jl b/src/additional_tools/modeling_to_generate_alternatives.jl index ba0fec33e3..0fe990c7be 100644 --- a/src/additional_tools/modeling_to_generate_alternatives.jl +++ b/src/additional_tools/modeling_to_generate_alternatives.jl @@ -3,20 +3,35 @@ We have implemented an updated Modeling to Generate Alternatives (MGA) Algorithm proposed by [Berntsen and Trutnevyte (2017)](https://www.sciencedirect.com/science/article/pii/S0360544217304097) to generate a set of feasible, near cost-optimal technology portfolios. This algorithm was developed by [Brill Jr, E. D., 1979](https://pubsonline.informs.org/doi/abs/10.1287/mnsc.25.5.413) and introduced to energy system planning by [DeCarolia, J. F., 2011](https://www.sciencedirect.com/science/article/pii/S0140988310000721). -To create the MGA formulation, we replace the cost-minimizing objective function of GenX with a new objective function that creates multiple generation portfolios by zone. We further add a new budget constraint based on the optimal objective function value $f^*$ of the least-cost model and the user-specified value of slack $\delta$. After adding the slack constraint, the resulting MGA formulation is given as: +To create the MGA formulation, we replace the cost-minimizing objective function of GenX with a new objective function that creates multiple generation portfolios by zone. We further add a new budget constraint based on the optimal objective function value $f^*$ of the least-cost model and the user-specified value of slack $\delta$. After adding the slack constraint, the resulting MGA formulation is given as (`MGAAnnualGeneration = 0` in the genx_settings.yml file, or not set): ```math \begin{aligned} \text{max/min} \quad &\sum_{z \in \mathcal{Z}}\sum_{r \in \mathcal{R}} \beta_{z,r}^{k}P_{z,r}\\ \text{s.t.} \quad - &P_{zr} = \sum_{y \in \mathcal{G}}\sum_{t \in \mathcal{T}} \omega_{t} \Theta_{y,t,z,r} \\ + &P_{z,r} = \sum_{y \in \mathcal{G}}C_{y,z,r} \\ & f \leq f^* + \delta \\ &Ax = b \end{aligned} ``` -where, $\beta_{zr}$ is a random objective fucntion coefficient betwen $[0,100]$ for MGA iteration $k$. $\Theta_{y,t,z,r}$ is a generation of technology $y$ in zone $z$ in time period $t$ that belongs to a resource type $r$. We aggregate $\Theta_{y,t,z,r}$ into a new variable $P_{z,r}$ that represents total generation from technology type $r$ in a zone $z$. In the second constraint above, $\delta$ denote the increase in budget from the least-cost solution and $f$ represents the expression for the total system cost. The constraint $Ax = b$ represents all other constraints in the power system model. We then solve the formulation with minimization and maximization objective function to explore near optimal solution space. +where, $\beta_{z,r}$ is a random objective function coefficient betwen $[0,1]$ for MGA iteration $k$. We aggregate capacity into a new variable $P_{z,r}$ that represents total capacity from technology type $r$ in a zone $z$. + +If the users set `MGAAnnualGeneration = 1` in the genx_settings.yml file, the MGA formulation is given as: +```math +\begin{aligned} +\text{max/min} \quad + &\sum_{z \in \mathcal{Z}}\sum_{r \in \mathcal{R}} \beta_{z,r}^{k}P_{z,r}\\ + \text{s.t.} \quad + &P_{z,r} = \sum_{y \in \mathcal{G}}\sum_{t \in \mathcal{T}} \omega_{t} \Theta_{y,t,z,r} \\ + & f \leq f^* + \delta \\ + &Ax = b +\end{aligned} +``` +where, $\beta_{z,r}$ is a random objective function coefficient betwen $[0,1]$ for MGA iteration $k$. $\Theta_{y,t,z,r}$ is a generation of technology $y$ in zone $z$ in time period $t$ that belongs to a resource type $r$. We aggregate $\Theta_{y,t,z,r}$ into a new variable $P_{z,r}$ that represents total generation from technology type $r$ in a zone $z$. + +In the second constraint in both the above formulations, $\delta$ denote the increase in budget from the least-cost solution and $f$ represents the expression for the total system cost. The constraint $Ax = b$ represents all other constraints in the power system model. We then solve the formulation with minimization and maximization objective function to explore near optimal solution space. """ function mga(EP::Model, path::AbstractString, setup::Dict, inputs::Dict) if setup["ModelingToGenerateAlternatives"] == 1 @@ -39,29 +54,10 @@ function mga(EP::Model, path::AbstractString, setup::Dict, inputs::Dict) # Read slack parameter representing desired increase in budget from the least cost solution slack = setup["ModelingtoGenerateAlternativeSlack"] - ### Variables ### - - @variable(EP, vSumvP[TechTypes = 1:length(TechTypes), z = 1:Z]>=0) # Variable denoting total generation from eligible technology of a given type - - ### End Variables ### - ### Constraints ### # Constraint to set budget for MGA iterations @constraint(EP, budget, EP[:eObj]<=Least_System_Cost * (1 + slack)) - - # Constraint to compute total generation in each zone from a given Technology Type - function resource_in_zone_with_TechType(tt::Int64, z::Int64) - condition::BitVector = (resource_type_mga.(gen) .== TechTypes[tt]) .& - (zone_id.(gen) .== z) - return resource_id.(gen[condition]) - end - @constraint(EP, - cGeneration[tt = 1:length(TechTypes), z = 1:Z], - vSumvP[tt, - z]==sum(EP[:vP][y, t] * inputs["omega"][t] - for y in resource_in_zone_with_TechType(tt, z), t in 1:T)) - ### End Constraints ### ### Create Results Directory for MGA iterations @@ -87,7 +83,8 @@ function mga(EP::Model, path::AbstractString, setup::Dict, inputs::Dict) ### Maximization objective @objective(EP, Max, - sum(pRand[tt, z] * vSumvP[tt, z] for tt in 1:length(TechTypes), z in 1:Z)) + sum(pRand[tt, z] * EP[:vMGA][tt, z] + for tt in 1:length(TechTypes), z in 1:Z)) # Solve Model Iteration status = optimize!(EP) @@ -101,7 +98,8 @@ function mga(EP::Model, path::AbstractString, setup::Dict, inputs::Dict) ### Minimization objective @objective(EP, Min, - sum(pRand[tt, z] * vSumvP[tt, z] for tt in 1:length(TechTypes), z in 1:Z)) + sum(pRand[tt, z] * EP[:vMGA][tt, z] + for tt in 1:length(TechTypes), z in 1:Z)) # Solve Model Iteration status = optimize!(EP) @@ -117,3 +115,61 @@ function mga(EP::Model, path::AbstractString, setup::Dict, inputs::Dict) ### End MGA Iterations ### end end + +@doc raw""" + mga!(EP::Model, inputs::Dict, setup::Dict) + +This function reads the input data, collect the resources with MGA flag on and creates a set of unique technology types. +The function then adds a constraint to the model to compute total capacity in each zone from a given Technology Type. + +If the user set `MGAAnnualGeneration = 0` in the genx_settings.yml file, the constraint has the following form: +```math +P_{z,r} = \sum_{y \in \mathcal{G}}C_{y,z,r} +``` +where, the aggregated capacity $P_{z,r}$ represents total capacity from technology type $r$ in a zone $z$. + +If the user set `MGAAnnualGeneration = 1` in the genx_settings.yml file, the constraint has the following form: +```math +P_{z,r} = \sum_{y \in \mathcal{G}}\sum_{t \in \mathcal{T}} \omega_{t} \Theta_{y,t,z,r} +``` +where $\Theta_{y,t,z,r}$ is a generation of technology $y$ in zone $z$ in time period $t$ that belongs to a resource type $r$. $\Theta_{y,t,z,r}$ is aggregated into a new variable $P_{z,r}$ that represents total generation from technology type $r$ in a zone $z$. + +# Arguments +- `EP::Model`: GenX model object +- `inputs::Dict`: Dictionary containing input data + +# Returns +- This function updates the model object `EP` with the MGA variables and constraints in-place. +""" +function mga!(EP::Model, inputs::Dict, setup::Dict) + println("MGA Module") + + T = inputs["T"] # Number of time steps (hours) + Z = inputs["Z"] # Number of zones + gen = inputs["RESOURCES"] # Resources data + # Read set of MGA variables + annual_gen = setup["MGAAnnualGeneration"] ### Choose setting in genx_settings.yaml: MGAAnnualGeneration: 1 = annual generation, otherwise, sum of capacity + # Create a set of unique technology types + resources_with_mga_on = gen[ids_with_mga(gen)] + TechTypes = unique(resource_type_mga.(resources_with_mga_on)) + + function resource_in_zone_same_TechType(tt::Int64, z::Int64) + condition::BitVector = (resource_type_mga.(gen) .== TechTypes[tt]) .& + (zone_id.(gen) .== z) + return resource_id.(gen[condition]) + end + # Constraint to compute total generation in each zone from a given Technology Type + ### Variables ### + @variable(EP, vMGA[TechTypes = 1:length(TechTypes), z = 1:Z]>=0) + + ### Constraint ### + if annual_gen == 1 # annual generation + @constraint(EP, cGeneration[tt = 1:length(TechTypes), z = 1:Z], + vMGA[tt,z]==sum(EP[:vP][y, t] * inputs["omega"][t] + for y in resource_in_zone_same_TechType(tt, z), t in 1:T)) + else + @constraint(EP, cCapEquiv[tt = 1:length(TechTypes), z = 1:Z], + vMGA[tt,z]==sum(EP[:eTotalCap][y] + for y in resource_in_zone_same_TechType(tt, z))) + end +end diff --git a/src/case_runners/case_runner.jl b/src/case_runners/case_runner.jl index f5725e7bfc..13b30ac7b5 100644 --- a/src/case_runners/case_runner.jl +++ b/src/case_runners/case_runner.jl @@ -94,7 +94,7 @@ function run_genx_case_simple!(case::AbstractString, mysetup::Dict, optimizer::A println(elapsed_time) if mysetup["ModelingToGenerateAlternatives"] == 1 println("Starting Model to Generate Alternatives (MGA) Iterations") - mga(EP, case, mysetup, myinputs, outputs_path) + mga(EP, case, mysetup, myinputs) end if mysetup["MethodofMorris"] == 1 diff --git a/src/configure_settings/configure_settings.jl b/src/configure_settings/configure_settings.jl index d37107f256..3130bab7cf 100644 --- a/src/configure_settings/configure_settings.jl +++ b/src/configure_settings/configure_settings.jl @@ -18,6 +18,7 @@ function default_settings() "TimeDomainReductionFolder" => "TDR_results", "ModelingToGenerateAlternatives" => 0, "ModelingtoGenerateAlternativeSlack" => 0.1, + "MGAAnnualGeneration" => 0, "MultiStage" => 0, "MethodofMorris" => 0, "IncludeLossesInESR" => 0, diff --git a/src/model/generate_model.jl b/src/model/generate_model.jl index 18f3f98775..bac3e661a8 100644 --- a/src/model/generate_model.jl +++ b/src/model/generate_model.jl @@ -225,6 +225,10 @@ function generate_model(setup::Dict, inputs::Dict, OPTIMIZER::MOI.OptimizerWithA maximum_capacity_requirement!(EP, inputs, setup) end + if setup["ModelingToGenerateAlternatives"] == 1 + mga!(EP, inputs, setup) + end + ## Define the objective function @objective(EP, Min, setup["ObjScale"]*EP[:eObj])