From 28fed7e962590b4e8b51dc50e66ccd51080ece09 Mon Sep 17 00:00:00 2001 From: Luca Bonaldo <39280783+lbonaldo@users.noreply.github.com> Date: Fri, 27 Jun 2025 03:51:58 -0400 Subject: [PATCH 1/5] Rename `NetExport` to `NetImport` in power balance output (#853) --- CHANGELOG.md | 3 +++ Project.toml | 2 +- src/write_outputs/write_power_balance.jl | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9a117e329..d5674acc09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed +- Rename `Transmission_NetExport` to `Transmission_NetImport` in `power_balance.csv` (#853). + ## [0.4.5] - 2025-07-07 ### Added diff --git a/Project.toml b/Project.toml index 732858a675..56f1ade663 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "GenX" uuid = "5d317b1e-30ec-4ed6-a8ce-8d2d88d7cfac" authors = ["Bonaldo, Luca", "Chakrabarti, Sambuddha", "Cheng, Fangwei", "Ding, Yifu", "Jenkins, Jesse D.", "Luo, Qian", "Macdonald, Ruaridh", "Mallapragada, Dharik", "Manocha, Aneesha", "Mantegna, Gabe ", "Morris, Jack", "Patankar, Neha", "Pecci, Filippo", "Schwartz, Aaron", "Schwartz, Jacob", "Schivley, Greg", "Sepulveda, Nestor", "Xu, Qingyu", "Zhou, Justin"] -version = "0.4.5" +version = "0.4.5-dev.1" [deps] CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" diff --git a/src/write_outputs/write_power_balance.jl b/src/write_outputs/write_power_balance.jl index 69dd596bc5..6a8e286ff1 100644 --- a/src/write_outputs/write_power_balance.jl +++ b/src/write_outputs/write_power_balance.jl @@ -16,7 +16,7 @@ function write_power_balance(path::AbstractString, inputs::Dict, setup::Dict, EP Com_list = ["Generation", "Storage_Discharge", "Storage_Charge", "Flexible_Demand_Defer", "Flexible_Demand_Stasify", "Demand_Response", "Nonserved_Energy", - "Transmission_NetExport", "Transmission_Losses", + "Transmission_NetImport", "Transmission_Losses", "Demand"] if !isempty(ELECTROLYZER) push!(Com_list, "Electrolyzer_Consumption") From 55eaa7271a2db0031428243a616d1e430276a843 Mon Sep 17 00:00:00 2001 From: Qingyu Xu Date: Wed, 2 Jul 2025 16:12:06 +0800 Subject: [PATCH 2/5] Separate fixed cost into FOM and investment cost This update introduces two new groups of expressions `cFom` and `cInv` to separate fixed costs into fixed O&M and investment cost. --------- Co-authored-by: lbonaldo --- CHANGELOG.md | 1 + .../core/discharge/investment_discharge.jl | 34 ++- .../resources/storage/investment_charge.jl | 31 ++- .../resources/storage/investment_energy.jl | 30 ++- src/model/resources/vre_stor/vre_stor.jl | 241 ++++++++++++++---- src/write_outputs/write_costs.jl | 140 ++++++++-- .../writing_outputs/test_zone_no_resources.jl | 29 ++- .../zone_no_resources/resources/Vre.csv | 6 +- 8 files changed, 405 insertions(+), 107 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5674acc09..3a297d69b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Models running with, non-default, solvers Cbc and Clp will fail unless `"EnableJuMPDirectMode"` is set to false (#835). - Improve `@expressions` performance by pre-processing sets (#815). +- Add `eCFom` and `eCInv` to track FOM and investment costs separately (#809). ### Fixed - Fix call to `get_retirement_stage` by casting `lifetime` to integer (#840). diff --git a/src/model/core/discharge/investment_discharge.jl b/src/model/core/discharge/investment_discharge.jl index e0b07120b1..0bacdcdbfe 100755 --- a/src/model/core/discharge/investment_discharge.jl +++ b/src/model/core/discharge/investment_discharge.jl @@ -111,21 +111,39 @@ function investment_discharge!(EP::Model, inputs::Dict, setup::Dict) eExistingCap[y] end) - ### Need editting ## - @expression(EP, eCFix[y in 1:G], - if y in NEW_CAP # Resources eligible for new capacity (Non-Retrofit) + ## Objective Function Expressions ## + + # Annuitized investment cost expression - only applies to resources eligible for new capacity + @expression(EP, eCInv[y in 1:G], + if y in NEW_CAP + investment_cost = inv_cost_per_mwyr(gen[y]) if y in COMMIT - inv_cost_per_mwyr(gen[y]) * cap_size(gen[y]) * vCAP[y] + - fixed_om_cost_per_mwyr(gen[y]) * eTotalCap[y] + investment_cost * cap_size(gen[y]) * vCAP[y] else - inv_cost_per_mwyr(gen[y]) * vCAP[y] + - fixed_om_cost_per_mwyr(gen[y]) * eTotalCap[y] + investment_cost * vCAP[y] end else - fixed_om_cost_per_mwyr(gen[y]) * eTotalCap[y] + 0 end) + + # Fixed O&M cost expression - applies to all resources + @expression(EP, eCFom[y in 1:G], + fixed_om_cost_per_mwyr(gen[y]) * eTotalCap[y]) + + # Total fixed cost expression - combines investment and fixed O&M costs + @expression(EP, eCFix[y in 1:G], + if y in NEW_CAP + # For resources with new capacity: investment cost + fixed O&M cost + EP[:eCInv][y] + EP[:eCFom][y] + else + # For existing resources: only fixed O&M cost + EP[:eCFom][y] + end) + # Sum individual resource contributions to fixed costs to get total fixed costs @expression(EP, eTotalCFix, sum(EP[:eCFix][y] for y in 1:G)) + @expression(EP, eTotalCInv, sum(EP[:eCInv][y] for y in 1:G)) + @expression(EP, eTotalCFom, sum(EP[:eCFom][y] for y in 1:G)) # Add term to objective function expression if MultiStage == 1 diff --git a/src/model/resources/storage/investment_charge.jl b/src/model/resources/storage/investment_charge.jl index cb14122840..140c54e158 100644 --- a/src/model/resources/storage/investment_charge.jl +++ b/src/model/resources/storage/investment_charge.jl @@ -89,20 +89,33 @@ function investment_charge!(EP::Model, inputs::Dict, setup::Dict) end) ## Objective Function Expressions ## - - # Fixed costs for resource "y" = annuitized investment cost plus fixed O&M costs - # If resource is not eligible for new charge capacity, fixed costs are only O&M costs + + # Annuitized investment cost expression for charge capacity - only applies to resources eligible for new charge capacity + @expression(EP, eCInvCharge[y in STOR_ASYMMETRIC], + if y in NEW_CAP_CHARGE + inv_cost_charge_per_mwyr(gen[y]) * vCAPCHARGE[y] + else + 0 + end) + + # Fixed O&M cost expression for charge capacity - applies to all resources + @expression(EP, eCFomCharge[y in STOR_ASYMMETRIC], + fixed_om_cost_charge_per_mwyr(gen[y]) * eTotalCapCharge[y]) + + # Total fixed cost expression for charge capacity - combines investment and fixed O&M costs @expression(EP, eCFixCharge[y in STOR_ASYMMETRIC], - if y in NEW_CAP_CHARGE # Resources eligible for new charge capacity - inv_cost_charge_per_mwyr(gen[y]) * vCAPCHARGE[y] + - fixed_om_cost_charge_per_mwyr(gen[y]) * eTotalCapCharge[y] + if y in NEW_CAP_CHARGE + # For resources with new charge capacity: investment cost + fixed O&M cost + EP[:eCInvCharge][y] + EP[:eCFomCharge][y] else - fixed_om_cost_charge_per_mwyr(gen[y]) * eTotalCapCharge[y] + # For existing resources: only fixed O&M cost + EP[:eCFomCharge][y] end) - + # Sum individual resource contributions to fixed costs to get total fixed costs @expression(EP, eTotalCFixCharge, sum(EP[:eCFixCharge][y] for y in STOR_ASYMMETRIC)) - + @expression(EP, eTotalCInvCharge, sum(EP[:eCInvCharge][y] for y in STOR_ASYMMETRIC)) + @expression(EP, eTotalCFomCharge, sum(EP[:eCFomCharge][y] for y in STOR_ASYMMETRIC)) # Add term to objective function expression if MultiStage == 1 # OPEX multiplier scales fixed costs to account for multiple years between two model stages diff --git a/src/model/resources/storage/investment_energy.jl b/src/model/resources/storage/investment_energy.jl index 24078b62d7..2891febad3 100644 --- a/src/model/resources/storage/investment_energy.jl +++ b/src/model/resources/storage/investment_energy.jl @@ -90,19 +90,33 @@ function investment_energy!(EP::Model, inputs::Dict, setup::Dict) end) ## Objective Function Expressions ## - - # Fixed costs for resource "y" = annuitized investment cost plus fixed O&M costs - # If resource is not eligible for new energy capacity, fixed costs are only O&M costs + + # Annuitized investment cost expression for energy capacity - only applies to resources eligible for new energy capacity + @expression(EP, eCInvEnergy[y in STOR_ALL], + if y in NEW_CAP_ENERGY + inv_cost_per_mwhyr(gen[y]) * vCAPENERGY[y] + else + 0 + end) + + # Fixed O&M cost expression for energy capacity - applies to all resources based on total energy capacity + @expression(EP, eCFomEnergy[y in STOR_ALL], + fixed_om_cost_per_mwhyr(gen[y]) * eTotalCapEnergy[y]) + + # Total fixed cost expression for energy capacity - combines investment and fixed O&M costs @expression(EP, eCFixEnergy[y in STOR_ALL], - if y in NEW_CAP_ENERGY # Resources eligible for new capacity - inv_cost_per_mwhyr(gen[y]) * vCAPENERGY[y] + - fixed_om_cost_per_mwhyr(gen[y]) * eTotalCapEnergy[y] + if y in NEW_CAP_ENERGY + # For resources with new energy capacity: investment cost + fixed O&M cost + EP[:eCInvEnergy][y] + EP[:eCFomEnergy][y] else - fixed_om_cost_per_mwhyr(gen[y]) * eTotalCapEnergy[y] + # For existing resources: only fixed O&M cost + EP[:eCFomEnergy][y] end) - + # Sum individual resource contributions to fixed costs to get total fixed costs @expression(EP, eTotalCFixEnergy, sum(EP[:eCFixEnergy][y] for y in STOR_ALL)) + @expression(EP, eTotalCInvEnergy, sum(EP[:eCInvEnergy][y] for y in STOR_ALL)) + @expression(EP, eTotalCFomEnergy, sum(EP[:eCFomEnergy][y] for y in STOR_ALL)) # Add term to objective function expression if MultiStage == 1 diff --git a/src/model/resources/vre_stor/vre_stor.jl b/src/model/resources/vre_stor/vre_stor.jl index 37960c9cb7..ede9fbd35f 100644 --- a/src/model/resources/vre_stor/vre_stor.jl +++ b/src/model/resources/vre_stor/vre_stor.jl @@ -478,18 +478,33 @@ function inverter_vre_stor!(EP::Model, inputs::Dict, setup::Dict) end) # 2. Objective function additions - - # Fixed costs for inverter component (if resource is not eligible for new inverter capacity, fixed costs are only O&M costs) + + # Annuitized investment cost expression for DC capacity - only applies to resources eligible for new DC capacity + @expression(EP, eCInvDC[y in DC], + if y in NEW_CAP_DC + by_rid(y, :inv_cost_inverter_per_mwyr) * vDCCAP[y] + else + 0 + end) + + # Fixed O&M cost expression for DC capacity - applies to all resources + @expression(EP, eCFomDC[y in DC], + by_rid(y, :fixed_om_inverter_cost_per_mwyr) * eTotalCap_DC[y]) + + # Total fixed cost expression for DC capacity - combines investment and fixed O&M costs @expression(EP, eCFixDC[y in DC], - if y in NEW_CAP_DC # Resources eligible for new capacity - by_rid(y, :inv_cost_inverter_per_mwyr) * vDCCAP[y] + - by_rid(y, :fixed_om_inverter_cost_per_mwyr) * eTotalCap_DC[y] + if y in NEW_CAP_DC + # For resources with new DC capacity: investment cost + fixed O&M cost + EP[:eCInvDC][y] + EP[:eCFomDC][y] else - by_rid(y, :fixed_om_inverter_cost_per_mwyr) * eTotalCap_DC[y] + # For existing resources: only fixed O&M cost + EP[:eCFomDC][y] end) - + # Sum individual resource contributions @expression(EP, eTotalCFixDC, sum(eCFixDC[y] for y in DC)) + @expression(EP, eTotalCInvDC, sum(eCInvDC[y] for y in DC)) + @expression(EP, eTotalCFomDC, sum(eCFomDC[y] for y in DC)) if MultiStage == 1 add_to_expression!(EP[:eObj], 1 / inputs["OPEXMULT"], eTotalCFixDC) @@ -644,16 +659,33 @@ function solar_vre_stor!(EP::Model, inputs::Dict, setup::Dict) # 2. Objective function additions - # Fixed costs for solar resources (if resource is not eligible for new solar capacity, fixed costs are only O&M costs) + # Annuitized investment cost expression for solar capacity - only applies to resources eligible for new solar capacity + @expression(EP, eCInvSolar[y in SOLAR], + if y in NEW_CAP_SOLAR + by_rid(y, :inv_cost_solar_per_mwyr) * vSOLARCAP[y] + else + 0 + end) + + # Fixed O&M cost expression for solar capacity - applies to all resources + @expression(EP, eCFomSolar[y in SOLAR], + by_rid(y, :fixed_om_solar_cost_per_mwyr) * eTotalCap_SOLAR[y]) + + # Total fixed cost expression for solar capacity - combines investment and fixed O&M costs @expression(EP, eCFixSolar[y in SOLAR], - if y in NEW_CAP_SOLAR # Resources eligible for new capacity - by_rid(y, :inv_cost_solar_per_mwyr) * vSOLARCAP[y] + - by_rid(y, :fixed_om_solar_cost_per_mwyr) * eTotalCap_SOLAR[y] + if y in NEW_CAP_SOLAR + # For resources with new solar capacity: investment cost + fixed O&M cost + EP[:eCInvSolar][y] + EP[:eCFomSolar][y] else - by_rid(y, :fixed_om_solar_cost_per_mwyr) * eTotalCap_SOLAR[y] + # For existing resources: only fixed O&M cost + EP[:eCFomSolar][y] end) + + # Sum individual resource contributions @expression(EP, eTotalCFixSolar, sum(eCFixSolar[y] for y in SOLAR)) - + @expression(EP, eTotalCInvSolar, sum(eCInvSolar[y] for y in SOLAR)) + @expression(EP, eTotalCFomSolar, sum(eCFomSolar[y] for y in SOLAR)) + if MultiStage == 1 add_to_expression!(EP[:eObj], 1 / inputs["OPEXMULT"], eTotalCFixSolar) else @@ -823,16 +855,32 @@ function wind_vre_stor!(EP::Model, inputs::Dict, setup::Dict) end) # 2. Objective function additions - - # Fixed costs for wind resources (if resource is not eligible for new wind capacity, fixed costs are only O&M costs) + + # Annuitized investment cost expression for wind capacity - only applies to resources eligible for new wind capacity + @expression(EP, eCInvWind[y in WIND], + if y in NEW_CAP_WIND + by_rid(y, :inv_cost_wind_per_mwyr) * vWINDCAP[y] + else + 0 + end) + # Fixed O&M cost expression for wind capacity - applies to all resources + @expression(EP, eCFomWind[y in WIND], + by_rid(y, :fixed_om_wind_cost_per_mwyr) * eTotalCap_WIND[y]) + + # Total fixed cost expression for wind capacity - combines investment and fixed O&M costs @expression(EP, eCFixWind[y in WIND], - if y in NEW_CAP_WIND # Resources eligible for new capacity - by_rid(y, :inv_cost_wind_per_mwyr) * vWINDCAP[y] + - by_rid(y, :fixed_om_wind_cost_per_mwyr) * eTotalCap_WIND[y] + if y in NEW_CAP_WIND + # For resources with new wind capacity: investment cost + fixed O&M cost + EP[:eCInvWind][y] + EP[:eCFomWind][y] else - by_rid(y, :fixed_om_wind_cost_per_mwyr) * eTotalCap_WIND[y] + # For existing resources: only fixed O&M cost + EP[:eCFomWind][y] end) + + # Sum individual resource contributions @expression(EP, eTotalCFixWind, sum(eCFixWind[y] for y in WIND)) + @expression(EP, eTotalCInvWind, sum(eCInvWind[y] for y in WIND)) + @expression(EP, eTotalCFomWind, sum(eCFomWind[y] for y in WIND)) if MultiStage == 1 add_to_expression!(EP[:eObj], 1 / inputs["OPEXMULT"], eTotalCFixWind) @@ -1104,15 +1152,27 @@ function stor_vre_stor!(EP::Model, inputs::Dict, setup::Dict) # 2. Objective function additions - # Fixed costs for storage resources (if resource is not eligible for new energy capacity, fixed costs are only O&M costs) + # Annuitized investment cost expression for storage energy capacity - only applies to resources eligible for new storage capacity + @expression(EP, eCInvEnergy_VS[y in STOR], + if y in NEW_CAP_STOR + inv_cost_per_mwhyr(gen[y]) * vCAPENERGY_VS[y] + else + 0 + end) + # Fixed O&M cost expression for storage energy capacity - applies to all resources based on total storage capacity + @expression(EP, eCFomEnergy_VS[y in STOR], + fixed_om_cost_per_mwhyr(gen[y]) * eTotalCap_STOR[y]) + # Total fixed cost expression for storage energy capacity - combines investment and fixed O&M costs @expression(EP, eCFixEnergy_VS[y in STOR], - if y in NEW_CAP_STOR # Resources eligible for new capacity - inv_cost_per_mwhyr(gen[y]) * vCAPENERGY_VS[y] + - fixed_om_cost_per_mwhyr(gen[y]) * eTotalCap_STOR[y] + if y in NEW_CAP_STOR + EP[:eCInvEnergy_VS][y] + EP[:eCFomEnergy_VS][y] else - fixed_om_cost_per_mwhyr(gen[y]) * eTotalCap_STOR[y] + EP[:eCFomEnergy_VS][y] end) + @expression(EP, eTotalCFixStor, sum(eCFixEnergy_VS[y] for y in STOR)) + @expression(EP, eTotalCInvStor, sum(eCInvEnergy_VS[y] for y in STOR)) + @expression(EP, eTotalCFomStor, sum(eCFomEnergy_VS[y] for y in STOR)) if MultiStage == 1 add_to_expression!(EP[:eObj], 1 / inputs["OPEXMULT"], eTotalCFixStor) @@ -1434,15 +1494,27 @@ function elec_vre_stor!(EP::Model, inputs::Dict, setup::Dict) # 2. Objective function additions - # Fixed costs for electrolyzer resources (if resource is not eligible for new electrolyzer capacity, fixed costs are only O&M costs) + # Annuitized investment cost expression for electrolyzer capacity - only applies to resources eligible for new electrolyzer capacity + @expression(EP, eCInvElec[y in ELEC], + if y in NEW_CAP_ELEC + by_rid(y, :inv_cost_elec_per_mwyr) * vELECCAP[y] + else + 0 + end) + # Fixed O&M cost expression for electrolyzer capacity - applies to all resources based on total electrolyzer capacity + @expression(EP, eCFomElec[y in ELEC], + by_rid(y, :fixed_om_elec_cost_per_mwyr) * eTotalCap_ELEC[y]) + # Total fixed cost expression for electrolyzer capacity - combines investment and fixed O&M costs @expression(EP, eCFixElec[y in ELEC], - if y in NEW_CAP_ELEC # Resources eligible for new capacity - by_rid(y, :inv_cost_elec_per_mwyr) * vELECCAP[y] + - by_rid(y, :fixed_om_elec_cost_per_mwyr) * eTotalCap_ELEC[y] + if y in NEW_CAP_ELEC + EP[:eCInvElec][y] + EP[:eCFomElec][y] else - by_rid(y, :fixed_om_elec_cost_per_mwyr) * eTotalCap_ELEC[y] + EP[:eCFomElec][y] end) + @expression(EP, eTotalCFixElec, sum(eCFixElec[y] for y in ELEC)) + @expression(EP, eTotalCInvElec, sum(eCInvElec[y] for y in ELEC)) + @expression(EP, eTotalCFomElec, sum(eCFomElec[y] for y in ELEC)) if MultiStage == 1 add_to_expression!(EP[:eObj], 1 / inputs["OPEXMULT"], eTotalCFixElec) @@ -1826,19 +1898,36 @@ function investment_charge_vre_stor!(EP::Model, inputs::Dict, setup::Dict) # 2. Objective Function Additions - # If resource is not eligible for new discharge DC capacity, fixed costs are only O&M costs - @expression(EP, eCFixDischarge_DC[y in VS_ASYM_DC_DISCHARGE], + # Annuitized investment cost expression for storage discharge DC capacity - only applies to resources eligible for new capacity + @expression(EP, eCInvDischarge_DC[y in VS_ASYM_DC_DISCHARGE], if y in NEW_CAP_DISCHARGE_DC # Resources eligible for new discharge DC capacity - by_rid(y, :inv_cost_discharge_dc_per_mwyr) * vCAPDISCHARGE_DC[y] + - by_rid(y, :fixed_om_cost_discharge_dc_per_mwyr) * eTotalCapDischarge_DC[y] + by_rid(y, :inv_cost_discharge_dc_per_mwyr) * vCAPDISCHARGE_DC[y] else - by_rid(y, :fixed_om_cost_discharge_dc_per_mwyr) * eTotalCapDischarge_DC[y] + 0 end) + # Fixed O&M cost expression for storage discharge DC capacity - applies to all resources + @expression(EP, eCFomDischarge_DC[y in VS_ASYM_DC_DISCHARGE], + by_rid(y, :fixed_om_cost_discharge_dc_per_mwyr) * eTotalCapDischarge_DC[y]) + + # Total fixed cost expression for storage discharge DC capacity - combines investment and fixed O&M costs + @expression(EP, eCFixDischarge_DC[y in VS_ASYM_DC_DISCHARGE], + if y in NEW_CAP_DISCHARGE_DC + EP[:eCInvDischarge_DC][y] + EP[:eCFomDischarge_DC][y] + else + EP[:eCFomDischarge_DC][y] + end) + # Sum individual resource contributions to fixed costs to get total fixed costs @expression(EP, eTotalCFixDischarge_DC, sum(EP[:eCFixDischarge_DC][y] for y in VS_ASYM_DC_DISCHARGE)) + @expression(EP, + eTotalCInvDischarge_DC, + sum(EP[:eCInvDischarge_DC][y] for y in VS_ASYM_DC_DISCHARGE)) + @expression(EP, + eTotalCFomDischarge_DC, + sum(EP[:eCFomDischarge_DC][y] for y in VS_ASYM_DC_DISCHARGE)) if MultiStage == 1 add_to_expression!(EP[:eObj], 1 / inputs["OPEXMULT"], eTotalCFixDischarge_DC) @@ -1926,20 +2015,37 @@ function investment_charge_vre_stor!(EP::Model, inputs::Dict, setup::Dict) # 2. Objective Function Additions - # If resource is not eligible for new charge DC capacity, fixed costs are only O&M costs + # Annuitized investment cost expression for storage charge DC capacity - only applies to resources eligible for new capacity + @expression(EP, eCInvCharge_DC[y in VS_ASYM_DC_CHARGE], + if y in NEW_CAP_CHARGE_DC + by_rid(y, :inv_cost_charge_dc_per_mwyr) * vCAPCHARGE_DC[y] + else + 0 + end) + + # Fixed O&M cost expression for storage charge DC capacity - applies to all resources + @expression(EP, eCFomCharge_DC[y in VS_ASYM_DC_CHARGE], + by_rid(y, :fixed_om_cost_charge_dc_per_mwyr) * eTotalCapCharge_DC[y]) + + # Total fixed cost expression for storage charge DC capacity - combines investment and fixed O&M costs @expression(EP, eCFixCharge_DC[y in VS_ASYM_DC_CHARGE], - if y in NEW_CAP_CHARGE_DC # Resources eligible for new charge DC capacity - by_rid(y, :inv_cost_charge_dc_per_mwyr) * vCAPCHARGE_DC[y] + - by_rid(y, :fixed_om_cost_charge_dc_per_mwyr) * eTotalCapCharge_DC[y] + if y in NEW_CAP_CHARGE_DC + EP[:eCInvCharge_DC][y] + EP[:eCFomCharge_DC][y] else - by_rid(y, :fixed_om_cost_charge_dc_per_mwyr) * eTotalCapCharge_DC[y] + EP[:eCFomCharge_DC][y] end) # Sum individual resource contributions to fixed costs to get total fixed costs @expression(EP, eTotalCFixCharge_DC, sum(EP[:eCFixCharge_DC][y] for y in VS_ASYM_DC_CHARGE)) - + @expression(EP, + eTotalCInvCharge_DC, + sum(EP[:eCInvCharge_DC][y] for y in VS_ASYM_DC_CHARGE)) + @expression(EP, + eTotalCFomCharge_DC, + sum(EP[:eCFomCharge_DC][y] for y in VS_ASYM_DC_CHARGE)) + if MultiStage == 1 add_to_expression!(EP[:eObj], 1 / inputs["OPEXMULT"], eTotalCFixCharge_DC) else @@ -2030,20 +2136,36 @@ function investment_charge_vre_stor!(EP::Model, inputs::Dict, setup::Dict) # 2. Objective Function Additions - # If resource is not eligible for new discharge AC capacity, fixed costs are only O&M costs + # Annuitized investment cost expression for storage discharge AC capacity - only applies to resources eligible for new capacity + @expression(EP, eCInvDischarge_AC[y in VS_ASYM_AC_DISCHARGE], + if y in NEW_CAP_DISCHARGE_AC + by_rid(y, :inv_cost_discharge_ac_per_mwyr) * vCAPDISCHARGE_AC[y] + else + 0 + end) + + # Fixed O&M cost expression for storage discharge AC capacity - applies to all resources + @expression(EP, eCFomDischarge_AC[y in VS_ASYM_AC_DISCHARGE], + by_rid(y, :fixed_om_cost_discharge_ac_per_mwyr) * eTotalCapDischarge_AC[y]) + + # Total fixed cost expression for storage discharge AC capacity - combines investment and fixed O&M costs @expression(EP, eCFixDischarge_AC[y in VS_ASYM_AC_DISCHARGE], - if y in NEW_CAP_DISCHARGE_AC # Resources eligible for new discharge AC capacity - by_rid(y, :inv_cost_discharge_ac_per_mwyr) * vCAPDISCHARGE_AC[y] + - by_rid(y, :fixed_om_cost_discharge_ac_per_mwyr) * eTotalCapDischarge_AC[y] + if y in NEW_CAP_DISCHARGE_AC + EP[:eCInvDischarge_AC][y] + EP[:eCFomDischarge_AC][y] else - by_rid(y, :fixed_om_cost_discharge_ac_per_mwyr) * eTotalCapDischarge_AC[y] + EP[:eCFomDischarge_AC][y] end) # Sum individual resource contributions to fixed costs to get total fixed costs @expression(EP, eTotalCFixDischarge_AC, sum(EP[:eCFixDischarge_AC][y] for y in VS_ASYM_AC_DISCHARGE)) - + @expression(EP, + eTotalCInvDischarge_AC, + sum(EP[:eCInvDischarge_AC][y] for y in VS_ASYM_AC_DISCHARGE)) + @expression(EP, + eTotalCFomDischarge_AC, + sum(EP[:eCFomDischarge_AC][y] for y in VS_ASYM_AC_DISCHARGE)) if MultiStage == 1 add_to_expression!(EP[:eObj], 1 / inputs["OPEXMULT"], eTotalCFixDischarge_AC) else @@ -2130,19 +2252,36 @@ function investment_charge_vre_stor!(EP::Model, inputs::Dict, setup::Dict) # 2. Objective Function Additions - # If resource is not eligible for new charge AC capacity, fixed costs are only O&M costs - @expression(EP, eCFixCharge_AC[y in VS_ASYM_AC_CHARGE], + # Annuitized investment cost expression for storage charge AC capacity - only applies to resources eligible for new capacity + @expression(EP, eCInvCharge_AC[y in VS_ASYM_AC_CHARGE], if y in NEW_CAP_CHARGE_AC # Resources eligible for new charge AC capacity - by_rid(y, :inv_cost_charge_ac_per_mwyr) * vCAPCHARGE_AC[y] + - by_rid(y, :fixed_om_cost_charge_ac_per_mwyr) * eTotalCapCharge_AC[y] + by_rid(y, :inv_cost_charge_ac_per_mwyr) * vCAPCHARGE_AC[y] else - by_rid(y, :fixed_om_cost_charge_ac_per_mwyr) * eTotalCapCharge_AC[y] + 0 end) + # Fixed O&M cost expression for storage charge AC capacity - applies to all resources + @expression(EP, eCFomCharge_AC[y in VS_ASYM_AC_CHARGE], + by_rid(y, :fixed_om_cost_charge_ac_per_mwyr) * eTotalCapCharge_AC[y]) + + # Total fixed cost expression for storage charge AC capacity - combines investment and fixed O&M costs + @expression(EP, eCFixCharge_AC[y in VS_ASYM_AC_CHARGE], + if y in NEW_CAP_CHARGE_AC + EP[:eCInvCharge_AC][y] + EP[:eCFomCharge_AC][y] + else + EP[:eCFomCharge_AC][y] + end) + # Sum individual resource contributions to fixed costs to get total fixed costs @expression(EP, eTotalCFixCharge_AC, sum(EP[:eCFixCharge_AC][y] for y in VS_ASYM_AC_CHARGE)) + @expression(EP, + eTotalCInvCharge_AC, + sum(EP[:eCInvCharge_AC][y] for y in VS_ASYM_AC_CHARGE)) + @expression(EP, + eTotalCFomCharge_AC, + sum(EP[:eCFomCharge_AC][y] for y in VS_ASYM_AC_CHARGE)) if MultiStage == 1 add_to_expression!(EP[:eObj], 1 / inputs["OPEXMULT"], eTotalCFixCharge_AC) diff --git a/src/write_outputs/write_costs.jl b/src/write_outputs/write_costs.jl index fb12d2d3bd..6d65330f07 100644 --- a/src/write_outputs/write_costs.jl +++ b/src/write_outputs/write_costs.jl @@ -25,7 +25,9 @@ function write_costs(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) "cUnmetRsv", "cNetworkExp", "cUnmetPolicyPenalty", - "cCO2" + "cCO2", + "cInv", + "cFom" ] if !isempty(VRE_STOR) push!(cost_list, "cGridConnection") @@ -41,13 +43,24 @@ function write_costs(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) cFix = value(EP[:eTotalCFix]) + (!isempty(inputs["STOR_ALL"]) ? value(EP[:eTotalCFixEnergy]) : 0.0) + (!isempty(inputs["STOR_ASYMMETRIC"]) ? value(EP[:eTotalCFixCharge]) : 0.0) - + cInv = value(EP[:eTotalCInv]) + + (!isempty(inputs["STOR_ALL"]) ? value(EP[:eTotalCInvEnergy]) : 0.0) + + (!isempty(inputs["STOR_ASYMMETRIC"]) ? value(EP[:eTotalCInvCharge]) : 0.0) + cFom = value(EP[:eTotalCFom]) + + (!isempty(inputs["STOR_ALL"]) ? value(EP[:eTotalCFomEnergy]) : 0.0) + + (!isempty(inputs["STOR_ASYMMETRIC"]) ? value(EP[:eTotalCFomCharge]) : 0.0) cFuel = value.(EP[:eTotalCFuelOut]) if !isempty(VRE_STOR) cFix += ((!isempty(inputs["VS_DC"]) ? value(EP[:eTotalCFixDC]) : 0.0) + (!isempty(inputs["VS_SOLAR"]) ? value(EP[:eTotalCFixSolar]) : 0.0) + (!isempty(inputs["VS_WIND"]) ? value(EP[:eTotalCFixWind]) : 0.0)) + cInv += ((!isempty(inputs["VS_DC"]) ? value(EP[:eTotalCInvDC]) : 0.0) + + (!isempty(inputs["VS_SOLAR"]) ? value(EP[:eTotalCInvSolar]) : 0.0) + + (!isempty(inputs["VS_WIND"]) ? value(EP[:eTotalCInvWind]) : 0.0)) + cFom += ((!isempty(inputs["VS_DC"]) ? value(EP[:eTotalCFomDC]) : 0.0) + + (!isempty(inputs["VS_SOLAR"]) ? value(EP[:eTotalCFomSolar]) : 0.0) + + (!isempty(inputs["VS_WIND"]) ? value(EP[:eTotalCFomWind]) : 0.0)) cVar += ((!isempty(inputs["VS_SOLAR"]) ? value(EP[:eTotalCVarOutSolar]) : 0.0) + (!isempty(inputs["VS_WIND"]) ? value(EP[:eTotalCVarOutWind]) : 0.0)) if !isempty(inputs["VS_STOR"]) @@ -60,6 +73,25 @@ function write_costs(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) value(EP[:eTotalCFixCharge_AC]) : 0.0) + (!isempty(inputs["VS_ASYM_AC_DISCHARGE"]) ? value(EP[:eTotalCFixDischarge_AC]) : 0.0)) + cInv += ((!isempty(inputs["VS_STOR"]) ? + value(EP[:eTotalCInvStor]) : 0.0) + + (!isempty(inputs["VS_ASYM_DC_CHARGE"]) ? + value(EP[:eTotalCInvCharge_DC]) : 0.0) + + (!isempty(inputs["VS_ASYM_DC_DISCHARGE"]) ? + value(EP[:eTotalCInvDischarge_DC]) : 0.0) + + (!isempty(inputs["VS_ASYM_AC_CHARGE"]) ? + value(EP[:eTotalCInvCharge_AC]) : 0.0) + + (!isempty(inputs["VS_ASYM_AC_DISCHARGE"]) ? + value(EP[:eTotalCInvDischarge_AC]) : 0.0)) + cFom += ((!isempty(inputs["VS_STOR"]) ? value(EP[:eTotalCFomStor]) : 0.0) + + (!isempty(inputs["VS_ASYM_DC_CHARGE"]) ? + value(EP[:eTotalCFomCharge_DC]) : 0.0) + + (!isempty(inputs["VS_ASYM_DC_DISCHARGE"]) ? + value(EP[:eTotalCFomDischarge_DC]) : 0.0) + + (!isempty(inputs["VS_ASYM_AC_CHARGE"]) ? + value(EP[:eTotalCFomCharge_AC]) : 0.0) + + (!isempty(inputs["VS_ASYM_AC_DISCHARGE"]) ? + value(EP[:eTotalCFomDischarge_AC]) : 0.0)) cVar += (!isempty(inputs["VS_STOR"]) ? value(EP[:eTotalCVarStor]) : 0.0) end total_cost = [ @@ -73,6 +105,8 @@ function write_costs(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) 0.0, 0.0, 0.0, + cInv, + cFom, 0.0 ] else @@ -86,7 +120,9 @@ function write_costs(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) 0.0, 0.0, 0.0, - 0.0 + 0.0, + cInv, + cFom ] end @@ -96,10 +132,6 @@ function write_costs(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) dfCost[!, Symbol("Total")] = total_cost - if setup["ParameterScale"] == 1 - dfCost.Total *= ModelScalingFactor^2 - end - if setup["UCommit"] >= 1 dfCost[6, 2] = value(EP[:eTotalCStart]) + value(EP[:eTotalCFuelStart]) end @@ -137,20 +169,16 @@ function write_costs(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) end if !isempty(VRE_STOR) - dfCost[!, 2][11] = value(EP[:eTotalCGrid]) * - (setup["ParameterScale"] == 1 ? ModelScalingFactor^2 : 1) + dfCost[13, 2] = value(EP[:eTotalCGrid]) end if any(co2_capture_fraction.(gen) .!= 0) dfCost[10, 2] += value(EP[:eTotaleCCO2Sequestration]) end + # if setup["ParameterScale"] == 1 - dfCost[6, 2] *= ModelScalingFactor^2 - dfCost[7, 2] *= ModelScalingFactor^2 - dfCost[8, 2] *= ModelScalingFactor^2 - dfCost[9, 2] *= ModelScalingFactor^2 - dfCost[10, 2] *= ModelScalingFactor^2 + dfCost.Total *= ModelScalingFactor^2 end for z in 1:Z @@ -162,6 +190,8 @@ function write_costs(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) tempCNSE = 0.0 tempHydrogenValue = 0.0 tempCCO2 = 0.0 + tempCInv = 0.0 + tempCFom = 0.0 Y_ZONE = resources_in_zone_by_rid(gen, z) STOR_ALL_ZONE = intersect(inputs["STOR_ALL"], Y_ZONE) @@ -173,6 +203,11 @@ function write_costs(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) eCFix = sum(value.(EP[:eCFix][Y_ZONE]), init = 0.0) tempCFix += eCFix + eCInv = sum(value.(EP[:eCInv][Y_ZONE]), init = 0.0) + tempCInv+= eCInv + eCFom = sum(value.(EP[:eCFom][Y_ZONE]), init = 0.0) + tempCFom += eCFom + tempCTotal += eCFix tempCVar = sum(value.(EP[:eCVar_out][Y_ZONE, :])) @@ -186,11 +221,20 @@ function write_costs(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) tempCVar += eCVar_in eCFixEnergy = sum(value.(EP[:eCFixEnergy][STOR_ALL_ZONE])) tempCFix += eCFixEnergy + eCInvEnergy = sum(value.(EP[:eCInvEnergy][STOR_ALL_ZONE])) + tempCInv += eCInvEnergy + eCFomEnergy = sum(value.(EP[:eCFomEnergy][STOR_ALL_ZONE])) + tempCFom += eCFomEnergy + tempCTotal += eCVar_in + eCFixEnergy end if !isempty(STOR_ASYMMETRIC_ZONE) eCFixCharge = sum(value.(EP[:eCFixCharge][STOR_ASYMMETRIC_ZONE])) tempCFix += eCFixCharge + eCInvCharge = sum(value.(EP[:eCInvCharge][STOR_ASYMMETRIC_ZONE])) + tempCInv += eCInvCharge + eCFomCharge = sum(value.(EP[:eCFomCharge][STOR_ASYMMETRIC_ZONE])) + tempCFom += eCFomCharge tempCTotal += eCFixCharge end if !isempty(FLEX_ZONE) @@ -246,6 +290,68 @@ function write_costs(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) end tempCFix += eCFix_VRE_STOR + # Investment Costs + eCInv_VRE_STOR = 0.0 + if !isempty(SOLAR_ZONE_VRE_STOR) + eCInv_VRE_STOR += sum(value.(EP[:eCInvSolar][SOLAR_ZONE_VRE_STOR])) + end + if !isempty(WIND_ZONE_VRE_STOR) + eCInv_VRE_STOR += sum(value.(EP[:eCInvWind][WIND_ZONE_VRE_STOR])) + end + if !isempty(ELEC_ZONE_VRE_STOR) + eCInv_VRE_STOR += sum(value.(EP[:eCInvElec][ELEC_ZONE_VRE_STOR])) + end + if !isempty(DC_ZONE_VRE_STOR) + eCInv_VRE_STOR += sum(value.(EP[:eCInvDC][DC_ZONE_VRE_STOR])) + end + if !isempty(STOR_ALL_ZONE_VRE_STOR) + eCInv_VRE_STOR += sum(value.(EP[:eCInvEnergy_VS][STOR_ALL_ZONE_VRE_STOR])) + if !isempty(DC_CHARGE_ALL_ZONE_VRE_STOR) + eCInv_VRE_STOR += sum(value.(EP[:eCInvCharge_DC][DC_CHARGE_ALL_ZONE_VRE_STOR])) + end + if !isempty(DC_DISCHARGE_ALL_ZONE_VRE_STOR) + eCInv_VRE_STOR += sum(value.(EP[:eCInvDischarge_DC][DC_DISCHARGE_ALL_ZONE_VRE_STOR])) + end + if !isempty(AC_DISCHARGE_ALL_ZONE_VRE_STOR) + eCInv_VRE_STOR += sum(value.(EP[:eCInvDischarge_AC][AC_DISCHARGE_ALL_ZONE_VRE_STOR])) + end + if !isempty(AC_CHARGE_ALL_ZONE_VRE_STOR) + eCInv_VRE_STOR += sum(value.(EP[:eCInvCharge_AC][AC_CHARGE_ALL_ZONE_VRE_STOR])) + end + end + tempCInv += eCInv_VRE_STOR + + # Fom Costs + eCFom_VRE_STOR = 0.0 + if !isempty(SOLAR_ZONE_VRE_STOR) + eCFom_VRE_STOR += sum(value.(EP[:eCFomSolar][SOLAR_ZONE_VRE_STOR])) + end + if !isempty(WIND_ZONE_VRE_STOR) + eCFom_VRE_STOR += sum(value.(EP[:eCFomWind][WIND_ZONE_VRE_STOR])) + end + if !isempty(ELEC_ZONE_VRE_STOR) + eCFom_VRE_STOR += sum(value.(EP[:eCFomElec][ELEC_ZONE_VRE_STOR])) + end + if !isempty(DC_ZONE_VRE_STOR) + eCFom_VRE_STOR += sum(value.(EP[:eCFomDC][DC_ZONE_VRE_STOR])) + end + if !isempty(STOR_ALL_ZONE_VRE_STOR) + eCFom_VRE_STOR += sum(value.(EP[:eCFomEnergy_VS][STOR_ALL_ZONE_VRE_STOR])) + if !isempty(DC_CHARGE_ALL_ZONE_VRE_STOR) + eCFom_VRE_STOR += sum(value.(EP[:eCFomCharge_DC][DC_CHARGE_ALL_ZONE_VRE_STOR])) + end + if !isempty(DC_DISCHARGE_ALL_ZONE_VRE_STOR) + eCFom_VRE_STOR += sum(value.(EP[:eCFomDischarge_DC][DC_DISCHARGE_ALL_ZONE_VRE_STOR])) + end + if !isempty(AC_DISCHARGE_ALL_ZONE_VRE_STOR) + eCFom_VRE_STOR += sum(value.(EP[:eCFomDischarge_AC][AC_DISCHARGE_ALL_ZONE_VRE_STOR])) + end + if !isempty(AC_CHARGE_ALL_ZONE_VRE_STOR) + eCFom_VRE_STOR += sum(value.(EP[:eCFomCharge_AC][AC_CHARGE_ALL_ZONE_VRE_STOR])) + end + end + tempCFom += eCFom_VRE_STOR + # Variable Costs eCVar_VRE_STOR = 0.0 if !isempty(SOLAR_ZONE_VRE_STOR) @@ -326,6 +432,8 @@ function write_costs(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) tempCStart *= ModelScalingFactor^2 tempHydrogenValue *= ModelScalingFactor^2 tempCCO2 *= ModelScalingFactor^2 + tempCInv *= ModelScalingFactor^2 + tempCFom *= ModelScalingFactor^2 end temp_cost_list = [ tempCTotal, @@ -337,7 +445,9 @@ function write_costs(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) "-", "-", "-", - tempCCO2 + tempCCO2, + tempCInv, + tempCFom ] if !isempty(VRE_STOR) push!(temp_cost_list, "-") diff --git a/test/writing_outputs/test_zone_no_resources.jl b/test/writing_outputs/test_zone_no_resources.jl index 8b19423fc4..2fc1a04a23 100644 --- a/test/writing_outputs/test_zone_no_resources.jl +++ b/test/writing_outputs/test_zone_no_resources.jl @@ -19,16 +19,18 @@ end function prepare_costs_true() df = DataFrame( - ["cTotal" 5.177363815260002e12 4.027191550200002e12 1.1501722650599993e12; - "cFix" 0.0 0.0 0.0; - "cVar" 5.849292224195126e-8 0.0 5.849292224195126e-8; - "cFuel" 0.0 0.0 0.0; - "cNSE" 5.177363815260002e12 4.027191550200002e12 1.1501722650599993e12; - "cStart" 0.0 0.0 0.0; - "cUnmetRsv" 0.0 0.0 0.0; - "cNetworkExp" 0.0 0.0 0.0; - "cUnmetPolicyPenalty" 0.0 0.0 0.0; - "cCO2" 0.0 0.0 0.0], + ["cTotal" 1.463768296693398e12 1.4588919221877012e12 4.840797505702658e9; + "cFix" 4.727103377348504e9 0.0 4.727103377348504e9; + "cVar" 2.3347293240589075e7 0.0 2.3347293240589064e7; + "cFuel" 4.4718223470249504e7 0.0 4.4718223470249504e7; + "cNSE" 1.4588919221877012e12 1.4588919221877012e12 0.0; + "cStart" 4.5628611643315025e7 0.0 4.5628611643315025e7; + "cUnmetRsv" 0.0 0.0 0.0; + "cNetworkExp" 3.5577e7 0.0 0.0; + "cUnmetPolicyPenalty" 0.0 0.0 0.0; + "cCO2" 0.0 0.0 0.0; + "cInv" 3.4603915632743273e9 0.0 3.4603915632743273e9; + "cFom" 1.266711814074177e9 0.0 1.266711814074177e9], [:Costs, :Total, :Zone1, :Zone2]) df[!, :Costs] = convert(Vector{String}, df[!, :Costs]) @@ -40,7 +42,7 @@ end function test_case() test_path = joinpath(@__DIR__, "zone_no_resources") - obj_true = 5.1773638153e12 + obj_true = 1.463768296693398e12 costs_true = prepare_costs_true() # Define test setup @@ -60,11 +62,12 @@ function test_case() optimal_tol = optimal_tol_rel * obj_test # Convert to absolute tolerance # Test the objective value - test_result = @test obj_test≈obj_true atol=optimal_tol + @test obj_test≈obj_true atol=optimal_tol # Test the costs costs_test = prepare_costs_test(test_path, inputs, genx_setup, EP) - test_result = @test costs_test[!, Not(:Costs)] ≈ costs_true[!, Not(:Costs)] + @test costs_test[!, Not(:Costs)] ≈ costs_true[!, Not(:Costs)] + @test Vector(costs_test[2, 2:end]) ≈ (Vector(costs_true[end-1, 2:end]) + Vector(costs_true[end, 2:end])) # Remove the costs file rm(joinpath(test_path, "costs.csv")) diff --git a/test/writing_outputs/zone_no_resources/resources/Vre.csv b/test/writing_outputs/zone_no_resources/resources/Vre.csv index 3f32d9b84b..dbf6162be9 100644 --- a/test/writing_outputs/zone_no_resources/resources/Vre.csv +++ b/test/writing_outputs/zone_no_resources/resources/Vre.csv @@ -1,3 +1,3 @@ -Resource,Zone,Num_VRE_Bins,New_Build,Can_Retire,Existing_Cap_MW,Max_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,Reg_Max,Rsv_Max,Reg_Cost,Rsv_Cost,region,cluster -CT_onshore_wind,2,1,0,0,0,-1,0,97200,43205,0.1,0,0,0,0,CT,1 -CT_solar_pv,2,1,0,0,0,-1,0,85300,18760,0,0,0,0,0,CT,1 \ No newline at end of file +Resource,Zone,Num_VRE_Bins,New_Build,Can_Retire,Existing_Cap_MW,Max_Cap_MW,Min_Cap_MW,Inv_Cost_per_MWyr,Fixed_OM_Cost_per_MWyr,Var_OM_Cost_per_MWh,region,cluster +CT_onshore_wind,2,1,1,1,0,-1,0,97200,43205,0.1,CT,1 +CT_solar_pv,2,1,1,1,0,-1,0,85300,18760,0,CT,1 \ No newline at end of file From 297e1783fcd16c28f9b59490a1b0776338dbe72e Mon Sep 17 00:00:00 2001 From: lbonaldo <39280783+lbonaldo@users.noreply.github.com> Date: Mon, 14 Jul 2025 10:59:28 +0200 Subject: [PATCH 3/5] Update documentation for `costs.csv` output file --- docs/src/User_Guide/model_output.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/src/User_Guide/model_output.md b/docs/src/User_Guide/model_output.md index a1534b48de..b0be5769d2 100644 --- a/docs/src/User_Guide/model_output.md +++ b/docs/src/User_Guide/model_output.md @@ -40,12 +40,17 @@ Reports optimal objective function value and contribution of each term by zone. | cTotal |Total objective function value |USD | | cFix |Total annualized investment and fixed operating & maintainenance (FOM) costs associated with all resources |USD | | cVar |Total annual variable cost associated with all resources; includes fuel costs for thermal plants |USD | +| cFuel |System level total fuel cost |USD | | cNSE |Total annual cost of non-served energy |USD | | cStart |Total annual cost of start-up of thermal power plants| USD | | cUnmetRsv |Total annual cost of not meeting time-dependent operating reserve (spinning) requirements |USD | | cNetworkExp |Total cost of network expansion |USD | -| cEmissionsRevenue |Total and zonal emissions revenue |USD | -| cEmissionsCost |Total an zonal emissions cost |USD | +| cUnmetPolicyPenalty |Total annual cost of not meeting policy-based requirements (e.g. ESR, Capacity Reserve Margin, CO2 emissions, etc.) |USD | +| cCO2 |Total annual cost of CO2 sequestration |USD | +| cInv |Total annualized investment costs associated with all resources |USD | +| cFom |Total annual fixed operating & maintainenance (FOM) costs associated with all resources |USD | +| cGridConnection |Total annual cost of grid connection for VRE-storage resources |USD | +| cHydrogenRevenue |Total annual revenue from hydrogen sales |USD | ### 1.3 emissions.csv From f3563b654be85858f94dc9b6dfba1a3c2acd0e83 Mon Sep 17 00:00:00 2001 From: lbonaldo <39280783+lbonaldo@users.noreply.github.com> Date: Mon, 14 Jul 2025 10:59:36 +0200 Subject: [PATCH 4/5] Update CHANGELOG.md --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a297d69b9..5a557ea2f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added +- Add `eCFom` and `eCInv` to track FOM and investment costs separately (#809). + ### Changed - Rename `Transmission_NetExport` to `Transmission_NetImport` in `power_balance.csv` (#853). @@ -19,7 +22,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Models running with, non-default, solvers Cbc and Clp will fail unless `"EnableJuMPDirectMode"` is set to false (#835). - Improve `@expressions` performance by pre-processing sets (#815). -- Add `eCFom` and `eCInv` to track FOM and investment costs separately (#809). ### Fixed - Fix call to `get_retirement_stage` by casting `lifetime` to integer (#840). From 5b20bc788dff2a85e02bfa10d5839ca3952e8c48 Mon Sep 17 00:00:00 2001 From: lbonaldo <39280783+lbonaldo@users.noreply.github.com> Date: Mon, 14 Jul 2025 11:31:02 +0200 Subject: [PATCH 5/5] Implement eCFix cost separation in Allam cycle module --- .../resources/flexible_ccs/allamcyclelox.jl | 43 ++++++++++++++----- src/write_outputs/write_costs.jl | 4 ++ 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/model/resources/flexible_ccs/allamcyclelox.jl b/src/model/resources/flexible_ccs/allamcyclelox.jl index 49f8efb6fb..ef63892e52 100644 --- a/src/model/resources/flexible_ccs/allamcyclelox.jl +++ b/src/model/resources/flexible_ccs/allamcyclelox.jl @@ -172,27 +172,48 @@ function allamcyclelox!(EP::Model, inputs::Dict, setup::Dict) # LOX storage tank capacity -> if they are not in WITH_LOX @constraint(EP, [y in setdiff(ALLAM_CYCLE_LOX, WITH_LOX)], eTotalCap_AllamcycleLOX[y,lox] == 0 ) - # Fixed cost of each component in Allam Cycle w/ LOX - # Set of generator eligible for new sCO2 turbine - # Allam Cycle is eligible for unit commitment - @expression(EP, eCFix_Allam[y in ALLAM_CYCLE_LOX, i in 1:3], + + ## Objective Function Expressions ## + + # Annuitized investment cost expression - only applies to resources eligible for new capacity + @expression(EP, eCInv_Allam[y in ALLAM_CYCLE_LOX, i in 1:3], if y in NEW_CAP_Allam # Resources eligible for new capacity if y in COMMIT_Allam # Resource eligible for Unit commitment - allam_dict[y,"inv_cost"][i] * allam_dict[y,"cap_size"][i] * EP[:vCAP_AllamCycleLOX][y, i]+ - allam_dict[y,"fom_cost"][i] * eTotalCap_AllamcycleLOX[y,i] + allam_dict[y,"inv_cost"][i] * allam_dict[y,"cap_size"][i] * EP[:vCAP_AllamCycleLOX][y, i] else - allam_dict[y,"inv_cost"][i] * EP[:vCAP_AllamCycleLOX][y, i]+ - allam_dict[y,"fom_cost"][i] * eTotalCap_AllamcycleLOX[y,i] + allam_dict[y,"inv_cost"][i] * EP[:vCAP_AllamCycleLOX][y, i] end else - allam_dict[y,"fom_cost"][i] * eTotalCap_AllamcycleLOX[y,i] + 0 + end) + + # Fixed O&M cost expression - applies to all resources + @expression(EP, eCFom_Allam[y in ALLAM_CYCLE_LOX, i in 1:3], + allam_dict[y,"fom_cost"][i] * eTotalCap_AllamcycleLOX[y,i]) + + # Total fixed cost expression - combines investment and fixed O&M costs + @expression(EP, eCFix_Allam[y in ALLAM_CYCLE_LOX, i in 1:3], + if y in NEW_CAP_Allam # Resources eligible for new capacity + # For resources with new capacity: investment cost + fixed O&M cost + EP[:eCInv_Allam][y,i] + EP[:eCFom_Allam][y,i] + else + # For existing resources: only fixed O&M cost + EP[:eCFom_Allam][y,i] end) - # connect eCFix_Allam_Plant to eCFix + # Sum individual resource contributions to fixed costs to get total fixed costs @expression(EP, eCFix_Allam_Plant[y in ALLAM_CYCLE_LOX], sum(EP[:eCFix_Allam][y,i] for i in 1:3)) + @expression(EP, eCInv_Allam_Plant[y in ALLAM_CYCLE_LOX], sum(EP[:eCInv_Allam][y,i] for i in 1:3)) + @expression(EP, eCFom_Allam_Plant[y in ALLAM_CYCLE_LOX], sum(EP[:eCFom_Allam][y,i] for i in 1:3)) + @expression(EP, eTotalCFix_Allam, sum(EP[:eCFix_Allam_Plant][y] for y in ALLAM_CYCLE_LOX )) - # add this to eTotalCFix + @expression(EP, eTotalCInv_Allam, sum(EP[:eCInv_Allam_Plant][y] for y in ALLAM_CYCLE_LOX)) + @expression(EP, eTotalCFom_Allam, sum(EP[:eCFom_Allam_Plant][y] for y in ALLAM_CYCLE_LOX)) + + # add this to eTotalCFix, eTotalCInv, and eTotalCFom add_to_expression!(EP[:eTotalCFix], eTotalCFix_Allam) + add_to_expression!(EP[:eTotalCInv], eTotalCInv_Allam) + add_to_expression!(EP[:eTotalCFom], eTotalCFom_Allam) # add to Obj add_to_expression!(EP[:eObj], eTotalCFix_Allam) diff --git a/src/write_outputs/write_costs.jl b/src/write_outputs/write_costs.jl index 6d65330f07..3a1fb02d4a 100644 --- a/src/write_outputs/write_costs.jl +++ b/src/write_outputs/write_costs.jl @@ -401,6 +401,10 @@ function write_costs(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) # Fixed Costs eCFix_Allam = sum(value.(EP[:eCFix_Allam_Plant][Y_ZONE_ALLAM_CYCLE_LOX])) tempCFix += eCFix_Allam + eCInv_Allam = sum(value.(EP[:eCInv_Allam_Plant][Y_ZONE_ALLAM_CYCLE_LOX])) + tempCInv += eCInv_Allam + eCFom_Allam = sum(value.(EP[:eCFom_Allam_Plant][Y_ZONE_ALLAM_CYCLE_LOX])) + tempCFom += eCFom_Allam # Variable Costs eCVar_Allam = sum(value.(EP[:eCVar_Allam][Y_ZONE_ALLAM_CYCLE_LOX])) tempCVar += eCVar_Allam